Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/fix imports #123

Merged
merged 2 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 43 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ pip install exchange-calendars-extensions
## Usage
Import the package and register extended exchange calendar classes with the `exchange_calendars` module.
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx

ecx.apply_extensions()
```
Expand All @@ -70,7 +70,7 @@ class `ecx.ExtendedExchangeCalendar`. This class inherits both from
`ecx.ExchangeCalendarExtensions` which defines the extended properties.

```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()
import exchange_calendars as ec

Expand All @@ -88,7 +88,7 @@ assert isinstance(calendar, ecx.ExchangeCalendarExtensions)
The original classes can be re-instated by calling `ecx.remove_extensions()`.

```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()
import exchange_calendars as ec

Expand Down Expand Up @@ -126,7 +126,7 @@ Extended exchange calendars provide the following calendars as properties:

For example,
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()
import exchange_calendars as ec

Expand All @@ -151,7 +151,7 @@ calendar, together with all regular holidays during the period.

Quarterly and monthly expiry days:
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()
import exchange_calendars as ec

Expand Down Expand Up @@ -179,7 +179,7 @@ dtype: object

Last trading days of months:
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()
import exchange_calendars as ec

Expand Down Expand Up @@ -222,11 +222,11 @@ in that month.

### Adding special days

The `exchange_calendars_extensions` module provides the methods `add_holiday(...)`, `add_special_open(...)`,
The `exchange_calendars_extensions.core` module provides the methods `add_holiday(...)`, `add_special_open(...)`,
`add_special_close(...)`, `add_monthly_expiry(...)` and `add_quarterly_expiry(...)` to add holidays and other types of
special days. For example,
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()
import exchange_calendars as ec

Expand All @@ -242,7 +242,7 @@ always added as regular holidays to allow for an individual name.

Adding special open or close days works similarly, but needs the respective special open or close time:
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx

ecx.apply_extensions()
import exchange_calendars as ec
Expand All @@ -254,15 +254,16 @@ calendar = ec.get_calendar('XLON')
assert '2022-12-28' in calendar.special_opens_all.holidays()
```

A more generic way to add a special day is via `add_day(...)` which takes either a `DaySpec` (holidays,
monthly/quarterly expiries) or `DaySpecWithTime` (special open/close days) Pydantic model:
A more generic way to add a special day is via `add_day(...)` which takes either a `DayProps` (holidays,
monthly/quarterly expiries) or `DayPropsWithTime` (special open/close days) Pydantic model:
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
from exchange_calendars_extensions.api.changes import DayProps, DayPropsWithTime
ecx.apply_extensions()
import exchange_calendars as ec

ecx.add_day('XLON', ecx.DaySpec(date='2022-12-27', type=ecx.DayType.HOLIDAY, name='Holiday'))
ecx.add_day('XLON', ecx.DaySpecWithTime(date='2022-12-28', type=ecx.DayType.SPECIAL_OPEN, name='Special Open', time='11:00'))
ecx.add_day('XLON', '2022-12-27', DayProps(type=ecx.DayType.HOLIDAY, name='Holiday'))
ecx.add_day('XLON', '2022-12-28', DayPropsWithTime(type=ecx.DayType.SPECIAL_OPEN, name='Special Open', time='11:00'))

calendar = ec.get_calendar('XLON')

Expand All @@ -273,12 +274,12 @@ assert '2022-12-28' in calendar.special_opens_all.holidays()

Thanks to Pydantic, an even easier way is to just use suitable dictionaries:
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()
import exchange_calendars as ec

ecx.add_day('XLON', {'date': '2022-12-27', 'type': 'holiday', 'name': 'Holiday'})
ecx.add_day('XLON', {'date': '2022-12-28', 'type': 'special_open', 'name': 'Special Open', 'time': '11:00'})
ecx.add_day('XLON', '2022-12-27', {'type': 'holiday', 'name': 'Holiday'})
ecx.add_day('XLON', '2022-12-28', {'type': 'special_open', 'name': 'Special Open', 'time': '11:00'})

