xpectral-utils 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xpectral_utils-0.1.0/.github/workflows/publish.yml +42 -0
- xpectral_utils-0.1.0/.gitignore +125 -0
- xpectral_utils-0.1.0/.pre-commit-config.yaml +25 -0
- xpectral_utils-0.1.0/CONTRIBUTING.md +10 -0
- xpectral_utils-0.1.0/LICENSE +21 -0
- xpectral_utils-0.1.0/PKG-INFO +55 -0
- xpectral_utils-0.1.0/README.md +15 -0
- xpectral_utils-0.1.0/assets/xpectral_banner.gif +0 -0
- xpectral_utils-0.1.0/examples/caching.ipynb +247 -0
- xpectral_utils-0.1.0/examples/rate_limiter.ipynb +87 -0
- xpectral_utils-0.1.0/pyproject.toml +57 -0
- xpectral_utils-0.1.0/tests/.gitkeep +0 -0
- xpectral_utils-0.1.0/tests/test_cache.py +136 -0
- xpectral_utils-0.1.0/tests/test_dates.py +66 -0
- xpectral_utils-0.1.0/tests/test_logger.py +87 -0
- xpectral_utils-0.1.0/tests/test_rate_limiter.py +92 -0
- xpectral_utils-0.1.0/tests/test_s3.py +153 -0
- xpectral_utils-0.1.0/uv.lock +847 -0
- xpectral_utils-0.1.0/xpectral_utils/__init__.py +20 -0
- xpectral_utils-0.1.0/xpectral_utils/cache.py +107 -0
- xpectral_utils-0.1.0/xpectral_utils/dates.py +61 -0
- xpectral_utils-0.1.0/xpectral_utils/logger.py +84 -0
- xpectral_utils-0.1.0/xpectral_utils/py.typed +0 -0
- xpectral_utils-0.1.0/xpectral_utils/rate_limiter.py +63 -0
- xpectral_utils-0.1.0/xpectral_utils/s3.py +136 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
|
+
|
|
18
|
+
- uses: astral-sh/setup-uv@v5
|
|
19
|
+
with:
|
|
20
|
+
version: "latest"
|
|
21
|
+
|
|
22
|
+
- name: Build package
|
|
23
|
+
run: uv build
|
|
24
|
+
|
|
25
|
+
- uses: actions/upload-artifact@v4
|
|
26
|
+
with:
|
|
27
|
+
name: dist
|
|
28
|
+
path: dist/
|
|
29
|
+
|
|
30
|
+
publish:
|
|
31
|
+
needs: build
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
environment:
|
|
34
|
+
name: pypi
|
|
35
|
+
url: https://pypi.org/project/xpectral-utils/
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/download-artifact@v4
|
|
38
|
+
with:
|
|
39
|
+
name: dist
|
|
40
|
+
path: dist/
|
|
41
|
+
|
|
42
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
MANIFEST
|
|
27
|
+
|
|
28
|
+
# PyInstaller
|
|
29
|
+
*.manifest
|
|
30
|
+
*.spec
|
|
31
|
+
|
|
32
|
+
# Installer logs
|
|
33
|
+
pip-log.txt
|
|
34
|
+
pip-delete-this-directory.txt
|
|
35
|
+
|
|
36
|
+
# Unit test / coverage reports
|
|
37
|
+
htmlcov/
|
|
38
|
+
.tox/
|
|
39
|
+
.nox/
|
|
40
|
+
.coverage
|
|
41
|
+
.coverage.*
|
|
42
|
+
.cache
|
|
43
|
+
nosetests.xml
|
|
44
|
+
coverage.xml
|
|
45
|
+
*.cover
|
|
46
|
+
*.py,cover
|
|
47
|
+
.hypothesis/
|
|
48
|
+
.pytest_cache/
|
|
49
|
+
|
|
50
|
+
# Translations
|
|
51
|
+
*.mo
|
|
52
|
+
*.pot
|
|
53
|
+
|
|
54
|
+
# Django stuff:
|
|
55
|
+
*.log
|
|
56
|
+
local_settings.py
|
|
57
|
+
db.sqlite3
|
|
58
|
+
db.sqlite3-journal
|
|
59
|
+
|
|
60
|
+
# Flask stuff:
|
|
61
|
+
instance/
|
|
62
|
+
.webassets-cache
|
|
63
|
+
|
|
64
|
+
# Scrapy stuff:
|
|
65
|
+
.scrapy
|
|
66
|
+
|
|
67
|
+
# Sphinx documentation
|
|
68
|
+
docs/build/
|
|
69
|
+
docs/_build/
|
|
70
|
+
|
|
71
|
+
# PyBuilder
|
|
72
|
+
target/
|
|
73
|
+
|
|
74
|
+
# Jupyter Notebook
|
|
75
|
+
.ipynb_checkpoints
|
|
76
|
+
|
|
77
|
+
# IPython
|
|
78
|
+
profile_default/
|
|
79
|
+
ipython_config.py
|
|
80
|
+
|
|
81
|
+
# pyenv
|
|
82
|
+
.python-version
|
|
83
|
+
|
|
84
|
+
# pipenv
|
|
85
|
+
Pipfile.lock
|
|
86
|
+
|
|
87
|
+
# poetry
|
|
88
|
+
poetry.lock
|
|
89
|
+
|
|
90
|
+
# PEP 582
|
|
91
|
+
__pypackages__/
|
|
92
|
+
|
|
93
|
+
# Ruff cache
|
|
94
|
+
.ruff_cache/
|
|
95
|
+
|
|
96
|
+
# Claude Code
|
|
97
|
+
.claude/worktrees/
|
|
98
|
+
|
|
99
|
+
# VS Code
|
|
100
|
+
.vscode/
|
|
101
|
+
|
|
102
|
+
# PyCharm
|
|
103
|
+
.idea/
|
|
104
|
+
|
|
105
|
+
# Local virtual environments
|
|
106
|
+
venv/
|
|
107
|
+
.env
|
|
108
|
+
.env/
|
|
109
|
+
env/
|
|
110
|
+
.env.*
|
|
111
|
+
!.env.example
|
|
112
|
+
.venv/
|
|
113
|
+
|
|
114
|
+
# macOS
|
|
115
|
+
.DS_Store
|
|
116
|
+
|
|
117
|
+
# Windows
|
|
118
|
+
Thumbs.db
|
|
119
|
+
|
|
120
|
+
# Project-specific data files
|
|
121
|
+
*.parquet
|
|
122
|
+
*.csv
|
|
123
|
+
*.dbn
|
|
124
|
+
*.dbn.zst
|
|
125
|
+
logs/
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: local # Hooks defined in this project, not pulled from a remote repo
|
|
3
|
+
hooks:
|
|
4
|
+
# ---- Linter: finds issues and auto-fixes them (includes import sorting) ----
|
|
5
|
+
- id: ruff-check # Unique identifier for this hook
|
|
6
|
+
name: Lint with ruff # Human-readable label shown in pre-commit's output
|
|
7
|
+
entry: uv run ruff check --fix --exit-non-zero-on-fix
|
|
8
|
+
# Command pre-commit runs:
|
|
9
|
+
# uv run → run inside the project's uv environment
|
|
10
|
+
# ruff check → the linter
|
|
11
|
+
# --fix → automatically fix what it can
|
|
12
|
+
# --exit-non-zero-on-fix → fail the hook when a fix is applied,
|
|
13
|
+
# so the run also fails in CI, not just locally
|
|
14
|
+
language: system # Use the command as-is from the system/uv env (don't let pre-commit build its own)
|
|
15
|
+
types_or: [python, pyi, jupyter] # Run on .py files, .pyi stubs, AND .ipynb notebooks
|
|
16
|
+
require_serial: true # Don't run this hook in parallel with others (avoids fix/format conflicts)
|
|
17
|
+
stages: [pre-commit] # Run at commit time (i.e. when you `git commit`)
|
|
18
|
+
|
|
19
|
+
# ---- Formatter: reformats code style (runs after the linter) ----
|
|
20
|
+
- id: ruff-format # Unique identifier for this hook
|
|
21
|
+
name: Format with ruff # Human-readable label shown in pre-commit's output
|
|
22
|
+
entry: uv run ruff format # Command: run `ruff format` inside the uv environment
|
|
23
|
+
language: system # Use the command as-is from the system/uv env
|
|
24
|
+
types_or: [python, pyi, jupyter] # Format .py files, .pyi stubs, AND .ipynb notebooks
|
|
25
|
+
stages: [pre-commit] # Run at commit time
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 BayQuant
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: xpectral-utils
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared utility functions for xpectral projects
|
|
5
|
+
Project-URL: Homepage, https://github.com/bayquant/xpectral-utils
|
|
6
|
+
Author: BayQuant
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2025 BayQuant
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Keywords: quant,utils
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Operating System :: OS Independent
|
|
32
|
+
Classifier: Programming Language :: Python :: 3
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
34
|
+
Requires-Python: >=3.12
|
|
35
|
+
Provides-Extra: cache
|
|
36
|
+
Requires-Dist: diskcache>=5.6.3; extra == 'cache'
|
|
37
|
+
Provides-Extra: s3
|
|
38
|
+
Requires-Dist: boto3>=1.34; extra == 's3'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# xpectral-utils
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
Shared utility functions used across xpectral projects.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv pip install xpectral-utils
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Development
|
|
54
|
+
|
|
55
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# xpectral-utils
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Shared utility functions used across xpectral projects.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv pip install xpectral-utils
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Development
|
|
14
|
+
|
|
15
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
Binary file
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"id": "3ae94973",
|
|
6
|
+
"metadata": {},
|
|
7
|
+
"source": "# ON-DISK FUNCTION CACHING WITH `DISK_CACHE`\n\n`xpectral_utils.cache.disk_cache` memoizes a function's return values **on disk**,\nkeyed by its arguments. Unlike `functools.lru_cache`, the results persist across\nprocesses and interpreter restarts, which is ideal for slow, repeatable work such\nas API data pulls.\n\nEach decorated function gets its own cache directory (no shared global instance),\nbacked by [`diskcache`](https://grantjenks.com/docs/diskcache/), which handles the\narg-hashing, atomic writes, TTL, size limits, and LRU eviction for us. Values are\npickled, so the function must take picklable/hashable arguments and return\npicklable objects."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"cell_type": "code",
|
|
11
|
+
"execution_count": null,
|
|
12
|
+
"id": "e3859601",
|
|
13
|
+
"metadata": {
|
|
14
|
+
"execution": {
|
|
15
|
+
"iopub.execute_input": "2026-07-05T07:59:03.978095Z",
|
|
16
|
+
"iopub.status.busy": "2026-07-05T07:59:03.977902Z",
|
|
17
|
+
"iopub.status.idle": "2026-07-05T07:59:03.993653Z",
|
|
18
|
+
"shell.execute_reply": "2026-07-05T07:59:03.992954Z"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"outputs": [],
|
|
22
|
+
"source": "%load_ext autoreload\n%autoreload 2\n\n# Standard library imports\nimport tempfile\nimport time\n\n# First-party imports\nfrom xpectral_utils.cache import disk_cache\n\n# Keep this example self-contained: cache into a throwaway temp directory\n# instead of the default ~/.cache/xpectral.\nCACHE_DIR = tempfile.mkdtemp(prefix=\"xpectral-cache-demo-\")\nCACHE_DIR"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"cell_type": "markdown",
|
|
26
|
+
"id": "00e28c86",
|
|
27
|
+
"metadata": {},
|
|
28
|
+
"source": [
|
|
29
|
+
"## A slow function\n",
|
|
30
|
+
"\n",
|
|
31
|
+
"We simulate expensive work with `time.sleep` and count how many times the body\n",
|
|
32
|
+
"actually runs, so we can prove the cache is doing its job."
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"cell_type": "code",
|
|
37
|
+
"execution_count": 2,
|
|
38
|
+
"id": "57933e19",
|
|
39
|
+
"metadata": {
|
|
40
|
+
"execution": {
|
|
41
|
+
"iopub.execute_input": "2026-07-05T07:59:03.995397Z",
|
|
42
|
+
"iopub.status.busy": "2026-07-05T07:59:03.995257Z",
|
|
43
|
+
"iopub.status.idle": "2026-07-05T07:59:04.003631Z",
|
|
44
|
+
"shell.execute_reply": "2026-07-05T07:59:04.003144Z"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"outputs": [],
|
|
48
|
+
"source": [
|
|
49
|
+
"calls = {\"n\": 0}\n",
|
|
50
|
+
"\n",
|
|
51
|
+
"\n",
|
|
52
|
+
"@disk_cache(\"demo\", cache_dir=CACHE_DIR)\n",
|
|
53
|
+
"def slow_add(a, b):\n",
|
|
54
|
+
" calls[\"n\"] += 1\n",
|
|
55
|
+
" time.sleep(0.5) # pretend this is a network / heavy compute call\n",
|
|
56
|
+
" return a + b"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"cell_type": "markdown",
|
|
61
|
+
"id": "93cf0bd5",
|
|
62
|
+
"metadata": {},
|
|
63
|
+
"source": [
|
|
64
|
+
"## Miss vs. hit\n",
|
|
65
|
+
"\n",
|
|
66
|
+
"The first call runs the body (a *miss*); the second call with identical arguments\n",
|
|
67
|
+
"is served from disk (a *hit*) and skips the work entirely."
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"cell_type": "code",
|
|
72
|
+
"execution_count": 3,
|
|
73
|
+
"id": "278ccd10",
|
|
74
|
+
"metadata": {
|
|
75
|
+
"execution": {
|
|
76
|
+
"iopub.execute_input": "2026-07-05T07:59:04.005358Z",
|
|
77
|
+
"iopub.status.busy": "2026-07-05T07:59:04.005250Z",
|
|
78
|
+
"iopub.status.idle": "2026-07-05T07:59:04.513898Z",
|
|
79
|
+
"shell.execute_reply": "2026-07-05T07:59:04.513257Z"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"outputs": [
|
|
83
|
+
{
|
|
84
|
+
"name": "stdout",
|
|
85
|
+
"output_type": "stream",
|
|
86
|
+
"text": [
|
|
87
|
+
"result: 5 -> 5\n",
|
|
88
|
+
"miss took 0.505s\n",
|
|
89
|
+
"hit took 0.0002s\n",
|
|
90
|
+
"function body ran 1 time(s)\n"
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"source": [
|
|
95
|
+
"t0 = time.perf_counter()\n",
|
|
96
|
+
"r1 = slow_add(2, 3) # miss\n",
|
|
97
|
+
"t1 = time.perf_counter()\n",
|
|
98
|
+
"r2 = slow_add(2, 3) # hit\n",
|
|
99
|
+
"t2 = time.perf_counter()\n",
|
|
100
|
+
"\n",
|
|
101
|
+
"print(f\"result: {r1} -> {r2}\")\n",
|
|
102
|
+
"print(f\"miss took {t1 - t0:.3f}s\")\n",
|
|
103
|
+
"print(f\"hit took {t2 - t1:.4f}s\")\n",
|
|
104
|
+
"print(f\"function body ran {calls['n']} time(s)\")\n",
|
|
105
|
+
"\n",
|
|
106
|
+
"assert r1 == r2\n",
|
|
107
|
+
"assert calls[\"n\"] == 1 # the body ran only for the miss"
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"cell_type": "markdown",
|
|
112
|
+
"id": "4d443128",
|
|
113
|
+
"metadata": {},
|
|
114
|
+
"source": [
|
|
115
|
+
"## Expiring stale entries (TTL)\n",
|
|
116
|
+
"\n",
|
|
117
|
+
"Pass `expire=<seconds>` to invalidate entries after a time-to-live. This matters\n",
|
|
118
|
+
"for data that goes stale (e.g. recent market bars). Here entries live for 1 second."
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"cell_type": "code",
|
|
123
|
+
"execution_count": 4,
|
|
124
|
+
"id": "9e86ad92",
|
|
125
|
+
"metadata": {
|
|
126
|
+
"execution": {
|
|
127
|
+
"iopub.execute_input": "2026-07-05T07:59:04.516530Z",
|
|
128
|
+
"iopub.status.busy": "2026-07-05T07:59:04.516301Z",
|
|
129
|
+
"iopub.status.idle": "2026-07-05T07:59:05.635004Z",
|
|
130
|
+
"shell.execute_reply": "2026-07-05T07:59:05.634097Z"
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"outputs": [
|
|
134
|
+
{
|
|
135
|
+
"name": "stdout",
|
|
136
|
+
"output_type": "stream",
|
|
137
|
+
"text": [
|
|
138
|
+
"after two quick calls: body ran 1 time(s)\n"
|
|
139
|
+
]
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"name": "stdout",
|
|
143
|
+
"output_type": "stream",
|
|
144
|
+
"text": [
|
|
145
|
+
"after TTL expiry: body ran 2 time(s)\n"
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
],
|
|
149
|
+
"source": [
|
|
150
|
+
"ttl_calls = {\"n\": 0}\n",
|
|
151
|
+
"\n",
|
|
152
|
+
"\n",
|
|
153
|
+
"@disk_cache(\"demo_ttl\", expire=1, cache_dir=CACHE_DIR)\n",
|
|
154
|
+
"def slow_double(x):\n",
|
|
155
|
+
" ttl_calls[\"n\"] += 1\n",
|
|
156
|
+
" return x * 2\n",
|
|
157
|
+
"\n",
|
|
158
|
+
"\n",
|
|
159
|
+
"slow_double(21) # miss -> body runs\n",
|
|
160
|
+
"slow_double(21) # hit -> cached\n",
|
|
161
|
+
"print(f\"after two quick calls: body ran {ttl_calls['n']} time(s)\")\n",
|
|
162
|
+
"\n",
|
|
163
|
+
"time.sleep(1.1) # let the entry expire\n",
|
|
164
|
+
"slow_double(21) # miss again -> body re-runs\n",
|
|
165
|
+
"print(f\"after TTL expiry: body ran {ttl_calls['n']} time(s)\")\n",
|
|
166
|
+
"\n",
|
|
167
|
+
"assert ttl_calls[\"n\"] == 2"
|
|
168
|
+
]
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"cell_type": "markdown",
|
|
172
|
+
"id": "21afaea3",
|
|
173
|
+
"metadata": {},
|
|
174
|
+
"source": [
|
|
175
|
+
"## Inspecting and clearing\n",
|
|
176
|
+
"\n",
|
|
177
|
+
"The wrapped function exposes its backing store as `func.cache` (a\n",
|
|
178
|
+
"`diskcache.Cache`), so you can check how many entries it holds or clear them."
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"cell_type": "code",
|
|
183
|
+
"execution_count": 5,
|
|
184
|
+
"id": "573e6c6e",
|
|
185
|
+
"metadata": {
|
|
186
|
+
"execution": {
|
|
187
|
+
"iopub.execute_input": "2026-07-05T07:59:05.637120Z",
|
|
188
|
+
"iopub.status.busy": "2026-07-05T07:59:05.636935Z",
|
|
189
|
+
"iopub.status.idle": "2026-07-05T07:59:06.148140Z",
|
|
190
|
+
"shell.execute_reply": "2026-07-05T07:59:06.147301Z"
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
"outputs": [
|
|
194
|
+
{
|
|
195
|
+
"name": "stdout",
|
|
196
|
+
"output_type": "stream",
|
|
197
|
+
"text": [
|
|
198
|
+
"entries before clear: 1\n",
|
|
199
|
+
"entries after clear: 0\n"
|
|
200
|
+
]
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"name": "stdout",
|
|
204
|
+
"output_type": "stream",
|
|
205
|
+
"text": [
|
|
206
|
+
"body ran again after clear: True\n"
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
],
|
|
210
|
+
"source": [
|
|
211
|
+
"print(f\"entries before clear: {len(slow_add.cache)}\")\n",
|
|
212
|
+
"\n",
|
|
213
|
+
"slow_add.cache.clear()\n",
|
|
214
|
+
"print(f\"entries after clear: {len(slow_add.cache)}\")\n",
|
|
215
|
+
"\n",
|
|
216
|
+
"# With the cache emptied, the next call is a miss again and the body re-runs.\n",
|
|
217
|
+
"before = calls[\"n\"]\n",
|
|
218
|
+
"slow_add(2, 3)\n",
|
|
219
|
+
"print(f\"body ran again after clear: {calls['n'] == before + 1}\")\n",
|
|
220
|
+
"\n",
|
|
221
|
+
"assert len(slow_add.cache) == 1\n",
|
|
222
|
+
"assert calls[\"n\"] == before + 1"
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
"metadata": {
|
|
227
|
+
"kernelspec": {
|
|
228
|
+
"display_name": "xpectral-utils",
|
|
229
|
+
"language": "python",
|
|
230
|
+
"name": "python3"
|
|
231
|
+
},
|
|
232
|
+
"language_info": {
|
|
233
|
+
"codemirror_mode": {
|
|
234
|
+
"name": "ipython",
|
|
235
|
+
"version": 3
|
|
236
|
+
},
|
|
237
|
+
"file_extension": ".py",
|
|
238
|
+
"mimetype": "text/x-python",
|
|
239
|
+
"name": "python",
|
|
240
|
+
"nbconvert_exporter": "python",
|
|
241
|
+
"pygments_lexer": "ipython3",
|
|
242
|
+
"version": "3.12.11"
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
"nbformat": 4,
|
|
246
|
+
"nbformat_minor": 5
|
|
247
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"id": "a1b2c3d4",
|
|
6
|
+
"metadata": {},
|
|
7
|
+
"source": "# RATE LIMITER\n\nThrottle calls to an API with `RateLimiter`: at most `calls` acquisitions per `per_seconds` window."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"cell_type": "code",
|
|
11
|
+
"execution_count": 1,
|
|
12
|
+
"id": "b2c3d4e5",
|
|
13
|
+
"metadata": {},
|
|
14
|
+
"outputs": [],
|
|
15
|
+
"source": "%load_ext autoreload\n%autoreload 2"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"cell_type": "code",
|
|
19
|
+
"execution_count": 2,
|
|
20
|
+
"id": "c3d4e5f6",
|
|
21
|
+
"metadata": {},
|
|
22
|
+
"outputs": [],
|
|
23
|
+
"source": "# Standard library imports\nimport time\n\n# First-party imports\nfrom xpectral_utils.rate_limiter import RateLimiter\n\nCALLS = 3\nPER_SECONDS = 1"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"cell_type": "markdown",
|
|
27
|
+
"id": "d4e5f6a7",
|
|
28
|
+
"metadata": {},
|
|
29
|
+
"source": "## Throttle a loop\n\nCall `acquire()` before each request. The first `CALLS` go through immediately; the next one blocks until the oldest call leaves the window."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"cell_type": "code",
|
|
33
|
+
"execution_count": 3,
|
|
34
|
+
"id": "e5f6a7b8",
|
|
35
|
+
"metadata": {},
|
|
36
|
+
"outputs": [
|
|
37
|
+
{
|
|
38
|
+
"name": "stdout",
|
|
39
|
+
"output_type": "stream",
|
|
40
|
+
"text": "call 0 at 0.0s\ncall 1 at 0.0s\ncall 2 at 0.0s\ncall 3 at 1.0s\ncall 4 at 1.0s\ncall 5 at 1.0s\ncall 6 at 2.0s\ncall 7 at 2.0s\ncall 8 at 2.0s\n"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"source": "limiter = RateLimiter(calls=CALLS, per_seconds=PER_SECONDS)\nstart = time.monotonic()\n\nfor i in range(9):\n limiter.acquire()\n print(f\"call {i} at {time.monotonic() - start:.1f}s\")"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"cell_type": "markdown",
|
|
47
|
+
"id": "f6a7b8c9",
|
|
48
|
+
"metadata": {},
|
|
49
|
+
"source": "## Invalid arguments\n\n`calls` and `per_seconds` must be positive."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"cell_type": "code",
|
|
53
|
+
"execution_count": 4,
|
|
54
|
+
"id": "a7b8c9d0",
|
|
55
|
+
"metadata": {},
|
|
56
|
+
"outputs": [
|
|
57
|
+
{
|
|
58
|
+
"name": "stdout",
|
|
59
|
+
"output_type": "stream",
|
|
60
|
+
"text": "calls and per_seconds must be positive\n"
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"source": "try:\n RateLimiter(calls=0, per_seconds=1)\nexcept ValueError as error:\n print(error)"
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"metadata": {
|
|
67
|
+
"kernelspec": {
|
|
68
|
+
"display_name": "xpectral-utils",
|
|
69
|
+
"language": "python",
|
|
70
|
+
"name": "python3"
|
|
71
|
+
},
|
|
72
|
+
"language_info": {
|
|
73
|
+
"codemirror_mode": {
|
|
74
|
+
"name": "ipython",
|
|
75
|
+
"version": 3
|
|
76
|
+
},
|
|
77
|
+
"file_extension": ".py",
|
|
78
|
+
"mimetype": "text/x-python",
|
|
79
|
+
"name": "python",
|
|
80
|
+
"nbconvert_exporter": "python",
|
|
81
|
+
"pygments_lexer": "ipython3",
|
|
82
|
+
"version": "3.13.7"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"nbformat": 4,
|
|
86
|
+
"nbformat_minor": 5
|
|
87
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "xpectral-utils"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Shared utility functions for xpectral projects"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = { file = "LICENSE" }
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "BayQuant" }
|
|
10
|
+
]
|
|
11
|
+
keywords = ["utils", "quant"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
dependencies = []
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
cache = ["diskcache>=5.6.3"] # xpectral_utils.cache
|
|
23
|
+
s3 = ["boto3>=1.34"] # xpectral_utils.s3
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/bayquant/xpectral-utils"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["hatchling"]
|
|
30
|
+
build-backend = "hatchling.build"
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"ruff>=0.6", # code formatter and linter
|
|
35
|
+
"pre-commit>=4.4.0", # git hook manager
|
|
36
|
+
"pytest>=8.0.0", # test runner
|
|
37
|
+
"ipykernel>=7.0.1", # jupyter notebook support
|
|
38
|
+
"boto3>=1.34", # tests for xpectral_utils.s3 (the s3 extra at runtime)
|
|
39
|
+
"diskcache>=5.6.3", # tests for xpectral_utils.cache (the cache extra at runtime)
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["xpectral_utils"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
line-length = 88
|
|
47
|
+
target-version = "py312"
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint]
|
|
50
|
+
select = ["E", "F", "W", "I"]
|
|
51
|
+
ignore = [
|
|
52
|
+
"E501", # Line too long: handled by formatter, not linter
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint.isort]
|
|
56
|
+
known-first-party = ["xpectral_utils"]
|
|
57
|
+
force-single-line = true
|
|
File without changes
|