settfex 0.2.0__tar.gz → 0.3.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.
- settfex-0.3.0/.editorconfig +23 -0
- settfex-0.3.0/.github/ISSUE_TEMPLATE/bug_report.md +43 -0
- settfex-0.3.0/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
- settfex-0.3.0/.github/PULL_REQUEST_TEMPLATE.md +26 -0
- settfex-0.3.0/.github/dependabot.yml +27 -0
- {settfex-0.2.0 → settfex-0.3.0}/.github/workflows/ci.yml +18 -11
- {settfex-0.2.0 → settfex-0.3.0}/.github/workflows/release.yml +5 -11
- settfex-0.3.0/.github/workflows/security.yml +62 -0
- settfex-0.3.0/.pre-commit-config.yaml +36 -0
- {settfex-0.2.0 → settfex-0.3.0}/CHANGELOG.md +41 -0
- settfex-0.3.0/CODE_OF_CONDUCT.md +83 -0
- settfex-0.3.0/COMPREHENSIVE_AUDIT.md +139 -0
- settfex-0.3.0/CONTRIBUTING.md +73 -0
- {settfex-0.2.0 → settfex-0.3.0}/PKG-INFO +1 -1
- settfex-0.3.0/RELEASING.md +73 -0
- settfex-0.3.0/SECURITY.md +49 -0
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/01_stock_list.ipynb +76 -72
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/02_highlight_data.ipynb +128 -88
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/03_stock_profile.ipynb +35 -28
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/04_company_profile.ipynb +43 -32
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/05_corporate_action.ipynb +53 -39
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/06_shareholder.ipynb +22 -17
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/07_nvdr_holder.ipynb +18 -11
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/08_board_of_director.ipynb +18 -16
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/09_trading_statistics.ipynb +40 -36
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/10_price_performance.ipynb +29 -24
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/11_financial.ipynb +64 -46
- {settfex-0.2.0 → settfex-0.3.0}/examples/tfex/01_series_list.ipynb +61 -59
- {settfex-0.2.0 → settfex-0.3.0}/examples/tfex/02_trading_statistics.ipynb +104 -103
- settfex-0.3.0/examples/tfex/03_underlying_price.ipynb +564 -0
- {settfex-0.2.0 → settfex-0.3.0}/pyproject.toml +22 -3
- {settfex-0.2.0 → settfex-0.3.0}/settfex/__init__.py +1 -1
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/list.py +10 -14
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/board_of_director.py +15 -38
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/chart_quotation.py +11 -20
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/corporate_action.py +11 -28
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/financial/financial.py +18 -37
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/highlight_data.py +15 -38
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/latest_historical_trading.py +22 -22
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/nvdr_holder.py +11 -28
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/price_performance.py +11 -26
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/profile_company.py +11 -27
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/profile_stock.py +13 -40
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/shareholder.py +12 -31
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/stock.py +9 -11
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/trading_stat.py +15 -38
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/tfex/__init__.py +11 -0
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/tfex/constants.py +1 -0
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/tfex/list.py +13 -19
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/tfex/trading_statistics.py +14 -13
- settfex-0.3.0/settfex/services/tfex/underlying_price.py +253 -0
- {settfex-0.2.0 → settfex-0.3.0}/settfex/utils/data_fetcher.py +31 -32
- settfex-0.3.0/settfex/utils/parsing.py +123 -0
- {settfex-0.2.0 → settfex-0.3.0}/settfex/utils/session_cache.py +13 -11
- {settfex-0.2.0 → settfex-0.3.0}/settfex/utils/session_manager.py +20 -19
- {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/stock/financial/test_financial.py +99 -47
- {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/test_board_of_director.py +3 -4
- {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/test_corporate_action.py +3 -3
- {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/test_shareholder.py +20 -1
- settfex-0.3.0/tests/services/tfex/test_list.py +73 -0
- settfex-0.3.0/tests/services/tfex/test_trading_statistics.py +79 -0
- settfex-0.3.0/tests/services/tfex/test_underlying_price.py +143 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/utils/test_data_fetcher.py +30 -8
- settfex-0.3.0/tests/utils/test_parsing.py +113 -0
- settfex-0.3.0/tests/utils/test_session_manager.py +78 -0
- {settfex-0.2.0 → settfex-0.3.0}/uv.lock +332 -1
- {settfex-0.2.0 → settfex-0.3.0}/.github/copilot-instructions.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/.github/instructions/core-architectrual-principles.instructions.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/.github/instructions/documentation-standards.instructions.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/.github/instructions/file-organization.instructions.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/.github/instructions/git-commit.instructions.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/.github/instructions/python-dependency-management.instructions.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/.github/prompts/Coding.prompt.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/.github/prompts/Git-Commit-Reviewer.prompt.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/.github/prompts/Prompt-Engineer.prompt.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/.github/prompts/Python-Architect.prompt.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/.gitignore +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/.python-version +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/LICENSE +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/MANIFEST.in +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/README.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/guide/PYTHON_LIBRARY_BEST_PRACTICES.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/index.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/API_PROTECTION_NOTE.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/board_of_director.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/corporate_action.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/financial.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/highlight_data.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/list.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/nvdr_holder.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/price_performance.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/profile_company.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/profile_stock.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/shareholder.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/trading_stat.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/tfex/list.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/tfex/trading_statistics.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/utils/data_fetcher.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/utils/session_caching.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/solution/FINDINGS.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/solution/Performance Boost Session Caching Implementation.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/solution/SESSION_CACHE_SUMMARY.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/docs/solution/SOLUTION_100_PERCENT.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/examples/README.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/README.md +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/cpall_balance_sheet.csv +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/market_dashboard_20251005_173221.csv +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/market_dashboard_20251005_210737.csv +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/market_dashboard_20251006_091440.csv +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/thailand_banking_stocks.csv +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/thailand_set_main_board.csv +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/examples/set/thailand_stock_universe.csv +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/settfex/py.typed +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/__init__.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/__init__.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/constants.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/__init__.py +9 -9
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/financial/__init__.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/utils.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/settfex/utils/__init__.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/settfex/utils/http.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/settfex/utils/logging.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/__init__.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/conftest.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/services/__init__.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/__init__.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/stock/financial/__init__.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/test_client.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/test_historical.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/test_realtime.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/services/tfex/__init__.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/services/tfex/test_client.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/services/tfex/test_historical.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/services/tfex/test_realtime.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/utils/__init__.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/utils/test_formatting.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/utils/test_http.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/utils/test_logging.py +0 -0
- {settfex-0.2.0 → settfex-0.3.0}/tests/utils/test_validation.py +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# EditorConfig — https://editorconfig.org
|
|
2
|
+
root = true
|
|
3
|
+
|
|
4
|
+
[*]
|
|
5
|
+
charset = utf-8
|
|
6
|
+
end_of_line = lf
|
|
7
|
+
insert_final_newline = true
|
|
8
|
+
trim_trailing_whitespace = true
|
|
9
|
+
indent_style = space
|
|
10
|
+
indent_size = 4
|
|
11
|
+
max_line_length = 100
|
|
12
|
+
|
|
13
|
+
[*.py]
|
|
14
|
+
indent_size = 4
|
|
15
|
+
|
|
16
|
+
[*.{yml,yaml,json,toml}]
|
|
17
|
+
indent_size = 2
|
|
18
|
+
|
|
19
|
+
[*.md]
|
|
20
|
+
trim_trailing_whitespace = false
|
|
21
|
+
|
|
22
|
+
[Makefile]
|
|
23
|
+
indent_style = tab
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug Report
|
|
3
|
+
about: Report a bug to help us improve settfex
|
|
4
|
+
labels: bug
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Description
|
|
8
|
+
|
|
9
|
+
A clear description of the bug.
|
|
10
|
+
|
|
11
|
+
## Steps to reproduce
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
# Minimal async snippet that triggers the bug
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
1.
|
|
18
|
+
2.
|
|
19
|
+
3.
|
|
20
|
+
|
|
21
|
+
## Expected behavior
|
|
22
|
+
|
|
23
|
+
What you expected to happen.
|
|
24
|
+
|
|
25
|
+
## Actual behavior
|
|
26
|
+
|
|
27
|
+
What actually happened — include the full error message / traceback or logs.
|
|
28
|
+
|
|
29
|
+
## Service / endpoint
|
|
30
|
+
|
|
31
|
+
Which settfex service is involved (e.g. `get_highlight_data`, `Stock`,
|
|
32
|
+
`get_stock_list`, TFEX series list) and the symbol used, if relevant.
|
|
33
|
+
|
|
34
|
+
## Environment
|
|
35
|
+
|
|
36
|
+
- OS: [e.g. macOS 15, Ubuntu 24.04]
|
|
37
|
+
- Python version: [e.g. 3.11.9]
|
|
38
|
+
- settfex version: [from `pip show settfex` or `uv pip show settfex`]
|
|
39
|
+
- uv version: [from `uv --version`, if applicable]
|
|
40
|
+
|
|
41
|
+
## Additional context
|
|
42
|
+
|
|
43
|
+
Add any other context (network/proxy setup, intermittent vs reproducible, etc.).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Feature Request
|
|
3
|
+
about: Suggest a new service, data field, or improvement for settfex
|
|
4
|
+
labels: enhancement
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
Describe the problem this feature would solve (e.g. "settfex has no service for
|
|
10
|
+
X data that the SET/TFEX website exposes").
|
|
11
|
+
|
|
12
|
+
## Proposed solution
|
|
13
|
+
|
|
14
|
+
Describe what you'd like to happen. If it's a new data service, include the
|
|
15
|
+
endpoint URL / API shape if you know it.
|
|
16
|
+
|
|
17
|
+
## Alternatives considered
|
|
18
|
+
|
|
19
|
+
Describe any alternative solutions or workarounds you've tried.
|
|
20
|
+
|
|
21
|
+
## Additional context
|
|
22
|
+
|
|
23
|
+
Add any other context, links to SET/TFEX pages, or sample responses.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
## Summary
|
|
2
|
+
|
|
3
|
+
<!-- Briefly describe what this PR does. -->
|
|
4
|
+
|
|
5
|
+
## Changes
|
|
6
|
+
|
|
7
|
+
<!-- List the key changes with file paths. -->
|
|
8
|
+
|
|
9
|
+
- ...
|
|
10
|
+
|
|
11
|
+
## Test plan
|
|
12
|
+
|
|
13
|
+
<!-- Check the boxes after verifying each step locally. -->
|
|
14
|
+
|
|
15
|
+
- [ ] `uv run ruff check .` passes
|
|
16
|
+
- [ ] `uv run ruff format --check .` passes
|
|
17
|
+
- [ ] `uv run mypy settfex/` passes
|
|
18
|
+
- [ ] `uv run pytest` passes (coverage gate met)
|
|
19
|
+
- [ ] New/changed behavior is covered by tests (external API calls mocked)
|
|
20
|
+
- [ ] Docs / docstrings / `CHANGELOG.md` updated if user-facing
|
|
21
|
+
|
|
22
|
+
## Related
|
|
23
|
+
|
|
24
|
+
<!-- Link to issues or discussions. -->
|
|
25
|
+
|
|
26
|
+
Closes #
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
# Python dependencies (uv.lock / pyproject.toml).
|
|
4
|
+
# If your Dependabot version rejects the "uv" ecosystem, change it to "pip".
|
|
5
|
+
- package-ecosystem: "uv"
|
|
6
|
+
directory: "/"
|
|
7
|
+
schedule:
|
|
8
|
+
interval: "weekly"
|
|
9
|
+
day: "monday"
|
|
10
|
+
open-pull-requests-limit: 5
|
|
11
|
+
commit-message:
|
|
12
|
+
prefix: "chore(deps)"
|
|
13
|
+
labels:
|
|
14
|
+
- "dependencies"
|
|
15
|
+
|
|
16
|
+
# GitHub Actions used in workflows.
|
|
17
|
+
- package-ecosystem: "github-actions"
|
|
18
|
+
directory: "/"
|
|
19
|
+
schedule:
|
|
20
|
+
interval: "weekly"
|
|
21
|
+
day: "monday"
|
|
22
|
+
open-pull-requests-limit: 5
|
|
23
|
+
commit-message:
|
|
24
|
+
prefix: "chore(ci)"
|
|
25
|
+
labels:
|
|
26
|
+
- "dependencies"
|
|
27
|
+
- "github-actions"
|
|
@@ -9,6 +9,7 @@ on:
|
|
|
9
9
|
paths:
|
|
10
10
|
- "**/*.py"
|
|
11
11
|
- "pyproject.toml"
|
|
12
|
+
- "uv.lock"
|
|
12
13
|
- "tests/**"
|
|
13
14
|
- ".github/workflows/ci.yml"
|
|
14
15
|
push:
|
|
@@ -18,12 +19,13 @@ on:
|
|
|
18
19
|
paths:
|
|
19
20
|
- "**/*.py"
|
|
20
21
|
- "pyproject.toml"
|
|
22
|
+
- "uv.lock"
|
|
21
23
|
- "tests/**"
|
|
22
24
|
- ".github/workflows/ci.yml"
|
|
23
25
|
|
|
24
26
|
jobs:
|
|
25
|
-
|
|
26
|
-
name:
|
|
27
|
+
lint:
|
|
28
|
+
name: Lint & Type
|
|
27
29
|
runs-on: ubuntu-latest
|
|
28
30
|
steps:
|
|
29
31
|
- uses: actions/checkout@v4
|
|
@@ -32,18 +34,23 @@ jobs:
|
|
|
32
34
|
with:
|
|
33
35
|
version: "latest"
|
|
34
36
|
enable-cache: true
|
|
35
|
-
cache-dependency-glob: "
|
|
37
|
+
cache-dependency-glob: "uv.lock"
|
|
36
38
|
|
|
37
39
|
- run: uv python install 3.11
|
|
38
|
-
- run: uv sync --group dev
|
|
40
|
+
- run: uv sync --group dev --frozen
|
|
39
41
|
|
|
40
42
|
- run: uv run ruff check .
|
|
41
43
|
- run: uv run ruff format --check .
|
|
44
|
+
- run: uv run mypy settfex/
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
name:
|
|
46
|
+
test:
|
|
47
|
+
name: Test (py${{ matrix.python-version }})
|
|
45
48
|
runs-on: ubuntu-latest
|
|
46
|
-
needs:
|
|
49
|
+
needs: lint
|
|
50
|
+
strategy:
|
|
51
|
+
fail-fast: false
|
|
52
|
+
matrix:
|
|
53
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
47
54
|
steps:
|
|
48
55
|
- uses: actions/checkout@v4
|
|
49
56
|
|
|
@@ -51,15 +58,15 @@ jobs:
|
|
|
51
58
|
with:
|
|
52
59
|
version: "latest"
|
|
53
60
|
enable-cache: true
|
|
54
|
-
cache-dependency-glob: "
|
|
61
|
+
cache-dependency-glob: "uv.lock"
|
|
55
62
|
|
|
56
|
-
- run: uv python install
|
|
57
|
-
- run: uv sync --group dev
|
|
63
|
+
- run: uv python install ${{ matrix.python-version }}
|
|
64
|
+
- run: uv sync --group dev --frozen
|
|
58
65
|
|
|
59
|
-
- run: uv run mypy settfex/
|
|
60
66
|
- run: uv run pytest --cov=settfex --cov-branch --cov-report=xml --cov-report=term-missing -v
|
|
61
67
|
|
|
62
68
|
- name: Upload coverage to Codecov
|
|
69
|
+
if: matrix.python-version == '3.12'
|
|
63
70
|
uses: codecov/codecov-action@v4
|
|
64
71
|
with:
|
|
65
72
|
files: coverage.xml
|
|
@@ -66,10 +66,10 @@ jobs:
|
|
|
66
66
|
with:
|
|
67
67
|
version: "latest"
|
|
68
68
|
enable-cache: true
|
|
69
|
-
cache-dependency-glob: "
|
|
69
|
+
cache-dependency-glob: "uv.lock"
|
|
70
70
|
|
|
71
71
|
- run: uv python install 3.11
|
|
72
|
-
- run: uv sync --group dev
|
|
72
|
+
- run: uv sync --group dev --frozen
|
|
73
73
|
|
|
74
74
|
- run: uv run ruff check .
|
|
75
75
|
- run: uv run ruff format --check .
|
|
@@ -89,7 +89,7 @@ jobs:
|
|
|
89
89
|
with:
|
|
90
90
|
version: "latest"
|
|
91
91
|
enable-cache: true
|
|
92
|
-
cache-dependency-glob: "
|
|
92
|
+
cache-dependency-glob: "uv.lock"
|
|
93
93
|
|
|
94
94
|
- run: uv python install 3.11
|
|
95
95
|
- run: uv run python -m build
|
|
@@ -141,7 +141,6 @@ jobs:
|
|
|
141
141
|
path: dist/
|
|
142
142
|
|
|
143
143
|
- name: Extract changelog section
|
|
144
|
-
id: changelog
|
|
145
144
|
run: |
|
|
146
145
|
VERSION="${{ needs.validate.outputs.version }}"
|
|
147
146
|
python3 - "$VERSION" <<'PYEOF'
|
|
@@ -152,19 +151,14 @@ jobs:
|
|
|
152
151
|
match = re.search(pattern, content, re.DOTALL)
|
|
153
152
|
notes = match.group(1).strip() if match else f"Release v{version}"
|
|
154
153
|
with open("release_notes.txt", "w") as f:
|
|
155
|
-
f.write(notes)
|
|
154
|
+
f.write(notes + "\n")
|
|
156
155
|
PYEOF
|
|
157
|
-
{
|
|
158
|
-
echo 'notes<<CHANGELOG_EOF'
|
|
159
|
-
cat release_notes.txt
|
|
160
|
-
echo 'CHANGELOG_EOF'
|
|
161
|
-
} >> "$GITHUB_OUTPUT"
|
|
162
156
|
|
|
163
157
|
- name: Create GitHub Release
|
|
164
158
|
uses: softprops/action-gh-release@v2
|
|
165
159
|
with:
|
|
166
160
|
name: "settfex v${{ needs.validate.outputs.version }}"
|
|
167
|
-
|
|
161
|
+
body_path: release_notes.txt
|
|
168
162
|
prerelease: ${{ needs.validate.outputs.is_prerelease }}
|
|
169
163
|
files: dist/*
|
|
170
164
|
make_latest: ${{ needs.validate.outputs.is_prerelease == 'false' }}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: Security
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: "23 3 * * 3" # Wednesdays at 03:23 UTC
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
paths:
|
|
9
|
+
- "**/*.py"
|
|
10
|
+
- "pyproject.toml"
|
|
11
|
+
- "uv.lock"
|
|
12
|
+
- ".github/workflows/security.yml"
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
bandit:
|
|
17
|
+
name: Bandit (static analysis)
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- uses: astral-sh/setup-uv@v5
|
|
23
|
+
with:
|
|
24
|
+
version: "latest"
|
|
25
|
+
enable-cache: true
|
|
26
|
+
cache-dependency-glob: "uv.lock"
|
|
27
|
+
|
|
28
|
+
- run: uv python install 3.12
|
|
29
|
+
- run: uv sync --group dev --frozen
|
|
30
|
+
|
|
31
|
+
# `|| true` so a finding uploads the report instead of failing the job;
|
|
32
|
+
# review findings in the artifact.
|
|
33
|
+
- run: uv run bandit -c pyproject.toml -r settfex -f json -o bandit-results.json || true
|
|
34
|
+
|
|
35
|
+
- name: Upload Bandit results
|
|
36
|
+
if: always()
|
|
37
|
+
uses: actions/upload-artifact@v4
|
|
38
|
+
with:
|
|
39
|
+
name: bandit-results
|
|
40
|
+
path: bandit-results.json
|
|
41
|
+
retention-days: 7
|
|
42
|
+
|
|
43
|
+
pip-audit:
|
|
44
|
+
name: pip-audit (dependency CVEs)
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@v4
|
|
48
|
+
|
|
49
|
+
- uses: astral-sh/setup-uv@v5
|
|
50
|
+
with:
|
|
51
|
+
version: "latest"
|
|
52
|
+
enable-cache: true
|
|
53
|
+
cache-dependency-glob: "uv.lock"
|
|
54
|
+
|
|
55
|
+
- run: uv python install 3.12
|
|
56
|
+
- run: uv sync --group dev --frozen
|
|
57
|
+
|
|
58
|
+
# Non-blocking: surfaces dependency CVEs for triage without blocking PRs.
|
|
59
|
+
# Some advisories are transitive or not-yet-fixed upstream (e.g. no fix
|
|
60
|
+
# released), so review results and bump deps in a dedicated PR.
|
|
61
|
+
- run: uv run pip-audit --desc
|
|
62
|
+
continue-on-error: true
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Pre-commit hooks mirroring the CI quality gate.
|
|
2
|
+
# Install once: uv run pre-commit install
|
|
3
|
+
# Run manually: uv run pre-commit run --all-files
|
|
4
|
+
# Update revs: uv run pre-commit autoupdate
|
|
5
|
+
repos:
|
|
6
|
+
# Ruff: lint (with autofix) + format — matches CI `ruff check` / `ruff format`.
|
|
7
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
8
|
+
rev: v0.13.2
|
|
9
|
+
hooks:
|
|
10
|
+
- id: ruff
|
|
11
|
+
args: [--fix]
|
|
12
|
+
- id: ruff-format
|
|
13
|
+
|
|
14
|
+
# Standard hygiene hooks.
|
|
15
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
16
|
+
rev: v5.0.0
|
|
17
|
+
hooks:
|
|
18
|
+
- id: trailing-whitespace
|
|
19
|
+
- id: end-of-file-fixer
|
|
20
|
+
- id: check-yaml
|
|
21
|
+
- id: check-toml
|
|
22
|
+
- id: check-added-large-files
|
|
23
|
+
- id: check-merge-conflict
|
|
24
|
+
- id: detect-private-key
|
|
25
|
+
|
|
26
|
+
# mypy runs as a local hook via `uv run` so it uses the project environment
|
|
27
|
+
# and the strict config in pyproject.toml — identical to CI (`mypy settfex/`).
|
|
28
|
+
- repo: local
|
|
29
|
+
hooks:
|
|
30
|
+
- id: mypy
|
|
31
|
+
name: mypy (settfex)
|
|
32
|
+
entry: uv run mypy settfex/
|
|
33
|
+
language: system
|
|
34
|
+
types: [python]
|
|
35
|
+
pass_filenames: false
|
|
36
|
+
files: ^settfex/
|
|
@@ -5,6 +5,47 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-06-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **TFEX underlying-price service** (`get_underlying_price`, `TFEXUnderlyingPriceService`,
|
|
13
|
+
`UnderlyingPrice`): fetches the underlying instrument price for a TFEX series via
|
|
14
|
+
`GET /api/set/tfex/series/{symbol}/underlying-price`. For SET50 index options/futures the underlying
|
|
15
|
+
is the **SET50 index spot** — exposes last/prior/high/low, change, total volume/value, and P/E + P/BV.
|
|
16
|
+
Mirrors the existing TFEX service pattern (SessionManager/Incapsula bypass, NaN-rejecting hardened
|
|
17
|
+
parsing, `get_*` convenience function + `*_raw` variant); 100% module test coverage. Adds the
|
|
18
|
+
`verify_underlying_price.py` script and the `examples/tfex/03_underlying_price.ipynb` notebook.
|
|
19
|
+
|
|
20
|
+
## [0.2.1] - 2026-06-17
|
|
21
|
+
|
|
22
|
+
Robustness and concurrency hardening release. No public API changes — function
|
|
23
|
+
signatures, return types, Pydantic model fields, and `en`/`th` + symbol normalization
|
|
24
|
+
are all preserved. See `COMPREHENSIVE_AUDIT.md` for full details and benchmarks.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Silent financial-data corruption:** `NaN`/`Infinity` values from the SET/TFEX APIs were
|
|
29
|
+
silently accepted into numeric model fields (prices, P/E, margins). They are now rejected at
|
|
30
|
+
decode time with a clear error that includes the originating symbol and endpoint.
|
|
31
|
+
- Parse and validation failures now raise with **symbol + endpoint context** (and per-item
|
|
32
|
+
index for lists) instead of a bare, context-free `ValidationError`/`JSONDecodeError`.
|
|
33
|
+
- Replaced unsafe `assert isinstance(data, dict)` in the TFEX trading-statistics and
|
|
34
|
+
series-list raw paths — `assert` is stripped under `python -O` — with explicit, contextful
|
|
35
|
+
errors.
|
|
36
|
+
- **Session warm-up stampede:** concurrent cold-start callers each fired their own warm-up
|
|
37
|
+
round-trip (which can trip bot detection); warm-up is now serialized to run at most once.
|
|
38
|
+
- Offloaded blocking cache initialization (directory creation + opening the on-disk cache) off
|
|
39
|
+
the asyncio event loop.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- Centralized JSON decode + Pydantic validation across all SET/TFEX services into a shared
|
|
44
|
+
internal helper (`settfex/utils/parsing.py`), removing ~111 lines of duplicated boilerplate.
|
|
45
|
+
- Hoisted static request headers in `AsyncDataFetcher.fetch()` to a module-level constant.
|
|
46
|
+
- Added regression tests for malformed/NaN/partial responses and TFEX coverage
|
|
47
|
+
(test suite 116 → 149; coverage 49% → 61%).
|
|
48
|
+
|
|
8
49
|
## [0.2.0] - 2026-06-09
|
|
9
50
|
|
|
10
51
|
### Added
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
|
6
|
+
|
|
7
|
+
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
|
8
|
+
|
|
9
|
+
## Our Standards
|
|
10
|
+
|
|
11
|
+
Examples of behavior that contributes to a positive environment for our community include:
|
|
12
|
+
|
|
13
|
+
* Demonstrating empathy and kindness toward other people
|
|
14
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
|
15
|
+
* Giving and gracefully accepting constructive feedback
|
|
16
|
+
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
|
17
|
+
* Focusing on what is best not just for us as individuals, but for the overall community
|
|
18
|
+
|
|
19
|
+
Examples of unacceptable behavior include:
|
|
20
|
+
|
|
21
|
+
* The use of sexualized language or imagery, and sexual attention or advances of any kind
|
|
22
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
23
|
+
* Public or private harassment
|
|
24
|
+
* Publishing others' private information, such as a physical or email address, without their explicit permission
|
|
25
|
+
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
|
26
|
+
|
|
27
|
+
## Enforcement Responsibilities
|
|
28
|
+
|
|
29
|
+
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
|
30
|
+
|
|
31
|
+
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
|
32
|
+
|
|
33
|
+
## Scope
|
|
34
|
+
|
|
35
|
+
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
|
36
|
+
|
|
37
|
+
## Enforcement
|
|
38
|
+
|
|
39
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at b@candythink.com. All complaints will be reviewed and investigated promptly and fairly.
|
|
40
|
+
|
|
41
|
+
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
|
42
|
+
|
|
43
|
+
## Enforcement Guidelines
|
|
44
|
+
|
|
45
|
+
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
|
46
|
+
|
|
47
|
+
### 1. Correction
|
|
48
|
+
|
|
49
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
|
50
|
+
|
|
51
|
+
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
|
52
|
+
|
|
53
|
+
### 2. Warning
|
|
54
|
+
|
|
55
|
+
**Community Impact**: A violation through a single incident or series of actions.
|
|
56
|
+
|
|
57
|
+
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
|
58
|
+
|
|
59
|
+
### 3. Temporary Ban
|
|
60
|
+
|
|
61
|
+
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
|
62
|
+
|
|
63
|
+
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
|
64
|
+
|
|
65
|
+
### 4. Permanent Ban
|
|
66
|
+
|
|
67
|
+
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
|
68
|
+
|
|
69
|
+
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
|
70
|
+
|
|
71
|
+
## Attribution
|
|
72
|
+
|
|
73
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
|
74
|
+
|
|
75
|
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
|
76
|
+
|
|
77
|
+
For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations].
|
|
78
|
+
|
|
79
|
+
[homepage]: https://www.contributor-covenant.org
|
|
80
|
+
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
|
81
|
+
[Mozilla CoC]: https://github.com/mozilla/diversity
|
|
82
|
+
[FAQ]: https://www.contributor-covenant.org/faq
|
|
83
|
+
[translations]: https://www.contributor-covenant.org/translations
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# settfex Hardening Pass — Comprehensive Audit
|
|
2
|
+
|
|
3
|
+
**Branch:** `claude-settfex-overhaul` (off `chore/adopt-python-template-tooling`)
|
|
4
|
+
**Scope:** robustness / financial-correctness + performance / concurrency hardening of the
|
|
5
|
+
SET/TFEX async client, with the public API contract preserved exactly.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Executive summary
|
|
10
|
+
|
|
11
|
+
| Metric | Before | After |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Tests passing | 116 | **149** (+33) |
|
|
14
|
+
| Test runtime | 3.77 s | 2.81 s |
|
|
15
|
+
| Coverage (gate = 45%) | 49.26% | **61.25%** (+11.99 pts) |
|
|
16
|
+
| `ruff check` / `ruff format` / `mypy --strict` | clean | clean |
|
|
17
|
+
|
|
18
|
+
- **5 classes of defect fixed** — the headline one being **silent NaN/Infinity acceptance**
|
|
19
|
+
into financial models (prices, P/E, margins), plus context-free parse failures, an
|
|
20
|
+
`assert`-based type check stripped under `python -O`, a cold-start **warmup stampede**, and
|
|
21
|
+
blocking disk I/O on the event loop.
|
|
22
|
+
- **~15 service parse sites** routed through one hardened helper, **removing ~111 net lines**
|
|
23
|
+
of duplicated decode/validation boilerplate while *adding* guarantees.
|
|
24
|
+
- **+33 regression tests**; two previously-0%-covered TFEX services now tested.
|
|
25
|
+
- **Performance:** measured every candidate; applied only the wins that benchmarks justified
|
|
26
|
+
(header hoist; warmup stampede 25→1). Notably, **blanket lazy-logging was measured to be a
|
|
27
|
+
regression here and deliberately NOT applied** (see §3).
|
|
28
|
+
- **Public API:** unchanged — verified by signature/return-type/field smoke test (§6).
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 2. Bugs & robustness
|
|
33
|
+
|
|
34
|
+
The shared fix is a new internal module **`settfex/utils/parsing.py`**
|
|
35
|
+
(`decode_json`, `validate_or_raise`, `validate_list_or_raise`, `ResponseParseError`), routed
|
|
36
|
+
into every service. Untrusted payloads still go through **full** Pydantic validation
|
|
37
|
+
(`model_validate`) — no `model_construct` bypass.
|
|
38
|
+
|
|
39
|
+
| # | Location (pre-change) | Trigger (malformed input) | Root cause | Fix | Regression test |
|
|
40
|
+
|---|---|---|---|---|---|
|
|
41
|
+
| 1 | All numeric models, via every service decode + `data_fetcher.fetch_json` | API returns `{"pe": NaN}` / `Infinity` / `-Infinity` | `json.loads` accepts non-finite literals by default and Pydantic `float` defaults to `allow_inf_nan=True`, so a non-finite **price/P-E/margin enters the model silently** | `decode_json` parses with `parse_constant=` a hook that **rejects** NaN/Infinity, raising `ResponseParseError` with symbol+endpoint context | `test_parsing.py::TestDecodeJson::test_nonfinite_rejected`; end-to-end `test_shareholder.py::...rejects_nan`, `test_financial.py::...rejects_nan_in_financial_amount`, `test_data_fetcher.py::...rejects_nonfinite` |
|
|
42
|
+
| 2 | Every `Model(**data)` / `[Model(**x) for x in data]` across ~15 services | Any missing key / wrong type / partial record | Bare `pydantic.ValidationError` surfaced with **no symbol/endpoint**, so logs/tracebacks weren't actionable | `validate_or_raise` / `validate_list_or_raise` log `symbol (endpoint)` (+ item index for lists) and re-raise; decode failures wrapped in `ResponseParseError` carrying context | `test_parsing.py` (unit); `test_board_of_director/corporate_action/shareholder ...json_decode_error` now assert the symbol appears |
|
|
43
|
+
| 3 | `tfex/trading_statistics.py:210`, `tfex/list.py:276` | Raw endpoint returns a JSON array (or `null`) instead of an object | `assert isinstance(data, dict)` is **stripped under `python -O`** and yields a context-free `AssertionError` | Explicit `if not isinstance(data, dict): raise ResponseParseError(... symbol/endpoint ...)` | `test_trading_statistics.py::...raw_rejects_non_dict_response`, `test_list.py::...raw_rejects_non_dict_response` |
|
|
44
|
+
| 4 | `session_manager.py::ensure_initialized` | N concurrent fetches on a cold cache (first use) | Method was unguarded: every coroutine passed the `not _initialized` check before any finished warming up → **N duplicate warmup round-trips** (wasteful; can trip Incapsula bot detection) | Per-instance `asyncio.Lock` serializes init; double-checked so the rest reuse the warmed session. **Measured 25 → 1** | `test_session_manager.py::...concurrent_cold_start_warms_once` |
|
|
45
|
+
| 5 | `session_cache.py::get_global_cache` | First cache use (cold) | `SessionCache.__init__` does blocking `mkdir` + opens the diskcache SQLite DB **directly on the event loop** | Construction offloaded via `asyncio.to_thread` | covered by session-manager init path; existing cache tests still green |
|
|
46
|
+
|
|
47
|
+
Also hardened in passing: the existing list-shape guards (`Expected list response …`) were
|
|
48
|
+
**kept** for defense-in-depth and backward-compatible non-list behavior; `validate_list_or_raise`
|
|
49
|
+
adds the same guard centrally with per-item index context.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 3. Performance & memory
|
|
54
|
+
|
|
55
|
+
Method: throwaway `timeit`/`tracemalloc` micro-benchmarks (200k iterations, loguru at ERROR so
|
|
56
|
+
`debug()` is disabled — the production default), deleted before finishing. Numbers are µs/call.
|
|
57
|
+
|
|
58
|
+
| Hot path | Change | Before | After | Verdict |
|
|
59
|
+
|---|---|---|---|---|
|
|
60
|
+
| Cold-start warmup under concurrency | per-instance init lock | **25** round-trips (25 callers) | **1** round-trip | **Real correctness + latency + politeness win** (avoids bot-detection risk) |
|
|
61
|
+
| `fetch()` request headers | hoist 11-entry literal → module constant, copy per call | 0.378 / 0.385 | **0.146** | Real ~2.6× micro-win (≈0.2µs/call; negligible vs network, zero-risk, de-duplicates) |
|
|
62
|
+
| JSON decode NaN guard | `json.loads(parse_constant=…)` on finite payload | 53.7 | ~50–52 | **Free** — within run-to-run noise; the hook fires only on NaN/Inf tokens |
|
|
63
|
+
| Pydantic build | `Model(**d)` → `model_validate(d)` | 2.11 | ~2.09 | Equivalent within noise (~0.3µs); no regression from the switch |
|
|
64
|
+
| Decode/validate de-duplication | ~15 inline `import json` + try/except blocks → 1 helper | — | — | **−111 net LOC**; fewer per-call closures/objects, single maintained path |
|
|
65
|
+
| Debug logging (`text[:500]`, `list(keys())`) | **considered `logger.opt(lazy=True)`; rejected** | eager 0.755 | lazy **1.841** | **Lazy is WORSE here** — `.opt()` overhead exceeds the bounded/cheap interpolation it would defer; kept eager (see note) |
|
|
66
|
+
|
|
67
|
+
**Lazy-logging note (evidence over assumption).** The brief suggested converting eager debug
|
|
68
|
+
logs to loguru lazy form. Benchmarks show that for this codebase's debug statements — all cheap,
|
|
69
|
+
bounded interpolations (`text[:500]`, `list(data.keys())` over ~15–40 keys) — `logger.opt(lazy=True)`
|
|
70
|
+
*adds* ~1µs/call of its own overhead and is a net regression when the level is disabled. There are
|
|
71
|
+
no debug logs inside hot loops or over unbounded data. So lazy conversion was **measured and
|
|
72
|
+
declined** rather than applied blindly; the logging win instead came from **removing ~15 duplicated
|
|
73
|
+
decode-error log lines** into the single helper. (At request scale all of these are <2µs vs ~50µs
|
|
74
|
+
JSON parse + network I/O, i.e. noise either way — reported honestly, not inflated.)
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 4. Test summary
|
|
79
|
+
|
|
80
|
+
| | Before | After |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| Passing | 116 | 149 |
|
|
83
|
+
| Runtime | 3.77 s | 2.81 s |
|
|
84
|
+
| Coverage | 49.26% | 61.25% |
|
|
85
|
+
|
|
86
|
+
New tests (+33): `tests/utils/test_parsing.py` (18), `tests/utils/test_session_manager.py` (3),
|
|
87
|
+
`tests/services/tfex/test_trading_statistics.py` (5), `tests/services/tfex/test_list.py` (4),
|
|
88
|
+
plus end-to-end NaN/decode cases in `test_data_fetcher.py`, `test_shareholder.py`,
|
|
89
|
+
`test_financial.py`. Three existing JSON-decode tests were updated from the over-specific
|
|
90
|
+
`json.JSONDecodeError` to the new context-rich `ResponseParseError` (now also asserting the symbol).
|
|
91
|
+
|
|
92
|
+
Coverage movers: `utils/parsing.py` 100%; `tfex/list.py` 0% → ~80%;
|
|
93
|
+
`tfex/trading_statistics.py` 0% → covered; `session_manager.py` 14% → 45%.
|
|
94
|
+
|
|
95
|
+
**Slowest-tests delta:** unchanged in shape — the four `test_data_fetcher` retry/rate-limit tests
|
|
96
|
+
(~0.30 s each) still dominate because they exercise real `retry_delay`/`rate_limit_delay`
|
|
97
|
+
`asyncio.sleep`s. The new concurrency and NaN tests run in ~0.01 s; no new slow tests introduced.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 5. Risks / deferred (intentionally not changed)
|
|
102
|
+
|
|
103
|
+
- **NaN rejection is coarse.** A single non-finite field fails the whole record (with context),
|
|
104
|
+
chosen deliberately for financial correctness over silently dropping data. `parse_constant`
|
|
105
|
+
catches the realistic case (a backend serializing the `NaN`/`Infinity` literal). It does **not**
|
|
106
|
+
catch numeric *overflow* to `inf` (e.g. `1e999`, which `json` parses to `float('inf')` without a
|
|
107
|
+
constant token). **Recommendation:** add `model_config = ConfigDict(allow_inf_nan=False)` to the
|
|
108
|
+
numeric-heavy models as a belt-and-suspenders follow-up; deferred here to avoid touching every
|
|
109
|
+
model's config in this pass.
|
|
110
|
+
- **Module/class-scope `asyncio.Lock`** (`session_manager._lock`, `session_cache._cache_lock`):
|
|
111
|
+
reviewed and left as-is. On Python 3.11 (the project floor) `asyncio.Lock()` binds to the running
|
|
112
|
+
loop lazily on first `await` (since 3.10), so construction at import/class scope is safe for the
|
|
113
|
+
normal single-event-loop process. Caveat: code that drives the singletons from *multiple* event
|
|
114
|
+
loops in one process could hit "bound to a different loop"; this pre-exists and the constraint to
|
|
115
|
+
not introduce new global state / respect the singleton argued against changing it now.
|
|
116
|
+
- **Monetary/price fields use `float`, not `Decimal`.** This is the existing **public** Pydantic
|
|
117
|
+
contract; switching would break field types, so per the brief it is **documented, not changed**.
|
|
118
|
+
Worth a future decision if exact decimal precision becomes a requirement.
|
|
119
|
+
- **No comma/locale-formatted numeric coercion was added.** An early hypothesis was that the API
|
|
120
|
+
might return `"1,234.56"` strings (which would fail `float()`); the test fixtures and observed
|
|
121
|
+
payloads use **native JSON numbers**, and adding silent comma-stripping coercion risks masking
|
|
122
|
+
malformed data. Left out by design; flagged as a watch item if such a payload is ever observed.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 6. Public API compatibility
|
|
127
|
+
|
|
128
|
+
Confirmed unchanged (smoke-tested via `inspect.signature` + attribute checks):
|
|
129
|
+
|
|
130
|
+
- `fetch_*()` (models), `fetch_*_raw()` (dicts), and `get_*()` convenience functions keep identical
|
|
131
|
+
signatures and return types (`symbol`, `lang="en"`, `config=None`); the unified
|
|
132
|
+
`Stock` class and all its accessor methods are present; `en`/`th` + symbol normalization
|
|
133
|
+
(`Stock("cpall").symbol == "CPALL"`) is intact.
|
|
134
|
+
- Pydantic model class names, field names, and field types are untouched.
|
|
135
|
+
- The new `settfex/utils/parsing.py` is **internal** (not exported from `settfex.utils.__all__`).
|
|
136
|
+
- Exception types: the documented `Raises: ValueError` contract is preserved —
|
|
137
|
+
`ResponseParseError` subclasses `ValueError`, and original exceptions are chained (`from e`) or
|
|
138
|
+
re-raised unchanged (`ValidationError`), so existing `except ValueError`/`Exception` handlers and
|
|
139
|
+
`pytest.raises` continue to work.
|