calendar = ec.get_calendar('XLON')

Expand All @@ -292,7 +293,7 @@ The dictionary format makes it particularly easy to read in changes from an exte

To remove a day as a special day (of any type) from a calendar, use `remove_day(...)`. For example,
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()
import exchange_calendars as ec

Expand Down Expand Up @@ -323,7 +324,7 @@ their string value. For example, `ecx.DayType.HOLIDAY` and `'holiday'` are equiv
Whenever a calendar has been modified programmatically, the changes are only reflected after obtaining a new exchange
calendar instance.
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()
import exchange_calendars as ec

Expand Down Expand Up @@ -365,12 +366,12 @@ exchange. When a new calendar instance is created, the changes are applied to th

It is also possible to create a changeset separately and then associate it with a particular exchange:
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()
import exchange_calendars as ec

changeset: ecx.ChangeSet = ecx.ChangeSet()
changeset.add_day({'date': '2022-12-28', 'type': 'holiday', 'name': 'Holiday'})
changeset.add_day('2022-12-28', {'type': 'holiday', 'name': 'Holiday'})
changeset.remove_day('2022-12-27')

ecx.update_calendar('XLON', changeset)
Expand All @@ -383,12 +384,12 @@ assert '2022-12-28' in calendar.holidays_all.holidays()
Again, an entire changeset can also be created from a suitably formatted dictionary, making it particularly easy to read
in and apply changes from an external source like a file.
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()
import exchange_calendars as ec

changeset: ecx.ChangeSet = ecx.ChangeSet(**{
'add': [{'date': '2022-12-28', 'type': 'holiday', 'name': 'Holiday'}],
'add': {'2022-12-28': {'type': 'holiday', 'name': 'Holiday'}},
'remove': ['2022-12-27']})

ecx.update_calendar('XLON', changeset)
Expand All @@ -404,7 +405,7 @@ assert '2022-12-28' in calendar.holidays_all.holidays()
The API permits to add and remove the same day as a special day. For example, the following code will add a holiday on
28 December 2022 to the calendar, and then remove the same day as well.
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()
import exchange_calendars as ec

Expand All @@ -420,7 +421,7 @@ more sense in a case where a day is added to change its type of special day. Con
holiday for the calendar `XLON` in the original version of the calendar. The following code will change the type of
special day to a special open by first removing the day (as a holiday), and then adding it back as a special open day:
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()
import exchange_calendars as ec

Expand All @@ -439,7 +440,7 @@ allows to change the type of special day in an existing calendar from one to ano
In fact, internally, each added days is always implicitly also removed from the calendar first, so that it strictly is
not necessary (but allowed) to explicitly remove a day, and then adding it back as a different type of special day:
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()
import exchange_calendars as ec

Expand All @@ -459,16 +460,21 @@ As seen above, changesets may contain the same day both in the list of days to a
However, changesets enforce consistency and will raise an exception if the same day is added more than once.
For example, the following code will raise an exception:
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()

ecx.add_holiday('XLON', date='2022-12-28', name='Holiday')
ecx.add_special_open('XLON', date='2022-12-28', name='Special Open', time='11:00')
try:
ecx.add_holiday('XLON', date='2022-12-28', name='Holiday')
ecx.add_special_open('XLON', date='2022-12-28', name='Special Open', time='11:00')
except ValueError as e:
print("Exception raised since the same day is added more than once in the changeset.")
else:
raise ValueError("Exception not raised.")
```
In contrast, removing a day is an idempotent operation, i.e. doing it twice will not raise an exception and keep the
corresponding changeset the same as after the first removal.
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()

ecx.remove_day('XLON', date='2022-12-27')
Expand All @@ -481,7 +487,7 @@ It is sometimes necessary to revert individual changes made to a calendar. To th
`reset_day(...)`:

```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()
import exchange_calendars as ec

