settfex 0.22.2__tar.gz → 0.23.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 (272) hide show
  1. {settfex-0.22.2 → settfex-0.23.0}/AGENTS.md +13 -4
  2. {settfex-0.22.2 → settfex-0.23.0}/CHANGELOG.md +80 -0
  3. {settfex-0.22.2 → settfex-0.23.0}/CLAUDE.md +5 -1
  4. {settfex-0.22.2 → settfex-0.23.0}/PKG-INFO +1 -1
  5. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/sec/financial_report.md +17 -0
  6. {settfex-0.22.2 → settfex-0.23.0}/pyproject.toml +1 -1
  7. {settfex-0.22.2 → settfex-0.23.0}/settfex/__init__.py +1 -1
  8. {settfex-0.22.2 → settfex-0.23.0}/settfex/exceptions.py +41 -1
  9. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/sec/__init__.py +2 -0
  10. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/sec/financial_report.py +220 -17
  11. {settfex-0.22.2 → settfex-0.23.0}/tests/golden/api_surface.json +127 -0
  12. settfex-0.23.0/tests/services/sec/test_code_isolation.py +281 -0
  13. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/test_financial_report_accounting.py +50 -0
  14. {settfex-0.22.2 → settfex-0.23.0}/uv.lock +1 -1
  15. {settfex-0.22.2 → settfex-0.23.0}/.editorconfig +0 -0
  16. {settfex-0.22.2 → settfex-0.23.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  17. {settfex-0.22.2 → settfex-0.23.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  18. {settfex-0.22.2 → settfex-0.23.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  19. {settfex-0.22.2 → settfex-0.23.0}/.github/dependabot.yml +0 -0
  20. {settfex-0.22.2 → settfex-0.23.0}/.github/instructions/core-architectrual-principles.instructions.md +0 -0
  21. {settfex-0.22.2 → settfex-0.23.0}/.github/instructions/documentation-standards.instructions.md +0 -0
  22. {settfex-0.22.2 → settfex-0.23.0}/.github/instructions/file-organization.instructions.md +0 -0
  23. {settfex-0.22.2 → settfex-0.23.0}/.github/instructions/git-commit.instructions.md +0 -0
  24. {settfex-0.22.2 → settfex-0.23.0}/.github/instructions/python-dependency-management.instructions.md +0 -0
  25. {settfex-0.22.2 → settfex-0.23.0}/.github/prompts/Coding.prompt.md +0 -0
  26. {settfex-0.22.2 → settfex-0.23.0}/.github/prompts/Git-Commit-Reviewer.prompt.md +0 -0
  27. {settfex-0.22.2 → settfex-0.23.0}/.github/prompts/Prompt-Engineer.prompt.md +0 -0
  28. {settfex-0.22.2 → settfex-0.23.0}/.github/prompts/Python-Architect.prompt.md +0 -0
  29. {settfex-0.22.2 → settfex-0.23.0}/.github/workflows/ci.yml +0 -0
  30. {settfex-0.22.2 → settfex-0.23.0}/.github/workflows/dependency-drift.yml +0 -0
  31. {settfex-0.22.2 → settfex-0.23.0}/.github/workflows/pre-commit.yml +0 -0
  32. {settfex-0.22.2 → settfex-0.23.0}/.github/workflows/release.yml +0 -0
  33. {settfex-0.22.2 → settfex-0.23.0}/.github/workflows/security.yml +0 -0
  34. {settfex-0.22.2 → settfex-0.23.0}/.gitignore +0 -0
  35. {settfex-0.22.2 → settfex-0.23.0}/.pre-commit-config.yaml +0 -0
  36. {settfex-0.22.2 → settfex-0.23.0}/.python-version +0 -0
  37. {settfex-0.22.2 → settfex-0.23.0}/CODE_OF_CONDUCT.md +0 -0
  38. {settfex-0.22.2 → settfex-0.23.0}/COMPREHENSIVE_AUDIT.md +0 -0
  39. {settfex-0.22.2 → settfex-0.23.0}/CONTRIBUTING.md +0 -0
  40. {settfex-0.22.2 → settfex-0.23.0}/LICENSE +0 -0
  41. {settfex-0.22.2 → settfex-0.23.0}/README.md +0 -0
  42. {settfex-0.22.2 → settfex-0.23.0}/RELEASING.md +0 -0
  43. {settfex-0.22.2 → settfex-0.23.0}/SECURITY.md +0 -0
  44. {settfex-0.22.2 → settfex-0.23.0}/docs/guide/PYTHON_LIBRARY_BEST_PRACTICES.md +0 -0
  45. {settfex-0.22.2 → settfex-0.23.0}/docs/index.md +0 -0
  46. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/API_PROTECTION_NOTE.md +0 -0
  47. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/analyst_consensus.md +0 -0
  48. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/board_of_director.md +0 -0
  49. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/chart_quotation.md +0 -0
  50. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/corporate_action.md +0 -0
  51. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/dr_indicative_price.md +0 -0
  52. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/earnings_call.md +0 -0
  53. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/financial.md +0 -0
  54. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/highlight_data.md +0 -0
  55. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/holiday.md +0 -0
  56. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/index.md +0 -0
  57. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/info.md +0 -0
  58. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/latest_historical_trading.md +0 -0
  59. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/list.md +0 -0
  60. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/news.md +0 -0
  61. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/nvdr_holder.md +0 -0
  62. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/price_performance.md +0 -0
  63. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/profile_company.md +0 -0
  64. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/profile_dr.md +0 -0
  65. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/profile_stock.md +0 -0
  66. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/shareholder.md +0 -0
  67. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/set/trading_stat.md +0 -0
  68. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/tfex/list.md +0 -0
  69. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/tfex/trading_statistics.md +0 -0
  70. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/services/thaibma/yield_curve.md +0 -0
  71. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/utils/data_fetcher.md +0 -0
  72. {settfex-0.22.2 → settfex-0.23.0}/docs/settfex/utils/session_caching.md +0 -0
  73. {settfex-0.22.2 → settfex-0.23.0}/docs/solution/FINDINGS.md +0 -0
  74. {settfex-0.22.2 → settfex-0.23.0}/docs/solution/Performance Boost Session Caching Implementation.md +0 -0
  75. {settfex-0.22.2 → settfex-0.23.0}/docs/solution/SESSION_CACHE_SUMMARY.md +0 -0
  76. {settfex-0.22.2 → settfex-0.23.0}/docs/solution/SOLUTION_100_PERCENT.md +0 -0
  77. {settfex-0.22.2 → settfex-0.23.0}/examples/README.md +0 -0
  78. {settfex-0.22.2 → settfex-0.23.0}/examples/sec/01_financial_report.ipynb +0 -0
  79. {settfex-0.22.2 → settfex-0.23.0}/examples/set/01_stock_list.ipynb +0 -0
  80. {settfex-0.22.2 → settfex-0.23.0}/examples/set/02_highlight_data.ipynb +0 -0
  81. {settfex-0.22.2 → settfex-0.23.0}/examples/set/03_stock_profile.ipynb +0 -0
  82. {settfex-0.22.2 → settfex-0.23.0}/examples/set/04_company_profile.ipynb +0 -0
  83. {settfex-0.22.2 → settfex-0.23.0}/examples/set/05_corporate_action.ipynb +0 -0
  84. {settfex-0.22.2 → settfex-0.23.0}/examples/set/06_shareholder.ipynb +0 -0
  85. {settfex-0.22.2 → settfex-0.23.0}/examples/set/07_nvdr_holder.ipynb +0 -0
  86. {settfex-0.22.2 → settfex-0.23.0}/examples/set/08_board_of_director.ipynb +0 -0
  87. {settfex-0.22.2 → settfex-0.23.0}/examples/set/09_trading_statistics.ipynb +0 -0
  88. {settfex-0.22.2 → settfex-0.23.0}/examples/set/10_price_performance.ipynb +0 -0
  89. {settfex-0.22.2 → settfex-0.23.0}/examples/set/11_financial.ipynb +0 -0
  90. {settfex-0.22.2 → settfex-0.23.0}/examples/set/12_earnings_call.ipynb +0 -0
  91. {settfex-0.22.2 → settfex-0.23.0}/examples/set/13_chart_quotation.ipynb +0 -0
  92. {settfex-0.22.2 → settfex-0.23.0}/examples/set/14_latest_historical_trading.ipynb +0 -0
  93. {settfex-0.22.2 → settfex-0.23.0}/examples/set/15_market_index.ipynb +0 -0
  94. {settfex-0.22.2 → settfex-0.23.0}/examples/set/16_news.ipynb +0 -0
  95. {settfex-0.22.2 → settfex-0.23.0}/examples/set/17_holiday.ipynb +0 -0
  96. {settfex-0.22.2 → settfex-0.23.0}/examples/set/18_dr_and_asset_type.ipynb +0 -0
  97. {settfex-0.22.2 → settfex-0.23.0}/examples/set/19_analyst_consensus.ipynb +0 -0
  98. {settfex-0.22.2 → settfex-0.23.0}/examples/set/20_stock_info.ipynb +0 -0
  99. {settfex-0.22.2 → settfex-0.23.0}/examples/set/README.md +0 -0
  100. {settfex-0.22.2 → settfex-0.23.0}/examples/set/cpall_balance_sheet.csv +0 -0
  101. {settfex-0.22.2 → settfex-0.23.0}/examples/set/market_dashboard_20251005_173221.csv +0 -0
  102. {settfex-0.22.2 → settfex-0.23.0}/examples/set/market_dashboard_20251005_210737.csv +0 -0
  103. {settfex-0.22.2 → settfex-0.23.0}/examples/set/market_dashboard_20251006_091440.csv +0 -0
  104. {settfex-0.22.2 → settfex-0.23.0}/examples/set/thailand_banking_stocks.csv +0 -0
  105. {settfex-0.22.2 → settfex-0.23.0}/examples/set/thailand_set_main_board.csv +0 -0
  106. {settfex-0.22.2 → settfex-0.23.0}/examples/set/thailand_stock_universe.csv +0 -0
  107. {settfex-0.22.2 → settfex-0.23.0}/examples/tfex/01_series_list.ipynb +0 -0
  108. {settfex-0.22.2 → settfex-0.23.0}/examples/tfex/02_trading_statistics.ipynb +0 -0
  109. {settfex-0.22.2 → settfex-0.23.0}/examples/tfex/03_underlying_price.ipynb +0 -0
  110. {settfex-0.22.2 → settfex-0.23.0}/examples/thaibma/01_government_yield_curve.ipynb +0 -0
  111. {settfex-0.22.2 → settfex-0.23.0}/settfex/py.typed +0 -0
  112. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/__init__.py +0 -0
  113. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/sec/company.py +0 -0
  114. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/sec/constants.py +0 -0
  115. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/sec/download.py +0 -0
  116. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/sec/sec.py +0 -0
  117. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/sec/utils.py +0 -0
  118. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/__init__.py +0 -0
  119. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/asset_type.py +0 -0
  120. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/constants.py +0 -0
  121. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/earnings_call.py +0 -0
  122. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/holiday.py +0 -0
  123. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/index/__init__.py +0 -0
  124. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/index/chart_quotation.py +0 -0
  125. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/index/composition.py +0 -0
  126. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/index/index.py +0 -0
  127. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/index/info.py +0 -0
  128. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/index/list.py +0 -0
  129. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/index/utils.py +0 -0
  130. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/list.py +0 -0
  131. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/news.py +0 -0
  132. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/__init__.py +0 -0
  133. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/analyst_consensus.py +0 -0
  134. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/board_of_director.py +0 -0
  135. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/chart_quotation.py +0 -0
  136. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/corporate_action.py +0 -0
  137. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/dr_indicative_price.py +0 -0
  138. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/financial/__init__.py +0 -0
  139. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/financial/financial.py +0 -0
  140. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/highlight_data.py +0 -0
  141. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/info.py +0 -0
  142. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/latest_historical_trading.py +0 -0
  143. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/nvdr_holder.py +0 -0
  144. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/price_performance.py +0 -0
  145. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/profile_company.py +0 -0
  146. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/profile_dr.py +0 -0
  147. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/profile_stock.py +0 -0
  148. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/shareholder.py +0 -0
  149. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/stock.py +0 -0
  150. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/trading_stat.py +0 -0
  151. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/set/stock/utils.py +0 -0
  152. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/tfex/__init__.py +0 -0
  153. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/tfex/constants.py +0 -0
  154. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/tfex/list.py +0 -0
  155. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/tfex/trading_statistics.py +0 -0
  156. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/tfex/underlying_price.py +0 -0
  157. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/thaibma/__init__.py +0 -0
  158. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/thaibma/availability.py +0 -0
  159. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/thaibma/constants.py +0 -0
  160. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/thaibma/history.py +0 -0
  161. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/thaibma/thaibma.py +0 -0
  162. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/thaibma/utils.py +0 -0
  163. {settfex-0.22.2 → settfex-0.23.0}/settfex/services/thaibma/yield_curve.py +0 -0
  164. {settfex-0.22.2 → settfex-0.23.0}/settfex/utils/__init__.py +0 -0
  165. {settfex-0.22.2 → settfex-0.23.0}/settfex/utils/data_fetcher.py +0 -0
  166. {settfex-0.22.2 → settfex-0.23.0}/settfex/utils/http.py +0 -0
  167. {settfex-0.22.2 → settfex-0.23.0}/settfex/utils/logging.py +0 -0
  168. {settfex-0.22.2 → settfex-0.23.0}/settfex/utils/parsing.py +0 -0
  169. {settfex-0.22.2 → settfex-0.23.0}/settfex/utils/session_cache.py +0 -0
  170. {settfex-0.22.2 → settfex-0.23.0}/settfex/utils/session_manager.py +0 -0
  171. {settfex-0.22.2 → settfex-0.23.0}/settfex/utils/youtube_transcript.py +0 -0
  172. {settfex-0.22.2 → settfex-0.23.0}/tests/__init__.py +0 -0
  173. {settfex-0.22.2 → settfex-0.23.0}/tests/conftest.py +0 -0
  174. {settfex-0.22.2 → settfex-0.23.0}/tests/golden/model_contract/analyst_consensus.json +0 -0
  175. {settfex-0.22.2 → settfex-0.23.0}/tests/golden/model_contract/dr_profile.json +0 -0
  176. {settfex-0.22.2 → settfex-0.23.0}/tests/golden/model_contract/earnings_call_item.json +0 -0
  177. {settfex-0.22.2 → settfex-0.23.0}/tests/golden/model_contract/highlight_data.json +0 -0
  178. {settfex-0.22.2 → settfex-0.23.0}/tests/golden/model_contract/holiday_calendar.json +0 -0
  179. {settfex-0.22.2 → settfex-0.23.0}/tests/golden/model_contract/index_list_response.json +0 -0
  180. {settfex-0.22.2 → settfex-0.23.0}/tests/golden/model_contract/news_search_response.json +0 -0
  181. {settfex-0.22.2 → settfex-0.23.0}/tests/golden/model_contract/sec_company_match.json +0 -0
  182. {settfex-0.22.2 → settfex-0.23.0}/tests/golden/model_contract/stock_info.json +0 -0
  183. {settfex-0.22.2 → settfex-0.23.0}/tests/golden/model_contract/stock_list_response.json +0 -0
  184. {settfex-0.22.2 → settfex-0.23.0}/tests/golden/model_contract/tfex_series_list_response.json +0 -0
  185. {settfex-0.22.2 → settfex-0.23.0}/tests/golden/model_contract/tfex_trading_statistics.json +0 -0
  186. {settfex-0.22.2 → settfex-0.23.0}/tests/golden/model_contract/tfex_underlying_price.json +0 -0
  187. {settfex-0.22.2 → settfex-0.23.0}/tests/golden/model_contract/yield_curve.json +0 -0
  188. {settfex-0.22.2 → settfex-0.23.0}/tests/integration/__init__.py +0 -0
  189. {settfex-0.22.2 → settfex-0.23.0}/tests/integration/test_live_endpoints.py +0 -0
  190. {settfex-0.22.2 → settfex-0.23.0}/tests/services/__init__.py +0 -0
  191. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/__init__.py +0 -0
  192. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/derive_th_fixtures.py +0 -0
  193. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/fixtures.py +0 -0
  194. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/fixtures_sec/README.md +0 -0
  195. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/fixtures_sec/capital_indirection_en.html +0 -0
  196. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/fixtures_sec/en_mother_fs_h1_2025.html +0 -0
  197. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/fixtures_sec/en_ptt_56_1.html +0 -0
  198. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/fixtures_sec/en_ptt_56_2.html +0 -0
  199. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/fixtures_sec/en_ptt_fs_h1_2025.html +0 -0
  200. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/fixtures_sec/en_ptt_viewmore_56_2.html +0 -0
  201. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/fixtures_sec/idisc_505_error_page.html +0 -0
  202. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/fixtures_sec/th_cpall_viewmore_56_1.html +0 -0
  203. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/fixtures_sec/th_mother_fs_h1_2025.html +0 -0
  204. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/fixtures_sec/th_ptt_56_1.html +0 -0
  205. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/fixtures_sec/th_ptt_56_2.html +0 -0
  206. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/fixtures_sec/th_ptt_fs_h1_2025.html +0 -0
  207. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/test_company.py +0 -0
  208. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/test_download.py +0 -0
  209. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/test_download_indirection.py +0 -0
  210. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/test_download_result.py +0 -0
  211. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/test_financial_report.py +0 -0
  212. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/test_financial_report_th.py +0 -0
  213. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/test_listing_transport.py +0 -0
  214. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/test_sec.py +0 -0
  215. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/test_utils.py +0 -0
  216. {settfex-0.22.2 → settfex-0.23.0}/tests/services/sec/test_view_more_slugs.py +0 -0
  217. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/__init__.py +0 -0
  218. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/index/__init__.py +0 -0
  219. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/index/test_chart_quotation.py +0 -0
  220. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/index/test_composition.py +0 -0
  221. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/index/test_index.py +0 -0
  222. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/index/test_info.py +0 -0
  223. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/index/test_list.py +0 -0
  224. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/stock/__init__.py +0 -0
  225. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/stock/financial/__init__.py +0 -0
  226. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/stock/financial/test_financial.py +0 -0
  227. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/stock/test_analyst_consensus.py +0 -0
  228. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/stock/test_chart_quotation.py +0 -0
  229. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/stock/test_dr_indicative_price.py +0 -0
  230. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/stock/test_info.py +0 -0
  231. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/stock/test_latest_historical_trading.py +0 -0
  232. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/stock/test_profile_dr.py +0 -0
  233. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/test_asset_type.py +0 -0
  234. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/test_board_of_director.py +0 -0
  235. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/test_client.py +0 -0
  236. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/test_corporate_action.py +0 -0
  237. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/test_earnings_call.py +0 -0
  238. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/test_historical.py +0 -0
  239. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/test_holiday.py +0 -0
  240. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/test_list.py +0 -0
  241. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/test_news.py +0 -0
  242. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/test_profile_company.py +0 -0
  243. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/test_realtime.py +0 -0
  244. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/test_shareholder.py +0 -0
  245. {settfex-0.22.2 → settfex-0.23.0}/tests/services/set/test_symbol_suggestion.py +0 -0
  246. {settfex-0.22.2 → settfex-0.23.0}/tests/services/tfex/__init__.py +0 -0
  247. {settfex-0.22.2 → settfex-0.23.0}/tests/services/tfex/test_client.py +0 -0
  248. {settfex-0.22.2 → settfex-0.23.0}/tests/services/tfex/test_historical.py +0 -0
  249. {settfex-0.22.2 → settfex-0.23.0}/tests/services/tfex/test_list.py +0 -0
  250. {settfex-0.22.2 → settfex-0.23.0}/tests/services/tfex/test_realtime.py +0 -0
  251. {settfex-0.22.2 → settfex-0.23.0}/tests/services/tfex/test_trading_statistics.py +0 -0
  252. {settfex-0.22.2 → settfex-0.23.0}/tests/services/tfex/test_underlying_price.py +0 -0
  253. {settfex-0.22.2 → settfex-0.23.0}/tests/services/thaibma/__init__.py +0 -0
  254. {settfex-0.22.2 → settfex-0.23.0}/tests/services/thaibma/fixtures.py +0 -0
  255. {settfex-0.22.2 → settfex-0.23.0}/tests/services/thaibma/test_availability.py +0 -0
  256. {settfex-0.22.2 → settfex-0.23.0}/tests/services/thaibma/test_history.py +0 -0
  257. {settfex-0.22.2 → settfex-0.23.0}/tests/services/thaibma/test_thaibma.py +0 -0
  258. {settfex-0.22.2 → settfex-0.23.0}/tests/services/thaibma/test_utils.py +0 -0
  259. {settfex-0.22.2 → settfex-0.23.0}/tests/services/thaibma/test_yield_curve.py +0 -0
  260. {settfex-0.22.2 → settfex-0.23.0}/tests/test_impersonate_target.py +0 -0
  261. {settfex-0.22.2 → settfex-0.23.0}/tests/test_model_contract.py +0 -0
  262. {settfex-0.22.2 → settfex-0.23.0}/tests/test_public_api_surface.py +0 -0
  263. {settfex-0.22.2 → settfex-0.23.0}/tests/utils/__init__.py +0 -0
  264. {settfex-0.22.2 → settfex-0.23.0}/tests/utils/test_data_fetcher.py +0 -0
  265. {settfex-0.22.2 → settfex-0.23.0}/tests/utils/test_formatting.py +0 -0
  266. {settfex-0.22.2 → settfex-0.23.0}/tests/utils/test_http.py +0 -0
  267. {settfex-0.22.2 → settfex-0.23.0}/tests/utils/test_logging.py +0 -0
  268. {settfex-0.22.2 → settfex-0.23.0}/tests/utils/test_parsing.py +0 -0
  269. {settfex-0.22.2 → settfex-0.23.0}/tests/utils/test_session_cache.py +0 -0
  270. {settfex-0.22.2 → settfex-0.23.0}/tests/utils/test_session_manager.py +0 -0
  271. {settfex-0.22.2 → settfex-0.23.0}/tests/utils/test_validation.py +0 -0
  272. {settfex-0.22.2 → settfex-0.23.0}/tests/utils/test_youtube_transcript.py +0 -0
@@ -130,10 +130,19 @@ listing where **every** usable row was lost raises `IncompleteListingError` rath
130
130
  transport: a failed request raises `FetchError`, and a body that is not a listing page raises
131
131
  `ParseError`, instead of parsing to zero rows and looking like "this issuer filed nothing".
132
132
 
133
- One thing deliberately does **not** raise: a broken "display all results" page. That section keeps
134
- its truncated inline rows and the shortfall shows in `completeness()` — so check that before
135
- treating a section as complete. As of 2026-09-20 SEC's Thai `fs-kf` page answers HTTP 500, which is
136
- an upstream bug, not a settfex one.
133
+ Two things deliberately do **not** raise. A broken "display all results" page: that section keeps
134
+ its truncated inline rows and the shortfall shows in `completeness()`. And **one failing report
135
+ code**: the others keep their documents, and the failure is recorded on
136
+ `docs.accounting.failed_codes` (with a WARNING). So after a call that returned normally, check
137
+ `docs.accounting.has_losses` before treating the result as complete — an exception is no longer how
138
+ a partial failure announces itself. `repr(docs)` states it too.
139
+
140
+ As of 2026-09-20 SEC's Thai `fs-kf` page answers HTTP 500, which is an upstream bug, not a settfex
141
+ one; it shows up as a `key_financial_ratio` shortfall on Thai listings.
142
+
143
+ ⚠️ Read `.accounting` / `.reported_counts` off the object `get_sec_documents` returned: like
144
+ `DownloadResult`, `SecDocumentList` is a `list` subclass, so slicing, `sorted()`, `list()` and
145
+ comprehensions return a plain `list` and silently drop them.
137
146
 
138
147
  `download_sec_documents(...)` returns a `DownloadResult` — a list of the files that downloaded,
139
148
  which also carries `.failed` (each with its target and reason) and `.is_complete`. Check it before
@@ -7,6 +7,86 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.23.0] - 2026-09-20
11
+
12
+ ### Fixed
13
+
14
+ - **One failing report code discarded the successfully parsed documents of every other code**
15
+ (issue #132). `fetch_documents` defaults to all five categories, which collapse to three
16
+ `ddlReportType` codes run through `asyncio.gather` **without** `return_exceptions=True` — so the
17
+ first exception propagated before the line that collects the results was ever reached, and a
18
+ total loss in the `R562` search threw away the `FS` documents that had parsed perfectly well in
19
+ the same call.
20
+
21
+ The irony is worth stating, because it is the whole argument: 0.22.0 added
22
+ `IncompleteListingError` so a caller could tell loss from emptiness, and this `gather` discarded
23
+ the evidence of what *did* parse at the exact moment of raising it.
24
+
25
+ The rule is the one this module already uses twice — **a partial loss reports, a total loss
26
+ raises**: one code failing keeps its siblings' documents, records a `CodeFailure` and warns;
27
+ every code failing re-raises the first cause with its type and traceback intact. Anything that is
28
+ **not** a `FetchError` propagates untouched — a bug in settfex must not be laundered into an
29
+ accounting row, where it would be buried in a field most callers never read.
30
+
31
+ Deliberately **not** an `ExceptionGroup`, despite requiring Python 3.11+: `except FetchError`
32
+ does not catch one, so it would silently break every existing handler. When every code fails the
33
+ other causes ride along as **PEP 678 notes** on the raised one, so "the caller can always tell
34
+ which code failed and why" holds even in the total-failure case — with no new type, no changed
35
+ signature, and the traceback intact. "First" means first in **report-code order**, not first to
36
+ fail in time: `gather` returns positionally, so two identical runs raise the same cause.
37
+
38
+ - **The same gather problem one level down**, on the "display all results" pages. 0.22.1 made a
39
+ failing ViewMore *response* degrade; a transport exception in that gather still killed the whole
40
+ call. It now degrades the same way: keep the section's truncated inline rows, warn, and let
41
+ `completeness()` show the shortfall.
42
+
43
+ - **`IncompleteListingError` could not carry `unknown_sections`.** The field is inherited from
44
+ `ParseError`, but the subclass's `__init__` never accepted it, so it was **always `[]`** — a
45
+ field that looked answered and was not. A page can lose every row to a missing download link
46
+ *and* carry a heading nobody recognises; that combination is now visible.
47
+
48
+ ### Added
49
+
50
+ - **`HTTPStatusError(FetchError)`** with `url` and `report_code`. `FetchError.status_code` already
51
+ distinguished a transport failure from an HTTP one; what was missing was somewhere for the two
52
+ facts a caller otherwise had to scrape out of the message string. An archive recording "the R562
53
+ search failed" should not have to parse prose to do it.
54
+
55
+ - **`ListingAccounting.failed_codes`** — a list of `CodeFailure` (`code`, `categories`, `error`,
56
+ `error_type`, `status_code`, `url`), deliberately the same shape as `FailedDownload` from #128:
57
+ one pattern for "what did not make it", whether it was a file or a whole search. `has_losses` is
58
+ true when it is non-empty, because a search that never ran is the largest loss there is.
59
+
60
+ - **`RowTally.unlinked_hrefs`** — a capped sample of the hrefs behind `no_link`, the smaller half of
61
+ issue #133. Every unknown download shape so far (`fsdl`, `viewdoc`, `capfin`) was found by a human
62
+ looking at a dropped row's URL, and each time that meant re-parsing the page by hand. The sample
63
+ is **diversity-first**: one href per distinct `(host, path)` before the cap of 20 is spent, so
64
+ twenty instances of an already-known shape can never crowd out the single instance of a new one —
65
+ which is precisely the row worth seeing. `no_link` remains the true total.
66
+
67
+ - **`SecDocumentList.__repr__`**, and a failed-codes block in `summary()`. A summary that lists only
68
+ what arrived reads as success when an entire search is missing.
69
+
70
+ ### Changed
71
+
72
+ - ⚠️ **Callers that relied on an exception to detect a partial failure must now check
73
+ `has_losses` / `accounting.failed_codes`.** Before 0.23.0, one report code failing raised and took
74
+ everything with it; now the call returns the documents that parsed. That is the point of the fix —
75
+ but a `try/except FetchError` that treated "no exception" as "complete" will now see a partial
76
+ result as a whole one. Every failed code is also logged at WARNING, which is the signal that
77
+ cannot be lost.
78
+
79
+ ### Documentation
80
+
81
+ - **Issue #134's scope now covers `SecDocumentList`, not only `DownloadResult`** — verified: 6 of 8
82
+ ordinary list operations (slicing, `sorted()`, `list()`, concatenation, comprehensions) return a
83
+ plain `list` and drop `.accounting` and `.reported_counts`; `copy.copy()` and `filter()` are the
84
+ exceptions, and `filter()` only because 0.22.0 wired it through deliberately. This matters more
85
+ now that `failed_codes` rides on that same attribute, which is why each failed code is *also*
86
+ logged. Interim warning on the class and in the docs; the redesign is breaking and covers both
87
+ types as one decision.
88
+
89
+
10
90
  ## [0.22.2] - 2026-09-20
11
91
 
12
92
  ### Fixed
@@ -173,7 +173,7 @@ Host is **`market.sec.or.th`** (the Thai SEC IDISC system), NOT set.or.th — a
173
173
 
174
174
  | # | Service | Module | Endpoint Pattern | Key Data |
175
175
  |---|---|---|---|---|
176
- | 1 | SEC Documents | `sec/{company,financial_report,download,sec}.py` | `POST /public/idisc/api/company/valuebyuniqueId`; `GET`/`POST /public/idisc/{lang}/FinancialReport/{FS\|R561\|R562\|KFR}`; `GET /public/idisc/{lang}/ViewMore/{slug}`; `GET /public/idisc/Download?FILEID=`; `GET /ipos/Common/IPOSGetFile.aspx?id=`; `GET /public/idisc/Views/FinancialStatementDownload?query=`; `GET /public/idisc/views/viewdoc?TransId=` | List + download **raw disclosure documents** for any issuer across 5 categories (`DocumentCategory`: financial_statement/form_56_1/form_56_2/key_financial_ratio/mda). Company resolver (`resolve_company` → 10-digit uniqueIDReference); listing replays the ASP.NET WebForms search (GET `__VIEWSTATE` → form POST → stdlib HTML-table parse), follows ViewMore for complete large sections; downloads return raw bytes (`DownloadedFile.save()`), concurrent `download_all`, soft-404 detection (dead links = HTML "file not found" under HTTP 200 → `FetchError`). Listing returns a **`SecDocumentList`** (a `list[SecDocument]` subclass, backward compatible) with `years_by_category()`/`available_years()`/`filter(category=,year=)`/`categories()`/`summary()` helpers — pass a **wide** date window to see full year history. `SecCompany("CPALL")` facade; `get_sec_documents()`/`download_sec_document(s)()`. dd/mm/yyyy dates. Stateless host (no SessionManager). **Bilingual** — `lang="th"` returns the Thai-language filing documents (different files from the English ones), with Buddhist-era years/dates normalized to C.E. in the model; free-text cells stay in the page's language. `SecDocumentList.reported_counts`/`completeness()` carry what the site said each section holds, and an unclassifiable page raises `ParseError` instead of returning `[]`. **Row/column accounting** (`docs.accounting`: `no_link`/`placeholders`/`navigation`/`unmapped_headers`/`unverifiable_sections`/`by_category`, computed totals) makes every drop visible in the return value; a **total** loss raises `IncompleteListingError`, a partial one warns. `download_all` returns a `DownloadResult` (a `list[DownloadedFile]` subclass) carrying `.failed`. |
176
+ | 1 | SEC Documents | `sec/{company,financial_report,download,sec}.py` | `POST /public/idisc/api/company/valuebyuniqueId`; `GET`/`POST /public/idisc/{lang}/FinancialReport/{FS\|R561\|R562\|KFR}`; `GET /public/idisc/{lang}/ViewMore/{slug}`; `GET /public/idisc/Download?FILEID=`; `GET /ipos/Common/IPOSGetFile.aspx?id=`; `GET /public/idisc/Views/FinancialStatementDownload?query=`; `GET /public/idisc/views/viewdoc?TransId=` | List + download **raw disclosure documents** for any issuer across 5 categories (`DocumentCategory`: financial_statement/form_56_1/form_56_2/key_financial_ratio/mda). Company resolver (`resolve_company` → 10-digit uniqueIDReference); listing replays the ASP.NET WebForms search (GET `__VIEWSTATE` → form POST → stdlib HTML-table parse), follows ViewMore for complete large sections; downloads return raw bytes (`DownloadedFile.save()`), concurrent `download_all`, soft-404 detection (dead links = HTML "file not found" under HTTP 200 → `FetchError`). Listing returns a **`SecDocumentList`** (a `list[SecDocument]` subclass, backward compatible) with `years_by_category()`/`available_years()`/`filter(category=,year=)`/`categories()`/`summary()` helpers — pass a **wide** date window to see full year history. `SecCompany("CPALL")` facade; `get_sec_documents()`/`download_sec_document(s)()`. dd/mm/yyyy dates. Stateless host (no SessionManager). **Bilingual** — `lang="th"` returns the Thai-language filing documents (different files from the English ones), with Buddhist-era years/dates normalized to C.E. in the model; free-text cells stay in the page's language. `SecDocumentList.reported_counts`/`completeness()` carry what the site said each section holds, and an unclassifiable page raises `ParseError` instead of returning `[]`. **Row/column accounting** (`docs.accounting`: `no_link`/`placeholders`/`navigation`/`unmapped_headers`/`unverifiable_sections`/`by_category`, computed totals) makes every drop visible in the return value; a **total** loss raises `IncompleteListingError`, a partial one warns. `download_all` returns a `DownloadResult` (a `list[DownloadedFile]` subclass) carrying `.failed`. **Transport guards** on every listing leg (non-2xx → `HTTPStatusError`, non-listing body → `ParseError`); **per-code isolation** so one failing `ddlReportType` keeps its siblings' documents and records a `CodeFailure` on `accounting.failed_codes`. |
177
177
 
178
178
  ### ThaiBMA Services (1)
179
179
 
@@ -418,6 +418,10 @@ See [`CHANGELOG.md`](CHANGELOG.md) for the full, versioned release history — t
418
418
  - **SEC's old-format filings are on a FIFTH host and behind a JavaScript indirection (issue #133):** `capital.sec.or.th/…/get_zip_all_public_page.php` carries pre-2014 `.DOC`/`.XLS` filings. The URL answers **HTTP 200 with a 2 KB HTML page whose entire body is `document.location = '/tmp/<id>.zip'`** — so recognising the link is necessary but not sufficient, and `download()` resolves the redirect at download time (one extra request). Three properties the code depends on: the target is **minted per request** (`XP`/`sD`/`V1` for the same filing across three captures), so `file_url` stays the stable indirection URL and the `/tmp/…` one is never stored; it is **per-language** (the Thai request mints a different archive, so `lang` is part of the `capfin:<comp_id>-<year>-<period>-<lang>` id); and `comp_id` is a **different id space from `set_id`** in the same query — they are not interchangeable. The redirect pattern accepts an absolute URL on purpose, so the same-host check is live rather than dead code.
419
419
  - **These old-format zips are MEMBER-STABLE, not byte-stable — do not key an archive on the container hash:** re-minting the same filing produced a different container sha256 while every member payload stayed byte-identical. Exactly **12 bytes of 334,498** differ, in four 3-byte runs, one per member: the Info-ZIP extended-timestamp (`UT`) extra field, i.e. the pack time. **Judge integrity per member.** A consumer that hashes the container will see a legitimate re-fetch as a new — or corrupted — object. (Also observed: one sample's embedded pack time *predated* the request that minted it, suggesting the server sometimes hands back a previously generated archive. INFERRED from two samples.)
420
420
  - **`DownloadResult` loses `.failed` the moment you derive a new list (issue #134, documented not fixed):** `.failed`, `.requested` and `.is_complete` are instance attributes on a `list` subclass, so slicing, `sorted()`, `list()`, concatenation and comprehensions all return a plain `list` without them — 6 of 7 ordinary operations, `copy.copy()` being the exception. `for f in sorted(result)` therefore quietly gives up the failure report #128 added. **Read `.failed` from the object `download_all` returned, before deriving anything from it.** `SecDocumentList` has the identical hazard for `.accounting`/`.reported_counts`. Removing the edge is a breaking change and is tracked for a later release.
421
+ - **One failing report code no longer discards the others — and `has_losses` is now the only reliable "was it complete?" check:** `fetch_documents` collapses the requested categories to up to three `ddlReportType` codes run concurrently; the gather had no `return_exceptions=True`, so the first exception propagated before the collecting line ran and a total loss in `R562` threw away the `FS` documents that had parsed fine (issue #132). Now: **partial loss reports, total loss raises** — one code failing keeps its siblings' documents and records a `CodeFailure` on `accounting.failed_codes`; every code failing re-raises the first cause with its type and traceback intact. ⚠️ **Anything that is not a `FetchError` propagates untouched** — a settfex bug must not be laundered into an accounting row, where it would be buried in a field most callers never read. ⚠️ **Deliberately NOT an `ExceptionGroup`** despite the 3.11+ floor: `except FetchError` does not catch one, so it would silently break every existing handler. When every code fails, the other causes ride along as **PEP 678 notes** (`exc.add_note`) on the raised one — otherwise re-raising a single exception would quietly drop them, breaking the rule that a caller can always tell which code failed and why. **"First" means first in report-code order, not first to fail in time** (`gather` returns positionally), so the raised cause is deterministic across runs; a test pins that by making the last code fail instantly while the first yields 20 times. ⚠️ **Upgrade trap:** a `try/except FetchError` that treated "no exception" as "complete" now sees a partial result as a whole one — check `has_losses` / `failed_codes`. The same `return_exceptions` treatment was applied to the ViewMore gather, so a transport error there costs one section's completeness rather than the call.
422
+ - **`unlinked_hrefs` is sampled for DIVERSITY, not "the first 20" — and that is the whole point:** `RowTally.unlinked_hrefs` keeps a capped sample of the hrefs behind `no_link`, because every unknown download shape so far (`fsdl`, `viewdoc`, `capfin`) was found by a human looking at a dropped row's URL, and each time that meant re-parsing the page by hand. The sampler takes **one href per distinct `(host, path)` first**, then fills to the cap of 20 — so thirty instances of an already-known shape can never crowd out the single instance of a new one, which is exactly the row worth seeing. `no_link` stays the true total, and `plus()` **re-samples** on merge rather than concatenating, or a merge could exceed the cap or lose the property the sample exists for.
423
+ - **`IncompleteListingError` could not carry `unknown_sections` until 0.23.0 — a field that looked answered and was not:** it is inherited from `ParseError`, but the subclass's `__init__` never accepted it, so it was **always `[]`**. Anyone who inspected it concluded "no unrecognised headings" from a value that had never been populated. Fixed by accepting and forwarding it; a page can lose every row to a missing link *and* carry a heading nobody recognises.
424
+ - **`SecDocumentList` has `DownloadResult`'s side-channel problem too (issue #134, documented not fixed):** 6 of 8 ordinary list operations — slicing, `sorted()`, `list()`, `+`, comprehensions — return a plain `list` and drop `.accounting` and `.reported_counts`. `copy.copy()` and `filter()` survive, and `filter()` only because 0.22.0 wired it through on purpose. **This got sharper in 0.23.0**, because `accounting.failed_codes` records which report code failed, so `sorted(docs, …)` would discard that too — which is why every failed code is **also logged at WARNING**: a log line cannot be sliced away. Read the accounting off the object `fetch_documents` returned. The redesign is breaking and covers both types as one decision.
421
425
  - **`no_href` was three different things, and only one is a defect:** a dropped SEC listing row is a `ไม่พบข้อมูล`/`Data not found` **placeholder** (the site saying the section is empty), a "display all results" **ViewMore navigation row** (how a long section truncates — 24% of live sections), or a **real data row whose download link is gone** (`no_link`). They shared one counter, which is why P1 could not be escalated: any rule strict enough to catch a genuine loss also fired on every truncated section. Split (`ListingAccounting.placeholders`/`navigation`/`no_link`), `no_link` is **0 on every real page captured** and non-zero only on a page derived to exhibit the bug — so it can be escalated without a record-count marker at all. `_MapResult.no_href` is kept as the sum of the three; **branch on `accounting.no_link`, never on `no_href`** (it is non-zero on perfectly healthy pages).
422
426
  - **Two cross-checks on a SEC listing, and they answer different questions:** `completeness()`/`reported_counts` compares what you hold against **the number the site printed** — a shortfall there is usually legitimate "view more" truncation, so it is reported and never raised. `docs.accounting` is **ours**: what the parser did with each row it received. A shortfall is a maybe; `accounting.no_link > 0` is always a loss. Do not fold one into the other.
423
427
  - **`IncompleteListingError` fires only on a TOTAL loss:** rows classified fine and *every* usable one was dropped for want of a download link. A **partial** loss warns and lands on `accounting.no_link` instead — raising there would turn one bad row in a 200-row listing into no listing at all, and unlike a total loss a partial one is visible in the return value. Same severity ladder as the 0.21.0 `ParseError` for unrecognised headings, and it is a `ParseError` subclass so `except ParseError`/`except FetchError` keep working. The escalation is scoped to the **requested** categories (a single `FS` search returns three sections) — the same principle PR #125 applied to `completeness()`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: settfex
3
- Version: 0.22.2
3
+ Version: 0.23.0
4
4
  Summary: Async Python library for fetching real-time and historical data from the Stock Exchange of Thailand (SET) and Thailand Futures Exchange (TFEX)
5
5
  Project-URL: Homepage, https://github.com/lumduan/settfex
6
6
  Project-URL: Documentation, https://github.com/lumduan/settfex#readme
@@ -134,8 +134,22 @@ docs.accounting.navigation # "display all results" rows — how a long sec
134
134
  docs.accounting.unmapped_headers # columns the site serves that map to no field
135
135
  docs.accounting.has_losses # the one flag worth branching on
136
136
  docs.accounting.by_category # the same tally per DocumentCategory
137
+ docs.accounting.failed_codes # report-code searches that failed while their siblings succeeded
138
+ docs.accounting.by_category["financial_statement"].unlinked_hrefs # a capped sample of the URLs
139
+ # behind no_link, for reporting
137
140
  ```
138
141
 
142
+ > ⚠️ **After a call that returned normally, check `has_losses` before treating the result as
143
+ > complete.** Since 0.23.0 one failing report code no longer takes the whole call down with it —
144
+ > its siblings keep their documents and the failure is recorded on `failed_codes` (and logged at
145
+ > WARNING). An exception is therefore no longer how a partial failure announces itself.
146
+ > `repr(docs)` and `summary()` both state it.
147
+
148
+ > ⚠️ **Read the accounting off the object `fetch_documents` returned.** `SecDocumentList` is a
149
+ > `list` subclass, so slicing, `sorted()`, `list()`, concatenation and comprehensions return a
150
+ > plain `list` and drop `.accounting` / `.reported_counts` — the same sharp edge `DownloadResult`
151
+ > has (issue #134 covers both). `copy.copy()` and `.filter()` are the exceptions.
152
+
139
153
  The three drop reasons used to share one counter, so the only one that is a defect looked exactly
140
154
  like the two that are the site working normally. They are separated because `no_link` is the signal
141
155
  worth escalating and the other two must never be able to trigger it.
@@ -147,6 +161,9 @@ carries data rows and **none** of them can be classified — the signature of a
147
161
  An issuer with no filings still returns an empty list and does **not** raise: the check keys on
148
162
  unrecognised section headings, not on emptiness.
149
163
 
164
+ `HTTPStatusError` (a subclass of `FetchError`) is raised for a non-2xx on any listing leg, carrying
165
+ `status_code`, `url` and `report_code` as data rather than as message text.
166
+
150
167
  `IncompleteListingError` (a subclass of `ParseError`) is its sibling: rows classified fine and
151
168
  then **every** usable one was dropped for want of a download link. Deliberately
152
169
  narrow — a *partial* loss does not raise, it warns and lands on `accounting.no_link`, because a
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "settfex"
3
- version = "0.22.2"
3
+ version = "0.23.0"
4
4
  description = "Async Python library for fetching real-time and historical data from the Stock Exchange of Thailand (SET) and Thailand Futures Exchange (TFEX)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -35,7 +35,7 @@ Usage:
35
35
  >>> asyncio.run(main())
36
36
  """
37
37
 
38
- __version__ = "0.22.2"
38
+ __version__ = "0.23.0"
39
39
  __author__ = "batt"
40
40
  __license__ = "MIT"
41
41
 
@@ -26,6 +26,7 @@ __all__ = [
26
26
  "StaleDataError",
27
27
  "ParseError",
28
28
  "IncompleteListingError",
29
+ "HTTPStatusError",
29
30
  "InvalidSymbolError",
30
31
  "InvalidLanguageError",
31
32
  "InvalidDateError",
@@ -160,6 +161,11 @@ class IncompleteListingError(ParseError):
160
161
  ``lost_rows`` is how many rows were dropped that way; ``reported`` is what the page said each
161
162
  section holds. Subclasses :class:`ParseError`, so ``except ParseError`` and
162
163
  ``except FetchError`` handlers keep working.
164
+
165
+ ``unknown_sections`` is inherited from :class:`ParseError` and, until 0.23.0, this subclass had
166
+ no way to accept it — so it was **always** ``[]`` here, a field that looked answered and was
167
+ not. A page can lose every row to a missing download link *and* carry a heading nobody
168
+ recognises, and that combination is worth seeing.
163
169
  """
164
170
 
165
171
  def __init__(
@@ -168,14 +174,48 @@ class IncompleteListingError(ParseError):
168
174
  *,
169
175
  url: str | None = None,
170
176
  rows_parsed: int | None = None,
177
+ unknown_sections: list[str] | None = None,
171
178
  lost_rows: int | None = None,
172
179
  reported: dict[str, int] | None = None,
173
180
  ) -> None:
174
- super().__init__(message, url=url, rows_parsed=rows_parsed)
181
+ super().__init__(
182
+ message, url=url, rows_parsed=rows_parsed, unknown_sections=unknown_sections
183
+ )
175
184
  self.lost_rows = lost_rows
176
185
  self.reported = dict(reported or {})
177
186
 
178
187
 
188
+ class HTTPStatusError(FetchError):
189
+ """A request returned a non-2xx status, with the context needed to act on it.
190
+
191
+ :class:`FetchError` already carries ``status_code``, so this adds no new *capability* — what it
192
+ adds is a place for the two facts a caller otherwise has to scrape out of the message string:
193
+ which URL, and which report code. An archive recording "the R562 search failed" should not have
194
+ to parse prose to do it, which is the same failure mode this family of fixes keeps closing.
195
+
196
+ Deliberately **not** raised via ``raise_for_status``: that helper maps 404 to
197
+ :class:`SymbolNotFoundError` and consults the symbol suggester, which is wrong on an endpoint
198
+ where the identifier was already resolved — a 404 there means the route or the host is wrong,
199
+ not that an issuer does not exist. (The same reasoning gives the DR-profile and
200
+ analyst-consensus endpoints their own handling; see CLAUDE.md's Known Gotchas.)
201
+
202
+ Subclasses :class:`FetchError`, so ``except FetchError`` keeps working.
203
+ """
204
+
205
+ def __init__(
206
+ self,
207
+ message: str,
208
+ *,
209
+ status_code: int | None = None,
210
+ url: str | None = None,
211
+ report_code: str | None = None,
212
+ symbol: str | None = None,
213
+ ) -> None:
214
+ super().__init__(message, status_code=status_code, symbol=symbol)
215
+ self.url = url
216
+ self.report_code = report_code
217
+
218
+
179
219
  class InvalidSymbolError(ValueError):
180
220
  """A symbol was empty or invalid."""
181
221
 
@@ -19,6 +19,7 @@ from settfex.services.sec.download import (
19
19
  )
20
20
  from settfex.services.sec.financial_report import (
21
21
  CATEGORY_TO_REPORT_TYPE,
22
+ CodeFailure,
22
23
  DocumentCategory,
23
24
  FinancialReportService,
24
25
  ListingAccounting,
@@ -33,6 +34,7 @@ from settfex.services.sec.sec import SecCompany
33
34
 
34
35
  __all__ = [
35
36
  "CATEGORY_TO_REPORT_TYPE",
37
+ "CodeFailure",
36
38
  "CompanyMatch",
37
39
  "DocumentCategory",
38
40
  "DocumentDownloadService",