settfex 0.24.2__tar.gz → 0.26.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 (344) hide show
  1. settfex-0.26.0/.github/workflows/live-smoke.yml +81 -0
  2. {settfex-0.24.2 → settfex-0.26.0}/.pre-commit-config.yaml +4 -3
  3. {settfex-0.24.2 → settfex-0.26.0}/AGENTS.md +113 -9
  4. {settfex-0.24.2 → settfex-0.26.0}/CHANGELOG.md +222 -0
  5. {settfex-0.24.2 → settfex-0.26.0}/CLAUDE.md +76 -24
  6. {settfex-0.24.2 → settfex-0.26.0}/PKG-INFO +114 -98
  7. {settfex-0.24.2 → settfex-0.26.0}/README.md +113 -97
  8. settfex-0.26.0/docs/settfex/services/sec/executive_trades.md +257 -0
  9. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/sec/financial_report.md +34 -7
  10. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/analyst_consensus.md +3 -2
  11. {settfex-0.24.2 → settfex-0.26.0}/examples/README.md +8 -0
  12. settfex-0.26.0/examples/sec/02_executive_trades.ipynb +235 -0
  13. {settfex-0.24.2 → settfex-0.26.0}/pyproject.toml +1 -1
  14. {settfex-0.24.2 → settfex-0.26.0}/settfex/__init__.py +25 -1
  15. settfex-0.26.0/settfex/deprecations.py +94 -0
  16. {settfex-0.24.2 → settfex-0.26.0}/settfex/exceptions.py +149 -6
  17. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/sec/__init__.py +27 -1
  18. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/sec/constants.py +35 -0
  19. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/sec/download.py +127 -15
  20. settfex-0.26.0/settfex/services/sec/executive_trades.py +1553 -0
  21. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/sec/financial_report.py +83 -9
  22. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/sec/utils.py +25 -9
  23. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/list.py +71 -1
  24. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/analyst_consensus.py +96 -18
  25. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/thaibma/history.py +17 -1
  26. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/thaibma/yield_curve.py +16 -1
  27. {settfex-0.24.2 → settfex-0.26.0}/settfex/utils/data_fetcher.py +50 -11
  28. {settfex-0.24.2 → settfex-0.26.0}/settfex/utils/logging.py +16 -8
  29. {settfex-0.24.2 → settfex-0.26.0}/settfex/utils/parsing.py +132 -2
  30. settfex-0.26.0/tests/conftest.py +29 -0
  31. {settfex-0.24.2 → settfex-0.26.0}/tests/golden/api_surface.json +1344 -38
  32. settfex-0.26.0/tests/golden/model_contract/sec_executive_trade.json +88 -0
  33. settfex-0.26.0/tests/golden/model_contract/sec_executive_trade_report.json +62 -0
  34. settfex-0.26.0/tests/integration/conftest.py +56 -0
  35. {settfex-0.24.2 → settfex-0.26.0}/tests/integration/test_live_endpoints.py +44 -3
  36. settfex-0.26.0/tests/services/sec/derive_r59_fixtures.py +318 -0
  37. settfex-0.26.0/tests/services/sec/fixtures_sec/README.md +110 -0
  38. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592000082610_th.json +1 -0
  39. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592000092610_th.json +1 -0
  40. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592000102610_th.json +1 -0
  41. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592000142610_th.json +1 -0
  42. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592000152610_th.json +1 -0
  43. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592000392610_th.json +1 -0
  44. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592000452610_th.json +1 -0
  45. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592000522610_th.json +1 -0
  46. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592000532609_th.json +1 -0
  47. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592000542609_th.json +1 -0
  48. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592001242610_th.json +1 -0
  49. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592001262610_th.json +1 -0
  50. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592001352609_th.json +1 -0
  51. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592001352610_th.json +1 -0
  52. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592001372609_th.json +1 -0
  53. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592001432610_en.json +1 -0
  54. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592001432610_th.json +1 -0
  55. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592002622609_th.json +1 -0
  56. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592002632609_th.json +1 -0
  57. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592002772609_th.json +1 -0
  58. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592002782609_th.json +1 -0
  59. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592002882609_th.json +1 -0
  60. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592002892609_th.json +1 -0
  61. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592003112609_th.json +1 -0
  62. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592003122609_th.json +1 -0
  63. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592003452609_th.json +1 -0
  64. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592003462609_th.json +1 -0
  65. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592003632609_th.json +1 -0
  66. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592003652609_th.json +1 -0
  67. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592004042609_th.json +1 -0
  68. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592004052609_th.json +1 -0
  69. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592004192609_th.json +1 -0
  70. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592004202609_th.json +1 -0
  71. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592004532609_th.json +1 -0
  72. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592004542609_th.json +1 -0
  73. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592004802609_th.json +1 -0
  74. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592004812609_th.json +1 -0
  75. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592004992609_th.json +1 -0
  76. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592005002609_th.json +1 -0
  77. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592005172609_th.json +1 -0
  78. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592005182609_th.json +1 -0
  79. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/detail_592005512609_th.json +1 -0
  80. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/en_default_20261007.html +1 -0
  81. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/th_capped_rcv_20261002.html +1 -0
  82. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/th_default_20261007.html +1 -0
  83. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/th_edge_rows.html +1 -0
  84. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/th_empty.html +16 -0
  85. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/th_kcg_txn_202609.html +1 -0
  86. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/th_sep_pairs.html +1 -0
  87. settfex-0.26.0/tests/services/sec/fixtures_sec/r59/th_spali_txn_202609.html +1 -0
  88. settfex-0.26.0/tests/services/sec/test_download_evidence.py +272 -0
  89. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/test_download_result.py +4 -1
  90. settfex-0.26.0/tests/services/sec/test_executive_trades.py +1060 -0
  91. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/test_listing_transport.py +174 -0
  92. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/test_utils.py +23 -0
  93. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/stock/test_analyst_consensus.py +119 -1
  94. settfex-0.26.0/tests/services/set/test_listed_symbol.py +80 -0
  95. {settfex-0.24.2 → settfex-0.26.0}/tests/services/test_fault_matrix.py +27 -1
  96. settfex-0.26.0/tests/test_deprecations.py +147 -0
  97. {settfex-0.24.2 → settfex-0.26.0}/tests/test_documented_exceptions.py +24 -22
  98. {settfex-0.24.2 → settfex-0.26.0}/tests/test_model_contract.py +2 -0
  99. settfex-0.26.0/tests/test_not_found_taxonomy.py +90 -0
  100. {settfex-0.24.2 → settfex-0.26.0}/tests/test_public_api_surface.py +17 -1
  101. {settfex-0.24.2 → settfex-0.26.0}/tests/test_readme_links.py +72 -9
  102. {settfex-0.24.2 → settfex-0.26.0}/tests/test_typing_surface.py +1 -0
  103. settfex-0.26.0/tests/utils/test_blocked_error.py +250 -0
  104. {settfex-0.24.2 → settfex-0.26.0}/tests/utils/test_logging_neighbourliness.py +38 -0
  105. {settfex-0.24.2 → settfex-0.26.0}/uv.lock +61 -45
  106. settfex-0.24.2/tests/conftest.py +0 -1
  107. settfex-0.24.2/tests/services/sec/fixtures_sec/README.md +0 -36
  108. {settfex-0.24.2 → settfex-0.26.0}/.editorconfig +0 -0
  109. {settfex-0.24.2 → settfex-0.26.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  110. {settfex-0.24.2 → settfex-0.26.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  111. {settfex-0.24.2 → settfex-0.26.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  112. {settfex-0.24.2 → settfex-0.26.0}/.github/dependabot.yml +0 -0
  113. {settfex-0.24.2 → settfex-0.26.0}/.github/instructions/core-architectrual-principles.instructions.md +0 -0
  114. {settfex-0.24.2 → settfex-0.26.0}/.github/instructions/documentation-standards.instructions.md +0 -0
  115. {settfex-0.24.2 → settfex-0.26.0}/.github/instructions/file-organization.instructions.md +0 -0
  116. {settfex-0.24.2 → settfex-0.26.0}/.github/instructions/git-commit.instructions.md +0 -0
  117. {settfex-0.24.2 → settfex-0.26.0}/.github/instructions/python-dependency-management.instructions.md +0 -0
  118. {settfex-0.24.2 → settfex-0.26.0}/.github/prompts/Coding.prompt.md +0 -0
  119. {settfex-0.24.2 → settfex-0.26.0}/.github/prompts/Git-Commit-Reviewer.prompt.md +0 -0
  120. {settfex-0.24.2 → settfex-0.26.0}/.github/prompts/Prompt-Engineer.prompt.md +0 -0
  121. {settfex-0.24.2 → settfex-0.26.0}/.github/prompts/Python-Architect.prompt.md +0 -0
  122. {settfex-0.24.2 → settfex-0.26.0}/.github/workflows/ci.yml +0 -0
  123. {settfex-0.24.2 → settfex-0.26.0}/.github/workflows/dependency-drift.yml +0 -0
  124. {settfex-0.24.2 → settfex-0.26.0}/.github/workflows/pre-commit.yml +0 -0
  125. {settfex-0.24.2 → settfex-0.26.0}/.github/workflows/release.yml +0 -0
  126. {settfex-0.24.2 → settfex-0.26.0}/.github/workflows/security.yml +0 -0
  127. {settfex-0.24.2 → settfex-0.26.0}/.gitignore +0 -0
  128. {settfex-0.24.2 → settfex-0.26.0}/.python-version +0 -0
  129. {settfex-0.24.2 → settfex-0.26.0}/CODE_OF_CONDUCT.md +0 -0
  130. {settfex-0.24.2 → settfex-0.26.0}/COMPREHENSIVE_AUDIT.md +0 -0
  131. {settfex-0.24.2 → settfex-0.26.0}/CONTRIBUTING.md +0 -0
  132. {settfex-0.24.2 → settfex-0.26.0}/LICENSE +0 -0
  133. {settfex-0.24.2 → settfex-0.26.0}/RELEASING.md +0 -0
  134. {settfex-0.24.2 → settfex-0.26.0}/SECURITY.md +0 -0
  135. {settfex-0.24.2 → settfex-0.26.0}/docs/guide/PYTHON_LIBRARY_BEST_PRACTICES.md +0 -0
  136. {settfex-0.24.2 → settfex-0.26.0}/docs/index.md +0 -0
  137. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/API_PROTECTION_NOTE.md +0 -0
  138. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/board_of_director.md +0 -0
  139. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/chart_quotation.md +0 -0
  140. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/corporate_action.md +0 -0
  141. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/dr_indicative_price.md +0 -0
  142. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/earnings_call.md +0 -0
  143. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/financial.md +0 -0
  144. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/highlight_data.md +0 -0
  145. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/holiday.md +0 -0
  146. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/index.md +0 -0
  147. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/info.md +0 -0
  148. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/latest_historical_trading.md +0 -0
  149. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/list.md +0 -0
  150. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/news.md +0 -0
  151. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/nvdr_holder.md +0 -0
  152. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/price_performance.md +0 -0
  153. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/profile_company.md +0 -0
  154. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/profile_dr.md +0 -0
  155. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/profile_stock.md +0 -0
  156. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/shareholder.md +0 -0
  157. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/set/trading_stat.md +0 -0
  158. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/tfex/list.md +0 -0
  159. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/tfex/trading_statistics.md +0 -0
  160. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/services/thaibma/yield_curve.md +0 -0
  161. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/utils/data_fetcher.md +0 -0
  162. {settfex-0.24.2 → settfex-0.26.0}/docs/settfex/utils/session_caching.md +0 -0
  163. {settfex-0.24.2 → settfex-0.26.0}/docs/solution/FINDINGS.md +0 -0
  164. {settfex-0.24.2 → settfex-0.26.0}/docs/solution/Performance Boost Session Caching Implementation.md +0 -0
  165. {settfex-0.24.2 → settfex-0.26.0}/docs/solution/SESSION_CACHE_SUMMARY.md +0 -0
  166. {settfex-0.24.2 → settfex-0.26.0}/docs/solution/SOLUTION_100_PERCENT.md +0 -0
  167. {settfex-0.24.2 → settfex-0.26.0}/examples/sec/01_financial_report.ipynb +0 -0
  168. {settfex-0.24.2 → settfex-0.26.0}/examples/set/01_stock_list.ipynb +0 -0
  169. {settfex-0.24.2 → settfex-0.26.0}/examples/set/02_highlight_data.ipynb +0 -0
  170. {settfex-0.24.2 → settfex-0.26.0}/examples/set/03_stock_profile.ipynb +0 -0
  171. {settfex-0.24.2 → settfex-0.26.0}/examples/set/04_company_profile.ipynb +0 -0
  172. {settfex-0.24.2 → settfex-0.26.0}/examples/set/05_corporate_action.ipynb +0 -0
  173. {settfex-0.24.2 → settfex-0.26.0}/examples/set/06_shareholder.ipynb +0 -0
  174. {settfex-0.24.2 → settfex-0.26.0}/examples/set/07_nvdr_holder.ipynb +0 -0
  175. {settfex-0.24.2 → settfex-0.26.0}/examples/set/08_board_of_director.ipynb +0 -0
  176. {settfex-0.24.2 → settfex-0.26.0}/examples/set/09_trading_statistics.ipynb +0 -0
  177. {settfex-0.24.2 → settfex-0.26.0}/examples/set/10_price_performance.ipynb +0 -0
  178. {settfex-0.24.2 → settfex-0.26.0}/examples/set/11_financial.ipynb +0 -0
  179. {settfex-0.24.2 → settfex-0.26.0}/examples/set/12_earnings_call.ipynb +0 -0
  180. {settfex-0.24.2 → settfex-0.26.0}/examples/set/13_chart_quotation.ipynb +0 -0
  181. {settfex-0.24.2 → settfex-0.26.0}/examples/set/14_latest_historical_trading.ipynb +0 -0
  182. {settfex-0.24.2 → settfex-0.26.0}/examples/set/15_market_index.ipynb +0 -0
  183. {settfex-0.24.2 → settfex-0.26.0}/examples/set/16_news.ipynb +0 -0
  184. {settfex-0.24.2 → settfex-0.26.0}/examples/set/17_holiday.ipynb +0 -0
  185. {settfex-0.24.2 → settfex-0.26.0}/examples/set/18_dr_and_asset_type.ipynb +0 -0
  186. {settfex-0.24.2 → settfex-0.26.0}/examples/set/19_analyst_consensus.ipynb +0 -0
  187. {settfex-0.24.2 → settfex-0.26.0}/examples/set/20_stock_info.ipynb +0 -0
  188. {settfex-0.24.2 → settfex-0.26.0}/examples/set/README.md +0 -0
  189. {settfex-0.24.2 → settfex-0.26.0}/examples/set/cpall_balance_sheet.csv +0 -0
  190. {settfex-0.24.2 → settfex-0.26.0}/examples/set/market_dashboard_20251005_173221.csv +0 -0
  191. {settfex-0.24.2 → settfex-0.26.0}/examples/set/market_dashboard_20251005_210737.csv +0 -0
  192. {settfex-0.24.2 → settfex-0.26.0}/examples/set/market_dashboard_20251006_091440.csv +0 -0
  193. {settfex-0.24.2 → settfex-0.26.0}/examples/set/thailand_banking_stocks.csv +0 -0
  194. {settfex-0.24.2 → settfex-0.26.0}/examples/set/thailand_set_main_board.csv +0 -0
  195. {settfex-0.24.2 → settfex-0.26.0}/examples/set/thailand_stock_universe.csv +0 -0
  196. {settfex-0.24.2 → settfex-0.26.0}/examples/tfex/01_series_list.ipynb +0 -0
  197. {settfex-0.24.2 → settfex-0.26.0}/examples/tfex/02_trading_statistics.ipynb +0 -0
  198. {settfex-0.24.2 → settfex-0.26.0}/examples/tfex/03_underlying_price.ipynb +0 -0
  199. {settfex-0.24.2 → settfex-0.26.0}/examples/thaibma/01_government_yield_curve.ipynb +0 -0
  200. {settfex-0.24.2 → settfex-0.26.0}/settfex/py.typed +0 -0
  201. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/__init__.py +0 -0
  202. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/sec/company.py +0 -0
  203. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/sec/sec.py +0 -0
  204. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/__init__.py +0 -0
  205. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/asset_type.py +0 -0
  206. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/constants.py +0 -0
  207. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/earnings_call.py +0 -0
  208. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/holiday.py +0 -0
  209. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/index/__init__.py +0 -0
  210. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/index/chart_quotation.py +0 -0
  211. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/index/composition.py +0 -0
  212. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/index/index.py +0 -0
  213. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/index/info.py +0 -0
  214. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/index/list.py +0 -0
  215. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/index/utils.py +0 -0
  216. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/news.py +0 -0
  217. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/__init__.py +0 -0
  218. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/board_of_director.py +0 -0
  219. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/chart_quotation.py +0 -0
  220. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/corporate_action.py +0 -0
  221. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/dr_indicative_price.py +0 -0
  222. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/financial/__init__.py +0 -0
  223. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/financial/financial.py +0 -0
  224. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/highlight_data.py +0 -0
  225. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/info.py +0 -0
  226. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/latest_historical_trading.py +0 -0
  227. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/nvdr_holder.py +0 -0
  228. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/price_performance.py +0 -0
  229. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/profile_company.py +0 -0
  230. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/profile_dr.py +0 -0
  231. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/profile_stock.py +0 -0
  232. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/shareholder.py +0 -0
  233. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/stock.py +0 -0
  234. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/trading_stat.py +0 -0
  235. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/set/stock/utils.py +0 -0
  236. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/tfex/__init__.py +0 -0
  237. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/tfex/constants.py +0 -0
  238. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/tfex/list.py +0 -0
  239. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/tfex/trading_statistics.py +0 -0
  240. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/tfex/underlying_price.py +0 -0
  241. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/thaibma/__init__.py +0 -0
  242. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/thaibma/availability.py +0 -0
  243. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/thaibma/constants.py +0 -0
  244. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/thaibma/thaibma.py +0 -0
  245. {settfex-0.24.2 → settfex-0.26.0}/settfex/services/thaibma/utils.py +0 -0
  246. {settfex-0.24.2 → settfex-0.26.0}/settfex/utils/__init__.py +0 -0
  247. {settfex-0.24.2 → settfex-0.26.0}/settfex/utils/http.py +0 -0
  248. {settfex-0.24.2 → settfex-0.26.0}/settfex/utils/session_cache.py +0 -0
  249. {settfex-0.24.2 → settfex-0.26.0}/settfex/utils/session_manager.py +0 -0
  250. {settfex-0.24.2 → settfex-0.26.0}/settfex/utils/youtube_transcript.py +0 -0
  251. {settfex-0.24.2 → settfex-0.26.0}/tests/__init__.py +0 -0
  252. {settfex-0.24.2 → settfex-0.26.0}/tests/golden/model_contract/analyst_consensus.json +0 -0
  253. {settfex-0.24.2 → settfex-0.26.0}/tests/golden/model_contract/dr_profile.json +0 -0
  254. {settfex-0.24.2 → settfex-0.26.0}/tests/golden/model_contract/earnings_call_item.json +0 -0
  255. {settfex-0.24.2 → settfex-0.26.0}/tests/golden/model_contract/highlight_data.json +0 -0
  256. {settfex-0.24.2 → settfex-0.26.0}/tests/golden/model_contract/holiday_calendar.json +0 -0
  257. {settfex-0.24.2 → settfex-0.26.0}/tests/golden/model_contract/index_list_response.json +0 -0
  258. {settfex-0.24.2 → settfex-0.26.0}/tests/golden/model_contract/news_search_response.json +0 -0
  259. {settfex-0.24.2 → settfex-0.26.0}/tests/golden/model_contract/sec_company_match.json +0 -0
  260. {settfex-0.24.2 → settfex-0.26.0}/tests/golden/model_contract/stock_info.json +0 -0
  261. {settfex-0.24.2 → settfex-0.26.0}/tests/golden/model_contract/stock_list_response.json +0 -0
  262. {settfex-0.24.2 → settfex-0.26.0}/tests/golden/model_contract/tfex_series_list_response.json +0 -0
  263. {settfex-0.24.2 → settfex-0.26.0}/tests/golden/model_contract/tfex_trading_statistics.json +0 -0
  264. {settfex-0.24.2 → settfex-0.26.0}/tests/golden/model_contract/tfex_underlying_price.json +0 -0
  265. {settfex-0.24.2 → settfex-0.26.0}/tests/golden/model_contract/yield_curve.json +0 -0
  266. {settfex-0.24.2 → settfex-0.26.0}/tests/integration/__init__.py +0 -0
  267. {settfex-0.24.2 → settfex-0.26.0}/tests/services/__init__.py +0 -0
  268. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/__init__.py +0 -0
  269. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/derive_th_fixtures.py +0 -0
  270. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/fixtures.py +0 -0
  271. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/fixtures_sec/capital_indirection_en.html +0 -0
  272. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/fixtures_sec/en_mother_fs_h1_2025.html +0 -0
  273. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/fixtures_sec/en_ptt_56_1.html +0 -0
  274. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/fixtures_sec/en_ptt_56_2.html +0 -0
  275. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/fixtures_sec/en_ptt_fs_h1_2025.html +0 -0
  276. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/fixtures_sec/en_ptt_viewmore_56_2.html +0 -0
  277. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/fixtures_sec/idisc_505_error_page.html +0 -0
  278. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/fixtures_sec/th_cpall_viewmore_56_1.html +0 -0
  279. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/fixtures_sec/th_mother_fs_h1_2025.html +0 -0
  280. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/fixtures_sec/th_ptt_56_1.html +0 -0
  281. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/fixtures_sec/th_ptt_56_2.html +0 -0
  282. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/fixtures_sec/th_ptt_fs_h1_2025.html +0 -0
  283. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/test_code_isolation.py +0 -0
  284. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/test_company.py +0 -0
  285. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/test_download.py +0 -0
  286. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/test_download_indirection.py +0 -0
  287. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/test_financial_report.py +0 -0
  288. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/test_financial_report_accounting.py +0 -0
  289. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/test_financial_report_th.py +0 -0
  290. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/test_sec.py +0 -0
  291. {settfex-0.24.2 → settfex-0.26.0}/tests/services/sec/test_view_more_slugs.py +0 -0
  292. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/__init__.py +0 -0
  293. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/index/__init__.py +0 -0
  294. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/index/test_chart_quotation.py +0 -0
  295. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/index/test_composition.py +0 -0
  296. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/index/test_index.py +0 -0
  297. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/index/test_info.py +0 -0
  298. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/index/test_list.py +0 -0
  299. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/stock/__init__.py +0 -0
  300. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/stock/financial/__init__.py +0 -0
  301. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/stock/financial/test_financial.py +0 -0
  302. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/stock/test_chart_quotation.py +0 -0
  303. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/stock/test_dr_indicative_price.py +0 -0
  304. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/stock/test_info.py +0 -0
  305. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/stock/test_latest_historical_trading.py +0 -0
  306. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/stock/test_profile_dr.py +0 -0
  307. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/test_asset_type.py +0 -0
  308. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/test_board_of_director.py +0 -0
  309. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/test_client.py +0 -0
  310. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/test_corporate_action.py +0 -0
  311. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/test_earnings_call.py +0 -0
  312. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/test_historical.py +0 -0
  313. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/test_holiday.py +0 -0
  314. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/test_list.py +0 -0
  315. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/test_news.py +0 -0
  316. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/test_profile_company.py +0 -0
  317. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/test_realtime.py +0 -0
  318. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/test_shareholder.py +0 -0
  319. {settfex-0.24.2 → settfex-0.26.0}/tests/services/set/test_symbol_suggestion.py +0 -0
  320. {settfex-0.24.2 → settfex-0.26.0}/tests/services/tfex/__init__.py +0 -0
  321. {settfex-0.24.2 → settfex-0.26.0}/tests/services/tfex/test_client.py +0 -0
  322. {settfex-0.24.2 → settfex-0.26.0}/tests/services/tfex/test_historical.py +0 -0
  323. {settfex-0.24.2 → settfex-0.26.0}/tests/services/tfex/test_list.py +0 -0
  324. {settfex-0.24.2 → settfex-0.26.0}/tests/services/tfex/test_realtime.py +0 -0
  325. {settfex-0.24.2 → settfex-0.26.0}/tests/services/tfex/test_trading_statistics.py +0 -0
  326. {settfex-0.24.2 → settfex-0.26.0}/tests/services/tfex/test_underlying_price.py +0 -0
  327. {settfex-0.24.2 → settfex-0.26.0}/tests/services/thaibma/__init__.py +0 -0
  328. {settfex-0.24.2 → settfex-0.26.0}/tests/services/thaibma/fixtures.py +0 -0
  329. {settfex-0.24.2 → settfex-0.26.0}/tests/services/thaibma/test_availability.py +0 -0
  330. {settfex-0.24.2 → settfex-0.26.0}/tests/services/thaibma/test_history.py +0 -0
  331. {settfex-0.24.2 → settfex-0.26.0}/tests/services/thaibma/test_thaibma.py +0 -0
  332. {settfex-0.24.2 → settfex-0.26.0}/tests/services/thaibma/test_utils.py +0 -0
  333. {settfex-0.24.2 → settfex-0.26.0}/tests/services/thaibma/test_yield_curve.py +0 -0
  334. {settfex-0.24.2 → settfex-0.26.0}/tests/test_impersonate_target.py +0 -0
  335. {settfex-0.24.2 → settfex-0.26.0}/tests/utils/__init__.py +0 -0
  336. {settfex-0.24.2 → settfex-0.26.0}/tests/utils/test_data_fetcher.py +0 -0
  337. {settfex-0.24.2 → settfex-0.26.0}/tests/utils/test_formatting.py +0 -0
  338. {settfex-0.24.2 → settfex-0.26.0}/tests/utils/test_http.py +0 -0
  339. {settfex-0.24.2 → settfex-0.26.0}/tests/utils/test_logging.py +0 -0
  340. {settfex-0.24.2 → settfex-0.26.0}/tests/utils/test_parsing.py +0 -0
  341. {settfex-0.24.2 → settfex-0.26.0}/tests/utils/test_session_cache.py +0 -0
  342. {settfex-0.24.2 → settfex-0.26.0}/tests/utils/test_session_manager.py +0 -0
  343. {settfex-0.24.2 → settfex-0.26.0}/tests/utils/test_validation.py +0 -0
  344. {settfex-0.24.2 → settfex-0.26.0}/tests/utils/test_youtube_transcript.py +0 -0
@@ -0,0 +1,81 @@
1
+ # Live smoke — the integration probes against the real SET / TFEX / SEC / ThaiBMA hosts, weekly.
2
+ #
3
+ # INERT BY DESIGN. Nothing runs until BOTH of these are true:
4
+ # 1. a self-hosted runner labelled `thailand` is registered for this repository, and
5
+ # 2. the repository variable LIVE_SMOKE_ENABLED is set to `true`.
6
+ # Until then the job is skipped on every trigger, and no request leaves any machine.
7
+ #
8
+ # Why self-hosted, and why in Thailand: nothing ran the `integration` marker, so the live endpoints
9
+ # had no pass/fail history (#135). GitHub-hosted runners sit outside Thailand, and SET's and SEC's
10
+ # bot protection treats datacenter IPs differently from a Thai residential one, so a green run
11
+ # there would not mean what it appears to mean.
12
+ #
13
+ # BEFORE ENABLING — read these, they are the reason this ships switched off:
14
+ # * Every machine on the home network shares ONE egress IP with production SET market capture,
15
+ # which cannot be backfilled. A WAF block on that IP would hit the capture too. Pick a window
16
+ # that never overlaps a SET session or the sec-idisc-archive's traffic; the schedule below is
17
+ # a Sunday-morning placeholder, not a verified quiet window.
18
+ # * This repository is PUBLIC. A self-hosted runner on a public repo can be made to run code
19
+ # from a pull request. Before registering one, set Settings → Actions → "Fork pull request
20
+ # workflows" approval to ALL outside contributors (it is `first_time_contributors` today).
21
+ # This workflow itself runs only on schedule and manual dispatch, never on pull_request.
22
+ #
23
+ # Politeness, enforced rather than remembered (CLAUDE.md, "Live probes — politeness budget"):
24
+ # * concurrency 1, and `-x`: the first failure may be a WAF block, and the next request deepens it
25
+ # * SETTFEX_LIVE_BUDGET caps request ATTEMPTS through the fetcher (tests/integration/conftest.py);
26
+ # the cap refuses the next request rather than sending it. Session warmups (one page view per
27
+ # site) are not counted.
28
+ # * no retry ladders: the live tests make one attempt each
29
+ #
30
+ # The holiday endpoint has answered 401 upstream since 2026-09-20 (#140), so it runs as a SIGNAL in
31
+ # its own step (continue-on-error) — it would otherwise keep the gate red for a reason no settfex
32
+ # change can fix.
33
+
34
+ name: Live smoke
35
+
36
+ on:
37
+ schedule:
38
+ - cron: "17 3 * * 0" # Sundays 03:17 UTC (10:17 Asia/Bangkok) — placeholder, see above
39
+ workflow_dispatch:
40
+
41
+ permissions:
42
+ contents: read
43
+
44
+ concurrency:
45
+ group: live-smoke
46
+ cancel-in-progress: false
47
+
48
+ jobs:
49
+ live:
50
+ name: Live endpoints (Thailand runner)
51
+ if: vars.LIVE_SMOKE_ENABLED == 'true' && github.repository == 'lumduan/settfex'
52
+ runs-on: [self-hosted, linux, thailand]
53
+ timeout-minutes: 20
54
+ steps:
55
+ - uses: actions/checkout@v7
56
+
57
+ # setup-uv stopped publishing floating major tags at v8 — pin the exact version.
58
+ - uses: astral-sh/setup-uv@v10.1.0
59
+ with:
60
+ # Pinned by hand like every other setup-uv step; the monthly drift report checks them all.
61
+ version: "0.12.17"
62
+ enable-cache: true
63
+ cache-dependency-glob: "uv.lock"
64
+
65
+ - run: uv python install 3.11
66
+ - run: uv sync --group dev --frozen
67
+
68
+ - name: Live probes (gate)
69
+ env:
70
+ SETTFEX_LIVE_BUDGET: "60"
71
+ run: >-
72
+ uv run pytest tests/integration -m integration --no-cov -x
73
+ --deselect tests/integration/test_live_endpoints.py::test_live_set_holidays
74
+
75
+ - name: Holiday endpoint (signal, not a gate — #140)
76
+ continue-on-error: true
77
+ env:
78
+ SETTFEX_LIVE_BUDGET: "1"
79
+ run: >-
80
+ uv run pytest -m integration --no-cov
81
+ tests/integration/test_live_endpoints.py::test_live_set_holidays
@@ -21,11 +21,12 @@ repos:
21
21
  # The SEC listing fixtures are response bodies captured VERBATIM -- byte ranges of what the
22
22
  # server sent, with their sha256 recorded in fixtures_sec/README.md. These two hooks rewrite
23
23
  # a file's trailing bytes, which would break both the byte-exactness and those hashes. The
24
- # live pages genuinely end without a newline; that is data, not untidiness.
24
+ # live pages genuinely end without a newline; that is data, not untidiness. The Form 59
25
+ # detail fixtures (r59/*.json) are verbatim API bodies on the same terms.
25
26
  - id: trailing-whitespace
26
- exclude: ^tests/services/sec/fixtures_sec/.*\.html$
27
+ exclude: ^tests/services/sec/fixtures_sec/.*\.(html|json)$
27
28
  - id: end-of-file-fixer
28
- exclude: ^tests/services/sec/fixtures_sec/.*\.html$
29
+ exclude: ^tests/services/sec/fixtures_sec/.*\.(html|json)$
29
30
  - id: check-yaml
30
31
  - id: check-toml
31
32
  - id: check-added-large-files
@@ -110,6 +110,8 @@ Indices: `get_index_list(lang)`, `get_index_info(symbol, lang)`,
110
110
  | resolve a company to its SEC id | `resolve_company(query, lang, allow_name_match=False)` |
111
111
  | list filings (financial statements, 56-1, 56-2, ratios, MD&A) | `get_sec_documents(query, types=..., from_date=..., to_date=...)` |
112
112
  | download the actual file(s) | `download_sec_document(target)` / `download_sec_documents(targets)` |
113
+ | directors'/executives' trades (Form 59, แบบ 59) | `get_executive_trades(received_date=None, symbol=..., date_type=..., start=..., end=..., with_details=False)` |
114
+ | one Form 59 report with holdings before/after | `get_executive_trade_report(batch_no)` |
113
115
 
114
116
  Dates here are **dd/mm/yyyy**. Pass a wide window to see full year history.
115
117
 
@@ -185,6 +187,69 @@ Also a Pydantic model since 0.24.0 (`{files, failed, requested}` + computed `is_
185
187
  failure report survives `model_dump()` and slicing. ⚠️ `isinstance(files, list)` is `False`; use
186
188
  `files.files`.
187
189
 
190
+ **Form 59 (executive trades) — four traps before you count anything:**
191
+
192
+ - **`reporter_name` is who FILED, not who traded.** The trader is `executor_name` / `executor_id`
193
+ (a spouse, a minor child, a family company). `is_self` says whether they are the same.
194
+ - **One trade can be listed twice**: by both spouses when both are executives, or by two executives
195
+ for one company they control. The copy has `duplicate_of`; `duplicate_basis` says why
196
+ (`holdings` / `executor_own_row` / `executor`). `holding_conflict` is True when the two filings
197
+ disagree on the holdings, False when both are known and agree, and **None when unknown** (a
198
+ report was not fetched) — never read None as "no conflict". Rows are never dropped.
199
+ - **Revoked filings stay listed** (`is_revoked=True`, about 5% of rows, mostly re-filings); with
200
+ `with_details=True` a transaction its report marks `CANCELED` is void too.
201
+ - So **count on `result.economic_trades()`**, which drops copies and void rows. `to_table()`
202
+ already skips void rows. **Known overcount:** a trade the same reporter filed twice in different
203
+ batches, with the first left live, is counted twice (duplicates are never merged within one
204
+ reporter). Example: CREDIT 2026-09-28, 10,000 shares, rows 165104_2_1 and 165237_3_1.
205
+
206
+ With no arguments `get_executive_trades()` returns what SEC **received today**, which is not the
207
+ site's default page (that one shows what was *recorded* today), and today's set can still grow.
208
+ The listing `price` is rounded to 2 dp; `with_details=True` (at most 100 reports per call) adds
209
+ `detail.avg_price` at full precision and the holdings. A page whose stated count differs from its
210
+ rows raises `ParseError`, so a result you receive is complete. A method label the library does not
211
+ know shows `side="other"` and is counted on `result.unknown_labels`.
212
+
213
+ **Form 59 recipes — question → call → what to read:**
214
+
215
+ | Question | Call | Read |
216
+ |---|---|---|
217
+ | Which executives reported trades today? | `get_executive_trades()` | `.economic_trades()`; `.to_table()` for a ready Thai summary |
218
+ | Did insiders of X buy or sell recently? | `get_executive_trades(symbol="X", date_type="transaction", start="YYYY-MM-DD", end="YYYY-MM-DD")` | `side` and `quantity` of each row in `.economic_trades()` |
219
+ | What did the executive hold after the trade? | add `with_details=True`, or `get_executive_trade_report(row.batch_no)` | `row.detail.holding_before` / `holding_after` |
220
+ | The exact average price | `with_details=True` | `row.detail.avg_price` (the listing's `price` is rounded to 2 dp) |
221
+ | On-exchange or off? Who received a transfer? | `with_details=True` | `row.detail.market_source`, `row.detail.counterparty` |
222
+
223
+ Dates here are **ISO `YYYY-MM-DD`** strings or `date` objects. This is unlike `get_sec_documents`,
224
+ which takes dd/mm/yyyy. Keep windows to what the question needs: anything over 366 days is fetched
225
+ in several requests, and `with_details=True` sends one request per report, at most 100.
226
+
227
+ ```python
228
+ from settfex.services.sec import get_executive_trades
229
+
230
+ result = await get_executive_trades(symbol="PANEL", date_type="transaction",
231
+ start="2026-09-01", end="2026-09-30")
232
+ trades = result.economic_trades() # copies and revoked/cancelled filings removed
233
+ bought = sum(t.quantity for t in trades if t.side == "buy" and t.security_type == "หุ้นสามัญ")
234
+ sold = sum(t.quantity for t in trades if t.side == "sell" and t.security_type == "หุ้นสามัญ")
235
+ ```
236
+
237
+ Before you state a number from Form 59:
238
+ - **Count only `economic_trades()`.**
239
+ - **Never add quantities across security types**: shares, warrants and NVDRs are different
240
+ instruments. Filter on `security_type`.
241
+ - **`transfer_in` / `transfer_out` are not market trades** (gifts to children, depositary moves).
242
+ Report them separately.
243
+ - **Name the window and the date type** (received vs transaction), and say the count may include
244
+ the known same-reporter re-filing overcount.
245
+ - **Read `holding_conflict is None` as "unknown"**, not as "no conflict".
246
+ - **With `with_details=True`, check `result.detail_failures` is empty** before relying on
247
+ holdings. Rows of a failed report keep `detail=None`.
248
+
249
+ As JSON (`result.model_dump(mode="json")`), the completeness fields travel with the rows:
250
+ `reported_count`, `revoked_count`, `duplicate_count`, `unknown_labels`, `detail_unmatched` and
251
+ `detail_failures`.
252
+
188
253
  ### ThaiBMA bonds — `from settfex.services.thaibma import ...`
189
254
 
190
255
  | I want… | Call |
@@ -226,9 +291,9 @@ being wrong, so none of them will announce itself.
226
291
 
227
292
  | Behaviour | What you must do |
228
293
  |---|---|
229
- | **Analyst consensus answers an uncovered symbol with HTTP 500, not 404** — and "uncovered" includes perfectly valid SET stocks (`ABICO`), DRs (`GOOG80`) and warrants (`JAS-W4`) | catch `FetchError`; report "no analyst coverage". **Do not** retry it as a typo or suggest a different symbol |
294
+ | **Analyst consensus answers an uncovered symbol with HTTP 500, not 404** — and "uncovered" includes perfectly valid SET stocks (`ABICO`), DRs (`GOOG80`) and warrants (`JAS-W4`) | catch **`NotFoundError` first**: since 0.25.0 a symbol that is not listed on SET raises `SymbolNotFoundError` (with `.suggestion`). What still arrives as a plain `FetchError(500)` is a listed symbol with no coverage — report "no analyst coverage", and **do not** retry it as a typo |
230
295
  | **The DR-profile endpoint 404s for every non-DR symbol**, including `CPALL` | a 404 here means "not a DR", not "unknown symbol" |
231
- | **The consensus summary endpoint fails silently**: an unknown symbol is HTTP 200 with `overall: []` | check `.count`, or use `.get(symbol)` which returns `None` |
296
+ | **The consensus summary cannot tell "unknown" from "uncovered" by itself**: both are HTTP 200 with `overall: []`. Since 0.26.0 an unlisted symbol raises `SymbolNotFoundError` | catch `NotFoundError` (do not retry); `count == 0` now means a listed symbol with no analyst coverage |
232
297
  | **The holiday endpoint returns HTTP 401 for any year but the current one** — and transiently on valid requests too | do not treat 401 as auth failure; there is no auth |
233
298
 
234
299
  ### Values that are placeholders, not data
@@ -273,10 +338,14 @@ Everything is importable from one place:
273
338
 
274
339
  ```python
275
340
  from settfex.exceptions import (
341
+ # --- the cross-cutting catch: a name that does not exist (0.25.0) ---
342
+ NotFoundError, # base of SymbolNotFoundError AND CompanyNotFoundError; never retry
276
343
  # --- the FETCH family: `except FetchError` catches all of these ---
277
344
  FetchError, # HTTP/transport failure; carries .status_code and .symbol
278
345
  HTTPStatusError, # a non-2xx, with .url and .report_code as data
279
- SymbolNotFoundError, # HTTP 404 on a SET endpoint; may carry .suggestion
346
+ UnexpectedPageError, # a PAGE where a document was expected (0.26.0); carries the answer
347
+ SoftNotFoundError, # its "file not found" page — can be FALSE; NOT a NotFoundError
348
+ SymbolNotFoundError, # SET 404, or an unlisted analyst-consensus symbol; .suggestion
280
349
  StaleDataError, # ThaiBMA rolled back and you asked it to raise
281
350
  ParseError, # a response arrived intact and could not be mapped
282
351
  IncompleteListingError, # a ParseError: rows classified, then every one was lost
@@ -287,7 +356,10 @@ from settfex.exceptions import (
287
356
  InvalidLanguageError, # unrecognized lang
288
357
  InvalidDateError, # malformed date string — raised before any request
289
358
  )
290
- from settfex.utils.parsing import ResponseParseError # also a ParseError since 0.24.0
359
+ from settfex.utils.parsing import (
360
+ ResponseParseError, # also a ParseError since 0.24.0
361
+ BlockedError, # a WAF block page (0.25.0): stop, never retry; an UnexpectedPageError
362
+ )
291
363
  ```
292
364
 
293
365
  **The split is the thing to internalise: a `FetchError` may be worth retrying; a `ValueError`
@@ -295,12 +367,44 @@ never is.** An input error means the request *succeeded* and your argument was w
295
367
  loop on one spins forever. `InvalidSymbolError`, `InvalidLanguageError` and `InvalidDateError` are
296
368
  raised before any network call at all.
297
369
 
298
- ⚠️ Two "not found" taxonomies coexist today: a SET 404 is a `SymbolNotFoundError` (a `FetchError`),
299
- while the SEC equivalent is a `CompanyNotFoundError` (a `ValueError`). Catch both if you handle
300
- both hosts. Unifying them is tracked on #135.
370
+ ⚠️ Two "not found" taxonomies still coexist: a SET 404 is a `SymbolNotFoundError` (a
371
+ `FetchError`), while the SEC equivalent is a `CompanyNotFoundError` (a `ValueError`). Since 0.25.0
372
+ both are also a **`NotFoundError`** — catch that one, and catch it **before** `FetchError`, because
373
+ a `SymbolNotFoundError` is a `FetchError` too and a retry handler would otherwise retry a typo
374
+ forever:
375
+
376
+ ```python
377
+ try:
378
+ data = await get_highlight_data(symbol)
379
+ except NotFoundError:
380
+ ... # the name does not exist: fix the input, never retry
381
+ except FetchError:
382
+ ... # upstream or transient: a retry may help
383
+ ```
384
+
385
+ Neither class moved family; taking `SymbolNotFoundError` out of `FetchError` would break existing
386
+ handlers, so that is a 1.0 decision, tracked on #135.
387
+
388
+ ⚠️ A **WAF block page** raises **`BlockedError`** (0.25.0) — **stop and back off; never retry**,
389
+ because every further request deepens the block. It is a `ResponseParseError` subclass so older
390
+ handlers still catch it, which also makes it a `ValueError`: catch `BlockedError` **before**
391
+ `ValueError` if you treat `ValueError` as bad input. Only SEC's block page is recognised; a SET
392
+ block may still arrive as a plain `ResponseParseError` — if one keeps repeating from one host,
393
+ treat it the same way.
394
+
395
+ **A download that answers with a page, not a file, says which page it was (0.26.0).** All three are
396
+ `UnexpectedPageError`s and carry the answer — `.status_code`, `.headers` (credentials removed),
397
+ `.body` (the first 8 KB), `.elapsed_seconds` — so you can decide from evidence, not from message
398
+ text:
399
+
400
+ | You caught | It means | Do |
401
+ |---|---|---|
402
+ | `BlockedError` | the WAF refused you | stop; back off for a long time; never retry soon |
403
+ | `SoftNotFoundError` | the host's "file not found" page | probably gone — but it has been **wrong** (sixty in a row, each slow, for files that existed). If it was slow (`.elapsed_seconds`) or many arrive together, retry later before recording it as gone. `.matched_marker` says which rule matched |
404
+ | `UnexpectedPageError` (itself) | any other page, e.g. an upstream error page | the source is degraded; try again later |
301
405
 
302
- ⚠️ `ResponseParseError` is also what a **WAF block page** currently surfaces as — right family,
303
- wrong diagnosis. If you see it repeatedly from one host, **stop and back off**; do not retry.
406
+ `download_all` keeps the kind on each failure: `FailedDownload.error_type` names the class and
407
+ `.status_code` the status.
304
408
 
305
409
  ---
306
410
 
@@ -7,6 +7,228 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.26.0] - 2026-10-08
11
+
12
+ **SEC Form 59 arrives: directors' and executives' trades, with the report behind each one.**
13
+ - **Downloads:** a download that answers with a page instead of a file now says which page it
14
+ was and carries the answer.
15
+ - **Analyst consensus:** the deprecation announced in 0.25.0 makes its change. An unlisted symbol
16
+ on the consensus summary raises.
17
+ - **Fixed:** a Thai SEC date on a leap day is no longer lost.
18
+
19
+ Nothing is removed from the public API. One behaviour change completes its warning period (see
20
+ **Changed**).
21
+
22
+ ### Added
23
+
24
+ - **SEC Form 59 (แบบ 59): directors' and executives' trades** (`settfex.services.sec`, top-level
25
+ `get_executive_trades` / `get_executive_trade_report`). Three tiers for the listing
26
+ (`get_executive_trades`, `fetch_executive_trades`, `fetch_executive_trades_raw`) and three for the
27
+ report behind each row (`get_executive_trade_report`, `fetch_executive_trade_report`, `…_raw`).
28
+ - **Who traded vs who filed:** `reporter_name` vs `executor_name`/`executor_id`, with `is_self`.
29
+ - **Duplicates are marked, not dropped** (`duplicate_of`, `duplicate_basis`). A row is a copy
30
+ when its report's holdings match another's, or when the same trader appears under a different
31
+ reporter with the same trade (`executor_own_row`: the trader filed the trade themselves, the
32
+ spouse case; `executor`: e.g. one company reported by two executives). Rows from the same
33
+ reporter are never merged that way, so a same-reporter re-filing is counted twice (documented).
34
+ `holding_conflict` (`bool | None`) flags a trader-rule merge whose holdings disagree; it is
35
+ `None` when a report was not fetched. All 28 September 2026 pairs are pinned.
36
+ - **Revoked filings are kept and marked** (`is_revoked`), never counted; a transaction the report
37
+ marks `CANCELED` is void too. `economic_trades()` drops copies and void rows.
38
+ - **No silent truncation:** the search page shows at most 100 rows, so the service reads the
39
+ site's "display all results" page, and every response's stated count must equal the rows parsed,
40
+ or `ParseError`. The URL builder refuses to build that page without dates: undated, it is the
41
+ whole database (70 MB).
42
+ - **Fail-loud cells:** an unknown layout, a bracketed amount or an unparseable number or date
43
+ raises. Real absences (`-` price, an empty date, the 19% of history without a link) come back as
44
+ `None`.
45
+ - **`with_details=True`** adds holdings before and after, the unrounded average price, the trading
46
+ channel and broker, the counterparty and `holding_consistent`. That is one request per distinct
47
+ report, at most 100 per call. A failed report is recorded on `detail_failures`; a block page
48
+ stops everything.
49
+ - **Output:** `to_table()` renders the agreed Thai summary; `to_dataframe()` needs the
50
+ `dataframe` extra.
51
+ - Docs: `docs/settfex/services/sec/executive_trades.md`.
52
+ - **A download that answers with a page instead of a file says which page it was, and carries
53
+ the answer** (#135). Three `FetchError` subclasses, with the messages 0.25.0 raised as plain
54
+ `FetchError`s, so every existing `except` keeps catching:
55
+ - `UnexpectedPageError` — any HTML where a document was expected, including an HTML error page
56
+ under a non-200 status. The common base of the other two.
57
+ - `SoftNotFoundError` — the host's "file not found" page, with `.matched_marker`
58
+ (`"sec-thai"` / `"generic-not-found"`). **Deliberately not a `NotFoundError`**: it has been
59
+ wrong (below).
60
+ - `BlockedError` gains `UnexpectedPageError` as a second base (after its 0.25.0 ones), and the
61
+ SEC download path now recognises the block page under **any** status, not only 2xx. JSON
62
+ endpoints still report a non-2xx as `HTTPStatusError`.
63
+ Each carries `url`, `final_url`, `status_code`, `content_type`, `headers` (without
64
+ `Set-Cookie`, `Cookie`, `Authorization`, `Proxy-Authorization`), `body` — the first 8,192 bytes,
65
+ support ID screened, the block detector's own cap — with `body_truncated`, and
66
+ `elapsed_seconds`.
67
+ - `DownloadedFile.elapsed_seconds` and `FailedDownload.status_code`.
68
+ - `SOFT_404_MARKERS` and `SOFT_404_WINDOW_BYTES` in `settfex.services.sec.constants`: the
69
+ soft-404 rule, documented so a caller can reason about it.
70
+
71
+ **Why:** a downstream consumer saw four non-document HTML answers in a row (HTTP 200,
72
+ `text/html`) and could not tell a WAF block page — back off for days — from an upstream error
73
+ page — retry the next night: the body was discarded before raising. The same consumer saw sixty
74
+ consecutive soft-404s, each ~38 s instead of ~3 s, for files that all downloaded the next
75
+ evening; the rule matches the SEC's Thai text **or any "not found"** in the first 400 bytes.
76
+
77
+ ### Changed
78
+
79
+ - **`get_consensus_overall()` / `fetch_overall()` raise `SymbolNotFoundError` for a symbol that is
80
+ not listed on SET.** Settrade answers that symbol, and a listed symbol with no analyst coverage,
81
+ with the same empty summary under HTTP 200; the SET stock list tells them apart.
82
+ - 0.25.0 warned (`DeprecationWarning`, registry id `consensus-overall-unknown-symbol`) and
83
+ returned the empty summary.
84
+ - The error carries `status_code=200`, the normalized `symbol` and a `suggestion`. It is a
85
+ `NotFoundError`, so catch `NotFoundError` and do not retry.
86
+ - `count == 0` now means a listed symbol without coverage.
87
+ - The whole-market call (`symbol=None`) is unchanged.
88
+ - `FailedDownload.error_type` names the new subclass (`"SoftNotFoundError"`,
89
+ `"UnexpectedPageError"`, `"BlockedError"`) where it said `"FetchError"`. The message is
90
+ unchanged; a filter on the class-name **string** needs updating.
91
+
92
+ ### Removed
93
+
94
+ - The deprecation registry entry `consensus-overall-unknown-symbol`, its change made (above). The
95
+ registry (`settfex.deprecations.DEPRECATIONS`) is empty.
96
+ - The README's "Upgrading to 0.24" notice, kept for two minor releases as planned. The permanent
97
+ copy is the 0.24.0 **Data completeness advisory** below, still linked from the README.
98
+
99
+ ### Fixed
100
+
101
+ - **A Thai SEC date on a leap day was lost** (`settfex.services.sec.utils.parse_dmy_date`). The
102
+ Buddhist-era conversion ran *after* the date was built in the B.E. year, and 543 is 3 mod 4, so
103
+ a B.E. year is a Gregorian leap year exactly when its C.E. year is not. `29/02/2567`, the real
104
+ 2024-02-29 (a Thursday), was rejected as "day out of range" and came back `None`: a Thai SEC
105
+ listing row received or dated that day had `receive_date` / `as_of` silently empty. The
106
+ impossible `29/02/2568` was accepted in 2568 and then escaped as a bare `ValueError`. The year is
107
+ now converted first. Malformed or impossible input still returns `None`, as documented: nothing
108
+ that parsed before parses differently, except the leap days that used to be lost. The Form 59
109
+ listing alone carries 92 such rows (55 on 29/02/2567, 37 on 29/02/2559).
110
+
111
+ ### Known issues
112
+
113
+ - **The SEC's Thai listing page prints a 29 February filing as 28 February** (upstream). A Thai
114
+ B.E. year is never a Gregorian leap year, and the page appears to clamp the day. 103 Thai MD&A
115
+ rows from 2024-02-29 carry 2024-02-28, and the FILEID shows the true date. settfex parses the
116
+ page faithfully. A cross-check is proposed in #153.
117
+ - **Form 59: a trade one reporter filed twice is counted twice** by `economic_trades()`. Proposal:
118
+ #154.
119
+
120
+ ## [0.25.0] - 2026-09-23
121
+
122
+ The seven items the 0.25.0 backlog on #135 held, plus two defects the 2026-09-23 checklist audit on
123
+ #135 found on the way. Nothing is removed and no exception leaves its family; one behaviour starts
124
+ its deprecation period (see **Deprecated**).
125
+
126
+ ### Added
127
+
128
+ - **`NotFoundError`** (`settfex.exceptions`, also top-level): one catch for "that name does not
129
+ exist". `SymbolNotFoundError` (a `FetchError`) and `CompanyNotFoundError` (a `ValueError`) are
130
+ both `NotFoundError`s now, **without moving family** — every handler written before keeps
131
+ working. Catch it *before* `FetchError`: a retry handler written as `except FetchError` used to
132
+ retry a SET typo forever. Moving `SymbolNotFoundError` out of `FetchError` would break existing
133
+ handlers, so that stays a 1.0 decision (#135).
134
+ - **`BlockedError`** (`settfex.utils.parsing`): a bot-protection block page is now named for what
135
+ it is. The fetcher recognises SEC's BIG-IP ASM page (`<title>Request Rejected</title>` plus
136
+ `Your support ID is:`) on the first 2xx that carries it and raises **without retrying** —
137
+ retrying deepens a block. It carries `url`, `status_code`, `content_type`, the headers minus
138
+ `Set-Cookie`, and the page itself with its support ID screened (kept out of `str(exc)`), so the
139
+ next real block yields the byte-exact fixture the synthetic test page stands in for. A
140
+ `ResponseParseError` subclass, so every handler that caught a block page before still does —
141
+ which also makes it a `ValueError`: **catch `BlockedError` before `ValueError`**. Only the SEC
142
+ page is recognised; no SET/Incapsula block page has ever been captured. Built from the recorded
143
+ markers with **zero live requests** (a single polite request cannot reproduce a block page, and
144
+ tripping the WAF on purpose would block the IP other workloads share).
145
+ - **Batches stop at the first block.** `download_all`, ThaiBMA `fetch_curves` and ThaiBMA history
146
+ send nothing more once a request comes back blocked. Items not yet requested are recorded as
147
+ failures — `FailedDownload(error_type="BlockedError", error="not requested: …")`, or a year on
148
+ `missing_years` — so the return shape is unchanged.
149
+ - **`setup_logger(filter=None)`**: passed to both sinks as loguru's own `filter`.
150
+ `filter="settfex"` gives settfex-only sinks, which is what an application that also logs through
151
+ loguru wants (#146 follow-up). The default stays unfiltered, as documented in 0.24.2.
152
+ - **`settfex.deprecations`**: the registry every `DeprecationWarning` now goes through
153
+ (`DEPRECATIONS`, `warn_deprecated`). The L1 golden records it by content (format 2), and
154
+ `tests/test_deprecations.py` triggers every entry, bans any other `warnings.warn` in the
155
+ package, and fails once an entry's `changes_in` release arrives. The warning names the caller's
156
+ line, not settfex's.
157
+ - **`.github/workflows/live-smoke.yml`** — a weekly run of the integration probes on a Thailand
158
+ self-hosted runner, **inert**: nothing runs until a runner labelled `thailand` is registered and
159
+ the repository variable `LIVE_SMOKE_ENABLED` is `true`. Its header states the two reasons it
160
+ ships switched off (the home egress IP is shared with production capture; the repository is
161
+ public). With it, `SETTFEX_LIVE_BUDGET=N` caps request attempts in any live run
162
+ (`tests/integration/conftest.py`).
163
+
164
+ ### Changed
165
+
166
+ - **Analyst consensus tells an unknown symbol from an uncovered one.** Settrade answers both with
167
+ HTTP 500 on the table and an empty list on the summary. On a table 500 the SET stock list now
168
+ decides — the in-process one if `get_stock_list()` already ran, otherwise **one** load per event
169
+ loop (no index enrichment, no retries; a failed load answers "unknowable" and is not repeated) —
170
+ and a symbol that is **not listed** raises `SymbolNotFoundError(status_code=500)` with a real
171
+ `.suggestion`. A listed symbol with no coverage stays the plain `FetchError(500)` it always was,
172
+ and the whole-market summary is never classified. Additive under the deprecation policy's new
173
+ sentence: raising a strict subclass that keeps every documented attribute is not a change.
174
+ - **The deprecation policy says how a deprecation is declared**, and its claim that the golden
175
+ records warnings is now true. Until 0.25.0 it asserted the golden pinned `DeprecationWarning`s;
176
+ the generator recorded none, and no warning existed yet to notice.
177
+ - **README links work on PyPI.** 87 relative links (docs, examples, `AGENTS.md`, `CHANGELOG.md`)
178
+ are absolute `blob/main` / `tree/main` URLs; PyPI resolves nothing relative.
179
+ `tests/test_readme_links.py` now forbids relative links, checks every repo link's target exists,
180
+ and scopes its tag-pin rule to CHANGELOG anchors — docs links deliberately follow `main`.
181
+ - **The live holiday probe makes one attempt.** `test_live_set_holidays` used a 7-attempt ladder
182
+ meant for the transient 401, which 0.24.1 stopped retrying; it only ever fired on 403/429, the
183
+ WAF signals the politeness rule says never to retry.
184
+ - The fault-injection matrix gains a **raised transport error** (`FetchError` from a connection
185
+ reset, as the real fetcher raises once its retries are spent). Every earlier fault was a
186
+ *response*; all 33 entry points now also pass against a request that never completes.
187
+ - `AsyncDataFetcher.fetch()` documents that a non-2xx status is **returned**, not raised.
188
+
189
+ ### Deprecated
190
+
191
+ - **`get_consensus_overall()` / `fetch_overall()` for a symbol that is not listed on SET.** It
192
+ answers with an empty summary (`count == 0`) — the same answer as a listed symbol with no
193
+ coverage — and now emits a `DeprecationWarning` (registry id
194
+ `consensus-overall-unknown-symbol`). **In 0.26.0 it raises `SymbolNotFoundError`.** Wrap the
195
+ call in `except NotFoundError` to be ready. `fetch_overall_raw()` never warns.
196
+
197
+ ### Fixed
198
+
199
+ - **A WAF block page was saved to disk as the requested filing.** The download guard rejected
200
+ HTML only when the response said it was HTML, and the observed BIG-IP page carries no
201
+ `Content-Type` — so `download()` returned its 242 bytes as a `DownloadedFile` and `download_all`
202
+ wrote them under the filing's filename. Found by the 2026-09-23 checklist audit on #135; closed
203
+ by `BlockedError`.
204
+ - **A "display all results" (ViewMore) page could reduce a section's rows (U-7).** A successful page
205
+ replaced the inline rows with no comparison, so a short or placeholder-only page deleted rows
206
+ while `has_losses` stayed `False`; and a page that failed to *map* raised out of the loop, so the
207
+ whole report code landed on `failed_codes` and every sibling category lost its rows. Both now
208
+ degrade like an unusable page: inline rows kept, a `DegradedSection` recorded, the inline
209
+ page's reported count left for `completeness()`. The rule is a count, not file identity. A
210
+ single-code listing that used to raise `ParseError` here now returns with `has_losses` — the
211
+ behaviour the 0.22.x notes already described.
212
+
213
+ ### Dependencies
214
+
215
+ - **pandas 3.0.5 → 3.0.6** (dev group; `dataframe` / `examples` extras keep their `>=2.0.0` floor,
216
+ so installs were never held back). Lock-only: the pandas entry alone moved — +16 wheels are
217
+ upstream's new `cp315`/`cp315t` builds — and upstream's 8 regression fixes and 2 bug fixes touch
218
+ no API settfex calls. Merged as #145 on 2026-09-23.
219
+
220
+ ### Migration to 0.25
221
+
222
+ - **Catch `NotFoundError` before `FetchError`** wherever you retry on `FetchError`.
223
+ - **Catch `BlockedError` before `ValueError`** if you treat `ValueError` as bad input — and never
224
+ retry it.
225
+ - A test that pins `type(e) is FetchError` for an analyst-consensus 500 still passes for a listed
226
+ symbol; for an unlisted one the type is now `SymbolNotFoundError` (`isinstance(e, FetchError)`
227
+ still holds).
228
+ - If your application logs through loguru, prefer `setup_logger(filter="settfex")` — or no
229
+ `setup_logger` at all and `logger.enable("settfex")` after `import settfex`.
230
+ - Before 0.26.0: stop relying on `get_consensus_overall(unknown)` returning an empty summary.
231
+
10
232
  ## [0.24.2] - 2026-09-22
11
233
 
12
234
  ### Fixed