Expand All @@ -506,7 +512,7 @@ assert '2022-12-28' not in calendar.holidays_all.holidays()
To reset an entire calendar to its original state, use the method `reset_calendar(...)` or update the calendar with an
empty ChangeSet:
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()
import exchange_calendars as ec

Expand All @@ -531,7 +537,7 @@ assert '2022-12-28' not in calendar.holidays_all.holidays()
### Retrieving changes
For any calendar, it is possible to retrieve a copy of the associated changeset:
```python
import exchange_calendars_extensions as ecx
import exchange_calendars_extensions.core as ecx
ecx.apply_extensions()

ecx.add_holiday('XLON', date='2022-12-28', name='Holiday')
Expand Down Expand Up @@ -590,7 +596,7 @@ sub-module `exchange_calendars_extensions.holiday_calendar`.

```python
from exchange_calendars.exchange_calendar_xlon import XLONExchangeCalendar
from exchange_calendars_extensions import extend_class
from exchange_calendars_extensions.core import extend_class

xlon_extended_cls = extend_class(XLONExchangeCalendar, day_of_week_expiry=4)
```
Expand All @@ -606,9 +612,10 @@ key of the parent class.
To register a new extended class for an exchange, use the `register_extension()` function before calling
`apply_extensions()`.
```python
from exchange_calendars_extensions import register_extension, apply_extensions
from exchange_calendars_extensions.core import register_extension, apply_extensions


register_extension(key, cls)
register_extension("XLON", day_of_week_expiry=4)
apply_extensions()
...
```
Expand Down
104 changes: 104 additions & 0 deletions support/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import sys
import subprocess
import tempfile
import os
import argparse


def extract_python_snippets(readme_path: str) -> list[tuple[int, int, str]]:
"""
Extracts Python code snippets from README.md file.
Returns list of tuples: (start_line, end_line, code).
"""
snippets = []
with open(readme_path, encoding="utf-8") as f:
lines = f.readlines()

in_snippet = False
start_line = 0
current_snippet = []

for i, line in enumerate(lines, 1):
if line.strip() == "```python":
in_snippet = True
start_line = i
current_snippet = []
elif line.strip() == "```" and in_snippet:
in_snippet = False
snippets.append((start_line + 1, i - 1, "".join(current_snippet)))
elif in_snippet:
current_snippet.append(line)

return snippets


def run_snippet(snippet: str) -> tuple[bool, str]:
"""
Runs a Python code snippet in a new interpreter process.
Returns tuple: (success, error_message).
"""
# Create a temporary file to hold the snippet
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as tf:
tf.write(snippet)
temp_path = tf.name

try:
# Run the snippet in a new Python interpreter process
result = subprocess.run(
[sys.executable, temp_path],
capture_output=True,
text=True,
env=os.environ.copy(), # Use current environment (including virtual env)
)

# Check if there was an error
if result.returncode != 0:
return False, result.stderr
return True, ""

finally:
# Clean up temporary file
os.unlink(temp_path)


def main(readme_path: str, exit_on_error: bool = False):
"""Main function to process README.md and run snippets."""
if not os.path.exists(readme_path):
print(f"Error: File {readme_path} not found!")
return

snippets = extract_python_snippets(readme_path)

for i, (start_line, end_line, code) in enumerate(snippets, 1):
print(f"\nProcessing snippet {i} (lines {start_line}-{end_line}):")

success, error = run_snippet(code)

if success:
print("✓ Snippet ran successfully")
else:
print("✗ Error occurred while running snippet:")
print("\nCode:")
print("-" * 40)
print(code.strip())
print("-" * 40)
print("\nError:")
print(error)
if exit_on_error:
sys.exit(1)


if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Run Python code snippets from README.md"
)
parser.add_argument("readme_path", help="Path to README.md file")
parser.add_argument(
"-e",
"--exit-on-error",
action="store_true",
help="Exit on first error encountered",
)

args = parser.parse_args()
main(args.readme_path, args.exit_on_error)
Loading