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.
Files changed (139) hide show
  1. settfex-0.3.0/.editorconfig +23 -0
  2. settfex-0.3.0/.github/ISSUE_TEMPLATE/bug_report.md +43 -0
  3. settfex-0.3.0/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
  4. settfex-0.3.0/.github/PULL_REQUEST_TEMPLATE.md +26 -0
  5. settfex-0.3.0/.github/dependabot.yml +27 -0
  6. {settfex-0.2.0 → settfex-0.3.0}/.github/workflows/ci.yml +18 -11
  7. {settfex-0.2.0 → settfex-0.3.0}/.github/workflows/release.yml +5 -11
  8. settfex-0.3.0/.github/workflows/security.yml +62 -0
  9. settfex-0.3.0/.pre-commit-config.yaml +36 -0
  10. {settfex-0.2.0 → settfex-0.3.0}/CHANGELOG.md +41 -0
  11. settfex-0.3.0/CODE_OF_CONDUCT.md +83 -0
  12. settfex-0.3.0/COMPREHENSIVE_AUDIT.md +139 -0
  13. settfex-0.3.0/CONTRIBUTING.md +73 -0
  14. {settfex-0.2.0 → settfex-0.3.0}/PKG-INFO +1 -1
  15. settfex-0.3.0/RELEASING.md +73 -0
  16. settfex-0.3.0/SECURITY.md +49 -0
  17. {settfex-0.2.0 → settfex-0.3.0}/examples/set/01_stock_list.ipynb +76 -72
  18. {settfex-0.2.0 → settfex-0.3.0}/examples/set/02_highlight_data.ipynb +128 -88
  19. {settfex-0.2.0 → settfex-0.3.0}/examples/set/03_stock_profile.ipynb +35 -28
  20. {settfex-0.2.0 → settfex-0.3.0}/examples/set/04_company_profile.ipynb +43 -32
  21. {settfex-0.2.0 → settfex-0.3.0}/examples/set/05_corporate_action.ipynb +53 -39
  22. {settfex-0.2.0 → settfex-0.3.0}/examples/set/06_shareholder.ipynb +22 -17
  23. {settfex-0.2.0 → settfex-0.3.0}/examples/set/07_nvdr_holder.ipynb +18 -11
  24. {settfex-0.2.0 → settfex-0.3.0}/examples/set/08_board_of_director.ipynb +18 -16
  25. {settfex-0.2.0 → settfex-0.3.0}/examples/set/09_trading_statistics.ipynb +40 -36
  26. {settfex-0.2.0 → settfex-0.3.0}/examples/set/10_price_performance.ipynb +29 -24
  27. {settfex-0.2.0 → settfex-0.3.0}/examples/set/11_financial.ipynb +64 -46
  28. {settfex-0.2.0 → settfex-0.3.0}/examples/tfex/01_series_list.ipynb +61 -59
  29. {settfex-0.2.0 → settfex-0.3.0}/examples/tfex/02_trading_statistics.ipynb +104 -103
  30. settfex-0.3.0/examples/tfex/03_underlying_price.ipynb +564 -0
  31. {settfex-0.2.0 → settfex-0.3.0}/pyproject.toml +22 -3
  32. {settfex-0.2.0 → settfex-0.3.0}/settfex/__init__.py +1 -1
  33. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/list.py +10 -14
  34. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/board_of_director.py +15 -38
  35. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/chart_quotation.py +11 -20
  36. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/corporate_action.py +11 -28
  37. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/financial/financial.py +18 -37
  38. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/highlight_data.py +15 -38
  39. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/latest_historical_trading.py +22 -22
  40. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/nvdr_holder.py +11 -28
  41. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/price_performance.py +11 -26
  42. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/profile_company.py +11 -27
  43. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/profile_stock.py +13 -40
  44. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/shareholder.py +12 -31
  45. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/stock.py +9 -11
  46. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/trading_stat.py +15 -38
  47. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/tfex/__init__.py +11 -0
  48. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/tfex/constants.py +1 -0
  49. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/tfex/list.py +13 -19
  50. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/tfex/trading_statistics.py +14 -13
  51. settfex-0.3.0/settfex/services/tfex/underlying_price.py +253 -0
  52. {settfex-0.2.0 → settfex-0.3.0}/settfex/utils/data_fetcher.py +31 -32
  53. settfex-0.3.0/settfex/utils/parsing.py +123 -0
  54. {settfex-0.2.0 → settfex-0.3.0}/settfex/utils/session_cache.py +13 -11
  55. {settfex-0.2.0 → settfex-0.3.0}/settfex/utils/session_manager.py +20 -19
  56. {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/stock/financial/test_financial.py +99 -47
  57. {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/test_board_of_director.py +3 -4
  58. {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/test_corporate_action.py +3 -3
  59. {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/test_shareholder.py +20 -1
  60. settfex-0.3.0/tests/services/tfex/test_list.py +73 -0
  61. settfex-0.3.0/tests/services/tfex/test_trading_statistics.py +79 -0
  62. settfex-0.3.0/tests/services/tfex/test_underlying_price.py +143 -0
  63. {settfex-0.2.0 → settfex-0.3.0}/tests/utils/test_data_fetcher.py +30 -8
  64. settfex-0.3.0/tests/utils/test_parsing.py +113 -0
  65. settfex-0.3.0/tests/utils/test_session_manager.py +78 -0
  66. {settfex-0.2.0 → settfex-0.3.0}/uv.lock +332 -1
  67. {settfex-0.2.0 → settfex-0.3.0}/.github/copilot-instructions.md +0 -0
  68. {settfex-0.2.0 → settfex-0.3.0}/.github/instructions/core-architectrual-principles.instructions.md +0 -0
  69. {settfex-0.2.0 → settfex-0.3.0}/.github/instructions/documentation-standards.instructions.md +0 -0
  70. {settfex-0.2.0 → settfex-0.3.0}/.github/instructions/file-organization.instructions.md +0 -0
  71. {settfex-0.2.0 → settfex-0.3.0}/.github/instructions/git-commit.instructions.md +0 -0
  72. {settfex-0.2.0 → settfex-0.3.0}/.github/instructions/python-dependency-management.instructions.md +0 -0
  73. {settfex-0.2.0 → settfex-0.3.0}/.github/prompts/Coding.prompt.md +0 -0
  74. {settfex-0.2.0 → settfex-0.3.0}/.github/prompts/Git-Commit-Reviewer.prompt.md +0 -0
  75. {settfex-0.2.0 → settfex-0.3.0}/.github/prompts/Prompt-Engineer.prompt.md +0 -0
  76. {settfex-0.2.0 → settfex-0.3.0}/.github/prompts/Python-Architect.prompt.md +0 -0
  77. {settfex-0.2.0 → settfex-0.3.0}/.gitignore +0 -0
  78. {settfex-0.2.0 → settfex-0.3.0}/.python-version +0 -0
  79. {settfex-0.2.0 → settfex-0.3.0}/LICENSE +0 -0
  80. {settfex-0.2.0 → settfex-0.3.0}/MANIFEST.in +0 -0
  81. {settfex-0.2.0 → settfex-0.3.0}/README.md +0 -0
  82. {settfex-0.2.0 → settfex-0.3.0}/docs/guide/PYTHON_LIBRARY_BEST_PRACTICES.md +0 -0
  83. {settfex-0.2.0 → settfex-0.3.0}/docs/index.md +0 -0
  84. {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/API_PROTECTION_NOTE.md +0 -0
  85. {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/board_of_director.md +0 -0
  86. {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/corporate_action.md +0 -0
  87. {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/financial.md +0 -0
  88. {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/highlight_data.md +0 -0
  89. {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/list.md +0 -0
  90. {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/nvdr_holder.md +0 -0
  91. {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/price_performance.md +0 -0
  92. {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/profile_company.md +0 -0
  93. {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/profile_stock.md +0 -0
  94. {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/shareholder.md +0 -0
  95. {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/set/trading_stat.md +0 -0
  96. {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/tfex/list.md +0 -0
  97. {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/services/tfex/trading_statistics.md +0 -0
  98. {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/utils/data_fetcher.md +0 -0
  99. {settfex-0.2.0 → settfex-0.3.0}/docs/settfex/utils/session_caching.md +0 -0
  100. {settfex-0.2.0 → settfex-0.3.0}/docs/solution/FINDINGS.md +0 -0
  101. {settfex-0.2.0 → settfex-0.3.0}/docs/solution/Performance Boost Session Caching Implementation.md +0 -0
  102. {settfex-0.2.0 → settfex-0.3.0}/docs/solution/SESSION_CACHE_SUMMARY.md +0 -0
  103. {settfex-0.2.0 → settfex-0.3.0}/docs/solution/SOLUTION_100_PERCENT.md +0 -0
  104. {settfex-0.2.0 → settfex-0.3.0}/examples/README.md +0 -0
  105. {settfex-0.2.0 → settfex-0.3.0}/examples/set/README.md +0 -0
  106. {settfex-0.2.0 → settfex-0.3.0}/examples/set/cpall_balance_sheet.csv +0 -0
  107. {settfex-0.2.0 → settfex-0.3.0}/examples/set/market_dashboard_20251005_173221.csv +0 -0
  108. {settfex-0.2.0 → settfex-0.3.0}/examples/set/market_dashboard_20251005_210737.csv +0 -0
  109. {settfex-0.2.0 → settfex-0.3.0}/examples/set/market_dashboard_20251006_091440.csv +0 -0
  110. {settfex-0.2.0 → settfex-0.3.0}/examples/set/thailand_banking_stocks.csv +0 -0
  111. {settfex-0.2.0 → settfex-0.3.0}/examples/set/thailand_set_main_board.csv +0 -0
  112. {settfex-0.2.0 → settfex-0.3.0}/examples/set/thailand_stock_universe.csv +0 -0
  113. {settfex-0.2.0 → settfex-0.3.0}/settfex/py.typed +0 -0
  114. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/__init__.py +0 -0
  115. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/__init__.py +0 -0
  116. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/constants.py +0 -0
  117. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/__init__.py +9 -9
  118. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/financial/__init__.py +0 -0
  119. {settfex-0.2.0 → settfex-0.3.0}/settfex/services/set/stock/utils.py +0 -0
  120. {settfex-0.2.0 → settfex-0.3.0}/settfex/utils/__init__.py +0 -0
  121. {settfex-0.2.0 → settfex-0.3.0}/settfex/utils/http.py +0 -0
  122. {settfex-0.2.0 → settfex-0.3.0}/settfex/utils/logging.py +0 -0
  123. {settfex-0.2.0 → settfex-0.3.0}/tests/__init__.py +0 -0
  124. {settfex-0.2.0 → settfex-0.3.0}/tests/conftest.py +0 -0
  125. {settfex-0.2.0 → settfex-0.3.0}/tests/services/__init__.py +0 -0
  126. {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/__init__.py +0 -0
  127. {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/stock/financial/__init__.py +0 -0
  128. {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/test_client.py +0 -0
  129. {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/test_historical.py +0 -0
  130. {settfex-0.2.0 → settfex-0.3.0}/tests/services/set/test_realtime.py +0 -0
  131. {settfex-0.2.0 → settfex-0.3.0}/tests/services/tfex/__init__.py +0 -0
  132. {settfex-0.2.0 → settfex-0.3.0}/tests/services/tfex/test_client.py +0 -0
  133. {settfex-0.2.0 → settfex-0.3.0}/tests/services/tfex/test_historical.py +0 -0
  134. {settfex-0.2.0 → settfex-0.3.0}/tests/services/tfex/test_realtime.py +0 -0
  135. {settfex-0.2.0 → settfex-0.3.0}/tests/utils/__init__.py +0 -0
  136. {settfex-0.2.0 → settfex-0.3.0}/tests/utils/test_formatting.py +0 -0
  137. {settfex-0.2.0 → settfex-0.3.0}/tests/utils/test_http.py +0 -0
  138. {settfex-0.2.0 → settfex-0.3.0}/tests/utils/test_logging.py +0 -0
  139. {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
- ruff:
26
- name: Ruff
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: "pyproject.toml"
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
- quality:
44
- name: Type & Tests
46
+ test:
47
+ name: Test (py${{ matrix.python-version }})
45
48
  runs-on: ubuntu-latest
46
- needs: ruff
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: "pyproject.toml"
61
+ cache-dependency-glob: "uv.lock"
55
62
 
56
- - run: uv python install 3.11
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: "pyproject.toml"
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: "pyproject.toml"
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
- body: ${{ steps.changelog.outputs.notes }}
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.