settfex 0.26.0__tar.gz → 0.26.1__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 (343) hide show
  1. {settfex-0.26.0 → settfex-0.26.1}/.github/workflows/ci.yml +2 -2
  2. {settfex-0.26.0 → settfex-0.26.1}/.github/workflows/dependency-drift.yml +1 -9
  3. {settfex-0.26.0 → settfex-0.26.1}/.github/workflows/live-smoke.yml +1 -1
  4. {settfex-0.26.0 → settfex-0.26.1}/.github/workflows/pre-commit.yml +1 -1
  5. {settfex-0.26.0 → settfex-0.26.1}/.github/workflows/release.yml +2 -2
  6. {settfex-0.26.0 → settfex-0.26.1}/.github/workflows/security.yml +3 -2
  7. {settfex-0.26.0 → settfex-0.26.1}/CHANGELOG.md +26 -0
  8. {settfex-0.26.0 → settfex-0.26.1}/CLAUDE.md +1 -1
  9. {settfex-0.26.0 → settfex-0.26.1}/PKG-INFO +1 -2
  10. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/utils/session_caching.md +14 -13
  11. {settfex-0.26.0 → settfex-0.26.1}/pyproject.toml +1 -2
  12. {settfex-0.26.0 → settfex-0.26.1}/settfex/__init__.py +1 -1
  13. {settfex-0.26.0 → settfex-0.26.1}/settfex/utils/session_cache.py +136 -47
  14. settfex-0.26.1/tests/utils/test_session_cache.py +240 -0
  15. {settfex-0.26.0 → settfex-0.26.1}/uv.lock +327 -271
  16. settfex-0.26.0/tests/utils/test_session_cache.py +0 -132
  17. {settfex-0.26.0 → settfex-0.26.1}/.editorconfig +0 -0
  18. {settfex-0.26.0 → settfex-0.26.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  19. {settfex-0.26.0 → settfex-0.26.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  20. {settfex-0.26.0 → settfex-0.26.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  21. {settfex-0.26.0 → settfex-0.26.1}/.github/dependabot.yml +0 -0
  22. {settfex-0.26.0 → settfex-0.26.1}/.github/instructions/core-architectrual-principles.instructions.md +0 -0
  23. {settfex-0.26.0 → settfex-0.26.1}/.github/instructions/documentation-standards.instructions.md +0 -0
  24. {settfex-0.26.0 → settfex-0.26.1}/.github/instructions/file-organization.instructions.md +0 -0
  25. {settfex-0.26.0 → settfex-0.26.1}/.github/instructions/git-commit.instructions.md +0 -0
  26. {settfex-0.26.0 → settfex-0.26.1}/.github/instructions/python-dependency-management.instructions.md +0 -0
  27. {settfex-0.26.0 → settfex-0.26.1}/.github/prompts/Coding.prompt.md +0 -0
  28. {settfex-0.26.0 → settfex-0.26.1}/.github/prompts/Git-Commit-Reviewer.prompt.md +0 -0
  29. {settfex-0.26.0 → settfex-0.26.1}/.github/prompts/Prompt-Engineer.prompt.md +0 -0
  30. {settfex-0.26.0 → settfex-0.26.1}/.github/prompts/Python-Architect.prompt.md +0 -0
  31. {settfex-0.26.0 → settfex-0.26.1}/.gitignore +0 -0
  32. {settfex-0.26.0 → settfex-0.26.1}/.pre-commit-config.yaml +0 -0
  33. {settfex-0.26.0 → settfex-0.26.1}/.python-version +0 -0
  34. {settfex-0.26.0 → settfex-0.26.1}/AGENTS.md +0 -0
  35. {settfex-0.26.0 → settfex-0.26.1}/CODE_OF_CONDUCT.md +0 -0
  36. {settfex-0.26.0 → settfex-0.26.1}/COMPREHENSIVE_AUDIT.md +0 -0
  37. {settfex-0.26.0 → settfex-0.26.1}/CONTRIBUTING.md +0 -0
  38. {settfex-0.26.0 → settfex-0.26.1}/LICENSE +0 -0
  39. {settfex-0.26.0 → settfex-0.26.1}/README.md +0 -0
  40. {settfex-0.26.0 → settfex-0.26.1}/RELEASING.md +0 -0
  41. {settfex-0.26.0 → settfex-0.26.1}/SECURITY.md +0 -0
  42. {settfex-0.26.0 → settfex-0.26.1}/docs/guide/PYTHON_LIBRARY_BEST_PRACTICES.md +0 -0
  43. {settfex-0.26.0 → settfex-0.26.1}/docs/index.md +0 -0
  44. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/sec/executive_trades.md +0 -0
  45. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/sec/financial_report.md +0 -0
  46. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/API_PROTECTION_NOTE.md +0 -0
  47. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/analyst_consensus.md +0 -0
  48. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/board_of_director.md +0 -0
  49. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/chart_quotation.md +0 -0
  50. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/corporate_action.md +0 -0
  51. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/dr_indicative_price.md +0 -0
  52. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/earnings_call.md +0 -0
  53. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/financial.md +0 -0
  54. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/highlight_data.md +0 -0
  55. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/holiday.md +0 -0
  56. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/index.md +0 -0
  57. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/info.md +0 -0
  58. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/latest_historical_trading.md +0 -0
  59. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/list.md +0 -0
  60. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/news.md +0 -0
  61. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/nvdr_holder.md +0 -0
  62. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/price_performance.md +0 -0
  63. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/profile_company.md +0 -0
  64. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/profile_dr.md +0 -0
  65. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/profile_stock.md +0 -0
  66. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/shareholder.md +0 -0
  67. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/trading_stat.md +0 -0
  68. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/tfex/list.md +0 -0
  69. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/tfex/trading_statistics.md +0 -0
  70. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/thaibma/yield_curve.md +0 -0
  71. {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/utils/data_fetcher.md +0 -0
  72. {settfex-0.26.0 → settfex-0.26.1}/docs/solution/FINDINGS.md +0 -0
  73. {settfex-0.26.0 → settfex-0.26.1}/docs/solution/Performance Boost Session Caching Implementation.md +0 -0
  74. {settfex-0.26.0 → settfex-0.26.1}/docs/solution/SESSION_CACHE_SUMMARY.md +0 -0
  75. {settfex-0.26.0 → settfex-0.26.1}/docs/solution/SOLUTION_100_PERCENT.md +0 -0
  76. {settfex-0.26.0 → settfex-0.26.1}/examples/README.md +0 -0
  77. {settfex-0.26.0 → settfex-0.26.1}/examples/sec/01_financial_report.ipynb +0 -0
  78. {settfex-0.26.0 → settfex-0.26.1}/examples/sec/02_executive_trades.ipynb +0 -0
  79. {settfex-0.26.0 → settfex-0.26.1}/examples/set/01_stock_list.ipynb +0 -0
  80. {settfex-0.26.0 → settfex-0.26.1}/examples/set/02_highlight_data.ipynb +0 -0
  81. {settfex-0.26.0 → settfex-0.26.1}/examples/set/03_stock_profile.ipynb +0 -0
  82. {settfex-0.26.0 → settfex-0.26.1}/examples/set/04_company_profile.ipynb +0 -0
  83. {settfex-0.26.0 → settfex-0.26.1}/examples/set/05_corporate_action.ipynb +0 -0
  84. {settfex-0.26.0 → settfex-0.26.1}/examples/set/06_shareholder.ipynb +0 -0
  85. {settfex-0.26.0 → settfex-0.26.1}/examples/set/07_nvdr_holder.ipynb +0 -0
  86. {settfex-0.26.0 → settfex-0.26.1}/examples/set/08_board_of_director.ipynb +0 -0
  87. {settfex-0.26.0 → settfex-0.26.1}/examples/set/09_trading_statistics.ipynb +0 -0
  88. {settfex-0.26.0 → settfex-0.26.1}/examples/set/10_price_performance.ipynb +0 -0
  89. {settfex-0.26.0 → settfex-0.26.1}/examples/set/11_financial.ipynb +0 -0
  90. {settfex-0.26.0 → settfex-0.26.1}/examples/set/12_earnings_call.ipynb +0 -0
  91. {settfex-0.26.0 → settfex-0.26.1}/examples/set/13_chart_quotation.ipynb +0 -0
  92. {settfex-0.26.0 → settfex-0.26.1}/examples/set/14_latest_historical_trading.ipynb +0 -0
  93. {settfex-0.26.0 → settfex-0.26.1}/examples/set/15_market_index.ipynb +0 -0
  94. {settfex-0.26.0 → settfex-0.26.1}/examples/set/16_news.ipynb +0 -0
  95. {settfex-0.26.0 → settfex-0.26.1}/examples/set/17_holiday.ipynb +0 -0
  96. {settfex-0.26.0 → settfex-0.26.1}/examples/set/18_dr_and_asset_type.ipynb +0 -0
  97. {settfex-0.26.0 → settfex-0.26.1}/examples/set/19_analyst_consensus.ipynb +0 -0
  98. {settfex-0.26.0 → settfex-0.26.1}/examples/set/20_stock_info.ipynb +0 -0
  99. {settfex-0.26.0 → settfex-0.26.1}/examples/set/README.md +0 -0
  100. {settfex-0.26.0 → settfex-0.26.1}/examples/set/cpall_balance_sheet.csv +0 -0
  101. {settfex-0.26.0 → settfex-0.26.1}/examples/set/market_dashboard_20251005_173221.csv +0 -0
  102. {settfex-0.26.0 → settfex-0.26.1}/examples/set/market_dashboard_20251005_210737.csv +0 -0
  103. {settfex-0.26.0 → settfex-0.26.1}/examples/set/market_dashboard_20251006_091440.csv +0 -0
  104. {settfex-0.26.0 → settfex-0.26.1}/examples/set/thailand_banking_stocks.csv +0 -0
  105. {settfex-0.26.0 → settfex-0.26.1}/examples/set/thailand_set_main_board.csv +0 -0
  106. {settfex-0.26.0 → settfex-0.26.1}/examples/set/thailand_stock_universe.csv +0 -0
  107. {settfex-0.26.0 → settfex-0.26.1}/examples/tfex/01_series_list.ipynb +0 -0
  108. {settfex-0.26.0 → settfex-0.26.1}/examples/tfex/02_trading_statistics.ipynb +0 -0
  109. {settfex-0.26.0 → settfex-0.26.1}/examples/tfex/03_underlying_price.ipynb +0 -0
  110. {settfex-0.26.0 → settfex-0.26.1}/examples/thaibma/01_government_yield_curve.ipynb +0 -0
  111. {settfex-0.26.0 → settfex-0.26.1}/settfex/deprecations.py +0 -0
  112. {settfex-0.26.0 → settfex-0.26.1}/settfex/exceptions.py +0 -0
  113. {settfex-0.26.0 → settfex-0.26.1}/settfex/py.typed +0 -0
  114. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/__init__.py +0 -0
  115. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/sec/__init__.py +0 -0
  116. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/sec/company.py +0 -0
  117. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/sec/constants.py +0 -0
  118. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/sec/download.py +0 -0
  119. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/sec/executive_trades.py +0 -0
  120. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/sec/financial_report.py +0 -0
  121. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/sec/sec.py +0 -0
  122. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/sec/utils.py +0 -0
  123. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/__init__.py +0 -0
  124. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/asset_type.py +0 -0
  125. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/constants.py +0 -0
  126. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/earnings_call.py +0 -0
  127. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/holiday.py +0 -0
  128. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/index/__init__.py +0 -0
  129. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/index/chart_quotation.py +0 -0
  130. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/index/composition.py +0 -0
  131. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/index/index.py +0 -0
  132. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/index/info.py +0 -0
  133. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/index/list.py +0 -0
  134. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/index/utils.py +0 -0
  135. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/list.py +0 -0
  136. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/news.py +0 -0
  137. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/__init__.py +0 -0
  138. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/analyst_consensus.py +0 -0
  139. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/board_of_director.py +0 -0
  140. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/chart_quotation.py +0 -0
  141. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/corporate_action.py +0 -0
  142. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/dr_indicative_price.py +0 -0
  143. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/financial/__init__.py +0 -0
  144. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/financial/financial.py +0 -0
  145. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/highlight_data.py +0 -0
  146. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/info.py +0 -0
  147. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/latest_historical_trading.py +0 -0
  148. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/nvdr_holder.py +0 -0
  149. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/price_performance.py +0 -0
  150. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/profile_company.py +0 -0
  151. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/profile_dr.py +0 -0
  152. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/profile_stock.py +0 -0
  153. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/shareholder.py +0 -0
  154. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/stock.py +0 -0
  155. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/trading_stat.py +0 -0
  156. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/utils.py +0 -0
  157. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/tfex/__init__.py +0 -0
  158. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/tfex/constants.py +0 -0
  159. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/tfex/list.py +0 -0
  160. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/tfex/trading_statistics.py +0 -0
  161. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/tfex/underlying_price.py +0 -0
  162. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/thaibma/__init__.py +0 -0
  163. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/thaibma/availability.py +0 -0
  164. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/thaibma/constants.py +0 -0
  165. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/thaibma/history.py +0 -0
  166. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/thaibma/thaibma.py +0 -0
  167. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/thaibma/utils.py +0 -0
  168. {settfex-0.26.0 → settfex-0.26.1}/settfex/services/thaibma/yield_curve.py +0 -0
  169. {settfex-0.26.0 → settfex-0.26.1}/settfex/utils/__init__.py +0 -0
  170. {settfex-0.26.0 → settfex-0.26.1}/settfex/utils/data_fetcher.py +0 -0
  171. {settfex-0.26.0 → settfex-0.26.1}/settfex/utils/http.py +0 -0
  172. {settfex-0.26.0 → settfex-0.26.1}/settfex/utils/logging.py +0 -0
  173. {settfex-0.26.0 → settfex-0.26.1}/settfex/utils/parsing.py +0 -0
  174. {settfex-0.26.0 → settfex-0.26.1}/settfex/utils/session_manager.py +0 -0
  175. {settfex-0.26.0 → settfex-0.26.1}/settfex/utils/youtube_transcript.py +0 -0
  176. {settfex-0.26.0 → settfex-0.26.1}/tests/__init__.py +0 -0
  177. {settfex-0.26.0 → settfex-0.26.1}/tests/conftest.py +0 -0
  178. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/api_surface.json +0 -0
  179. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/analyst_consensus.json +0 -0
  180. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/dr_profile.json +0 -0
  181. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/earnings_call_item.json +0 -0
  182. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/highlight_data.json +0 -0
  183. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/holiday_calendar.json +0 -0
  184. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/index_list_response.json +0 -0
  185. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/news_search_response.json +0 -0
  186. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/sec_company_match.json +0 -0
  187. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/sec_executive_trade.json +0 -0
  188. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/sec_executive_trade_report.json +0 -0
  189. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/stock_info.json +0 -0
  190. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/stock_list_response.json +0 -0
  191. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/tfex_series_list_response.json +0 -0
  192. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/tfex_trading_statistics.json +0 -0
  193. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/tfex_underlying_price.json +0 -0
  194. {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/yield_curve.json +0 -0
  195. {settfex-0.26.0 → settfex-0.26.1}/tests/integration/__init__.py +0 -0
  196. {settfex-0.26.0 → settfex-0.26.1}/tests/integration/conftest.py +0 -0
  197. {settfex-0.26.0 → settfex-0.26.1}/tests/integration/test_live_endpoints.py +0 -0
  198. {settfex-0.26.0 → settfex-0.26.1}/tests/services/__init__.py +0 -0
  199. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/__init__.py +0 -0
  200. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/derive_r59_fixtures.py +0 -0
  201. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/derive_th_fixtures.py +0 -0
  202. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures.py +0 -0
  203. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/README.md +0 -0
  204. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/capital_indirection_en.html +0 -0
  205. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/en_mother_fs_h1_2025.html +0 -0
  206. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/en_ptt_56_1.html +0 -0
  207. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/en_ptt_56_2.html +0 -0
  208. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/en_ptt_fs_h1_2025.html +0 -0
  209. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/en_ptt_viewmore_56_2.html +0 -0
  210. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/idisc_505_error_page.html +0 -0
  211. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000082610_th.json +0 -0
  212. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000092610_th.json +0 -0
  213. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000102610_th.json +0 -0
  214. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000142610_th.json +0 -0
  215. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000152610_th.json +0 -0
  216. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000392610_th.json +0 -0
  217. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000452610_th.json +0 -0
  218. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000522610_th.json +0 -0
  219. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000532609_th.json +0 -0
  220. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000542609_th.json +0 -0
  221. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592001242610_th.json +0 -0
  222. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592001262610_th.json +0 -0
  223. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592001352609_th.json +0 -0
  224. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592001352610_th.json +0 -0
  225. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592001372609_th.json +0 -0
  226. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592001432610_en.json +0 -0
  227. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592001432610_th.json +0 -0
  228. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592002622609_th.json +0 -0
  229. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592002632609_th.json +0 -0
  230. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592002772609_th.json +0 -0
  231. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592002782609_th.json +0 -0
  232. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592002882609_th.json +0 -0
  233. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592002892609_th.json +0 -0
  234. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592003112609_th.json +0 -0
  235. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592003122609_th.json +0 -0
  236. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592003452609_th.json +0 -0
  237. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592003462609_th.json +0 -0
  238. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592003632609_th.json +0 -0
  239. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592003652609_th.json +0 -0
  240. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004042609_th.json +0 -0
  241. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004052609_th.json +0 -0
  242. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004192609_th.json +0 -0
  243. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004202609_th.json +0 -0
  244. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004532609_th.json +0 -0
  245. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004542609_th.json +0 -0
  246. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004802609_th.json +0 -0
  247. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004812609_th.json +0 -0
  248. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004992609_th.json +0 -0
  249. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592005002609_th.json +0 -0
  250. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592005172609_th.json +0 -0
  251. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592005182609_th.json +0 -0
  252. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592005512609_th.json +0 -0
  253. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/en_default_20261007.html +0 -0
  254. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/th_capped_rcv_20261002.html +0 -0
  255. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/th_default_20261007.html +0 -0
  256. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/th_edge_rows.html +0 -0
  257. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/th_empty.html +0 -0
  258. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/th_kcg_txn_202609.html +0 -0
  259. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/th_sep_pairs.html +0 -0
  260. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/th_spali_txn_202609.html +0 -0
  261. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/th_cpall_viewmore_56_1.html +0 -0
  262. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/th_mother_fs_h1_2025.html +0 -0
  263. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/th_ptt_56_1.html +0 -0
  264. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/th_ptt_56_2.html +0 -0
  265. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/th_ptt_fs_h1_2025.html +0 -0
  266. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_code_isolation.py +0 -0
  267. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_company.py +0 -0
  268. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_download.py +0 -0
  269. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_download_evidence.py +0 -0
  270. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_download_indirection.py +0 -0
  271. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_download_result.py +0 -0
  272. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_executive_trades.py +0 -0
  273. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_financial_report.py +0 -0
  274. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_financial_report_accounting.py +0 -0
  275. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_financial_report_th.py +0 -0
  276. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_listing_transport.py +0 -0
  277. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_sec.py +0 -0
  278. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_utils.py +0 -0
  279. {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_view_more_slugs.py +0 -0
  280. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/__init__.py +0 -0
  281. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/index/__init__.py +0 -0
  282. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/index/test_chart_quotation.py +0 -0
  283. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/index/test_composition.py +0 -0
  284. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/index/test_index.py +0 -0
  285. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/index/test_info.py +0 -0
  286. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/index/test_list.py +0 -0
  287. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/__init__.py +0 -0
  288. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/financial/__init__.py +0 -0
  289. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/financial/test_financial.py +0 -0
  290. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/test_analyst_consensus.py +0 -0
  291. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/test_chart_quotation.py +0 -0
  292. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/test_dr_indicative_price.py +0 -0
  293. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/test_info.py +0 -0
  294. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/test_latest_historical_trading.py +0 -0
  295. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/test_profile_dr.py +0 -0
  296. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_asset_type.py +0 -0
  297. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_board_of_director.py +0 -0
  298. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_client.py +0 -0
  299. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_corporate_action.py +0 -0
  300. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_earnings_call.py +0 -0
  301. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_historical.py +0 -0
  302. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_holiday.py +0 -0
  303. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_list.py +0 -0
  304. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_listed_symbol.py +0 -0
  305. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_news.py +0 -0
  306. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_profile_company.py +0 -0
  307. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_realtime.py +0 -0
  308. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_shareholder.py +0 -0
  309. {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_symbol_suggestion.py +0 -0
  310. {settfex-0.26.0 → settfex-0.26.1}/tests/services/test_fault_matrix.py +0 -0
  311. {settfex-0.26.0 → settfex-0.26.1}/tests/services/tfex/__init__.py +0 -0
  312. {settfex-0.26.0 → settfex-0.26.1}/tests/services/tfex/test_client.py +0 -0
  313. {settfex-0.26.0 → settfex-0.26.1}/tests/services/tfex/test_historical.py +0 -0
  314. {settfex-0.26.0 → settfex-0.26.1}/tests/services/tfex/test_list.py +0 -0
  315. {settfex-0.26.0 → settfex-0.26.1}/tests/services/tfex/test_realtime.py +0 -0
  316. {settfex-0.26.0 → settfex-0.26.1}/tests/services/tfex/test_trading_statistics.py +0 -0
  317. {settfex-0.26.0 → settfex-0.26.1}/tests/services/tfex/test_underlying_price.py +0 -0
  318. {settfex-0.26.0 → settfex-0.26.1}/tests/services/thaibma/__init__.py +0 -0
  319. {settfex-0.26.0 → settfex-0.26.1}/tests/services/thaibma/fixtures.py +0 -0
  320. {settfex-0.26.0 → settfex-0.26.1}/tests/services/thaibma/test_availability.py +0 -0
  321. {settfex-0.26.0 → settfex-0.26.1}/tests/services/thaibma/test_history.py +0 -0
  322. {settfex-0.26.0 → settfex-0.26.1}/tests/services/thaibma/test_thaibma.py +0 -0
  323. {settfex-0.26.0 → settfex-0.26.1}/tests/services/thaibma/test_utils.py +0 -0
  324. {settfex-0.26.0 → settfex-0.26.1}/tests/services/thaibma/test_yield_curve.py +0 -0
  325. {settfex-0.26.0 → settfex-0.26.1}/tests/test_deprecations.py +0 -0
  326. {settfex-0.26.0 → settfex-0.26.1}/tests/test_documented_exceptions.py +0 -0
  327. {settfex-0.26.0 → settfex-0.26.1}/tests/test_impersonate_target.py +0 -0
  328. {settfex-0.26.0 → settfex-0.26.1}/tests/test_model_contract.py +0 -0
  329. {settfex-0.26.0 → settfex-0.26.1}/tests/test_not_found_taxonomy.py +0 -0
  330. {settfex-0.26.0 → settfex-0.26.1}/tests/test_public_api_surface.py +0 -0
  331. {settfex-0.26.0 → settfex-0.26.1}/tests/test_readme_links.py +0 -0
  332. {settfex-0.26.0 → settfex-0.26.1}/tests/test_typing_surface.py +0 -0
  333. {settfex-0.26.0 → settfex-0.26.1}/tests/utils/__init__.py +0 -0
  334. {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_blocked_error.py +0 -0
  335. {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_data_fetcher.py +0 -0
  336. {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_formatting.py +0 -0
  337. {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_http.py +0 -0
  338. {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_logging.py +0 -0
  339. {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_logging_neighbourliness.py +0 -0
  340. {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_parsing.py +0 -0
  341. {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_session_manager.py +0 -0
  342. {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_validation.py +0 -0
  343. {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_youtube_transcript.py +0 -0
@@ -31,7 +31,7 @@ jobs:
31
31
  - uses: actions/checkout@v7
32
32
 
33
33
  # setup-uv stopped publishing floating major tags at v8 — pin the exact version.
34
- - uses: astral-sh/setup-uv@v10.1.0
34
+ - uses: astral-sh/setup-uv@v10.2.0
35
35
  with:
36
36
  # Pinned for reproducible builds — "latest" silently adopts whatever uv shipped
37
37
  # that day. Dependabot does not bump action *inputs*, so bump this by hand.
@@ -63,7 +63,7 @@ jobs:
63
63
  - uses: actions/checkout@v7
64
64
 
65
65
  # setup-uv stopped publishing floating major tags at v8 — pin the exact version.
66
- - uses: astral-sh/setup-uv@v10.1.0
66
+ - uses: astral-sh/setup-uv@v10.2.0
67
67
  with:
68
68
  # Pinned for reproducible builds — "latest" silently adopts whatever uv shipped
69
69
  # that day. Dependabot does not bump action *inputs*, so bump this by hand.
@@ -40,7 +40,7 @@ jobs:
40
40
  - uses: actions/checkout@v7
41
41
 
42
42
  # setup-uv stopped publishing floating major tags at v8 — pin the exact version.
43
- - uses: astral-sh/setup-uv@v10.1.0
43
+ - uses: astral-sh/setup-uv@v10.2.0
44
44
  with:
45
45
  # Pinned for reproducible builds — "latest" silently adopts whatever uv shipped
46
46
  # that day. Dependabot does not bump action *inputs*, so bump this by hand.
@@ -138,14 +138,6 @@ jobs:
138
138
  echo " A uv bump is its own change with its own gates — Dependency policy, CLAUDE.md."
139
139
  echo "- **hatchling** (build backend) is unconstrained in [build-system] and absent from"
140
140
  echo " uv.lock — releases resolve whatever is current at build time."
141
- echo "- **diskcache** upstream is dormant (last release 2023, last commit 2024) and is"
142
- echo " at its last release — so its advisory **PYSEC-2026-2447 / CVE-2025-69872**"
143
- echo " (pickle deserialization; empty Fix Versions because no fixed release exists)"
144
- echo " cannot be cleared by a bump. It is the only flagged **runtime** dependency —"
145
- echo " everything else pip-audit reports is dev/extras. Exploiting it needs write"
146
- echo " access to the cache dir, which settfex now creates \`0700\` and tightens in place"
147
- echo " (2026-09-19) -- so the exposure is mitigated and the row below is expected to"
148
- echo " stay. Do not re-triage it monthly; revisit only if upstream ships a fix."
149
141
  echo "- Bumps follow the **Dependency policy** in CLAUDE.md: one package per commit, full"
150
142
  echo " gates each, and curl_cffi additionally requires the live-probe protocol"
151
143
  echo ' (`uv run pytest -m integration --no-cov` before/after with cache clear).'
@@ -55,7 +55,7 @@ jobs:
55
55
  - uses: actions/checkout@v7
56
56
 
57
57
  # setup-uv stopped publishing floating major tags at v8 — pin the exact version.
58
- - uses: astral-sh/setup-uv@v10.1.0
58
+ - uses: astral-sh/setup-uv@v10.2.0
59
59
  with:
60
60
  # Pinned by hand like every other setup-uv step; the monthly drift report checks them all.
61
61
  version: "0.12.17"
@@ -34,7 +34,7 @@ jobs:
34
34
  - uses: actions/checkout@v7
35
35
 
36
36
  # setup-uv stopped publishing floating major tags at v8 — pin the exact version.
37
- - uses: astral-sh/setup-uv@v10.1.0
37
+ - uses: astral-sh/setup-uv@v10.2.0
38
38
  with:
39
39
  # Pinned for reproducible builds — "latest" silently adopts whatever uv shipped
40
40
  # that day. Dependabot does not bump action *inputs*, so bump this by hand.
@@ -87,7 +87,7 @@ jobs:
87
87
  - uses: actions/checkout@v7
88
88
 
89
89
  # setup-uv stopped publishing floating major tags at v8 — pin the exact version.
90
- - uses: astral-sh/setup-uv@v10.1.0
90
+ - uses: astral-sh/setup-uv@v10.2.0
91
91
  with:
92
92
  # Pinned for reproducible builds — "latest" silently adopts whatever uv shipped
93
93
  # that day. Dependabot does not bump action *inputs*, so bump this by hand.
@@ -114,7 +114,7 @@ jobs:
114
114
  - uses: actions/checkout@v7
115
115
 
116
116
  # setup-uv stopped publishing floating major tags at v8 — pin the exact version.
117
- - uses: astral-sh/setup-uv@v10.1.0
117
+ - uses: astral-sh/setup-uv@v10.2.0
118
118
  with:
119
119
  # Pinned for reproducible builds — "latest" silently adopts whatever uv shipped
120
120
  # that day. Dependabot does not bump action *inputs*, so bump this by hand.
@@ -20,7 +20,7 @@ jobs:
20
20
  - uses: actions/checkout@v7
21
21
 
22
22
  # setup-uv stopped publishing floating major tags at v8 — pin the exact version.
23
- - uses: astral-sh/setup-uv@v10.1.0
23
+ - uses: astral-sh/setup-uv@v10.2.0
24
24
  with:
25
25
  # Pinned for reproducible builds — "latest" silently adopts whatever uv shipped
26
26
  # that day. Dependabot does not bump action *inputs*, so bump this by hand.
@@ -50,7 +50,7 @@ jobs:
50
50
  - uses: actions/checkout@v7
51
51
 
52
52
  # setup-uv stopped publishing floating major tags at v8 — pin the exact version.
53
- - uses: astral-sh/setup-uv@v10.1.0
53
+ - uses: astral-sh/setup-uv@v10.2.0
54
54
  with:
55
55
  # Pinned for reproducible builds — "latest" silently adopts whatever uv shipped
56
56
  # that day. Dependabot does not bump action *inputs*, so bump this by hand.
@@ -64,5 +64,6 @@ jobs:
64
64
  # Non-blocking: surfaces dependency CVEs for triage without blocking PRs.
65
65
  # Some advisories are transitive or not-yet-fixed upstream (e.g. no fix
66
66
  # released), so review results and bump deps in a dedicated PR.
67
+ # (0.26.1 removed diskcache, so its PYSEC-2026-2447 ignore is gone too: nothing is ignored.)
67
68
  - run: uv run pip-audit --desc
68
69
  continue-on-error: true
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.26.1] - 2026-11-02
11
+
12
+ **Security: settfex no longer depends on `diskcache`, and nothing it caches is ever unpickled.**
13
+ The same fix is released for the older lines that consumers still pin, as 0.17.1, 0.15.1 and
14
+ 0.3.1, so each can take a patch bump instead of migrating across 0.24.0.
15
+
16
+ ### Security
17
+
18
+ - **The session cache no longer unpickles anything, and `diskcache` is no longer a dependency**
19
+ (`settfex.utils.session_cache`). `diskcache` reads cached values back with pickle
20
+ (PYSEC-2026-2447 / CVE-2025-69872 / GHSA-w8v5-vhqr-4h9v). It has no fixed release, and upstream
21
+ has been dormant since 2023, so anyone able to write into the cache directory could execute code
22
+ in the process that read it. Every consumer's pip-audit flagged it on a runtime dependency.
23
+ - The store is now stdlib `sqlite3` holding JSON text (`sessions.sqlite3`, WAL, created `0600`
24
+ inside the `0700` directory). A row that is not a JSON object is a cache miss, never code.
25
+ - The pre-0.26.1 `cache.db` is never opened, so it is never unpickled. A session warms up once
26
+ after the upgrade, and an INFO line says the old file can be deleted.
27
+ - `SessionCache`'s API is unchanged (the same methods and parameters), and so is
28
+ `SessionManager`.
29
+ - settfex's own Security workflow no longer needs an ignore. This release also moves four
30
+ packages that `pip-audit` flagged in `uv.lock`, one commit each: `certifi` 2026.10.23 (a
31
+ `curl-cffi` dependency), plus `virtualenv` 21.14.13, `filelock` 4.2.6 and `msgpack` 1.2.4
32
+ (dev tools only). After these bumps `pip-audit` reports no known vulnerabilities. None of them
33
+ is a floor in `pyproject.toml`, so the only change to the published wheel's requirements is
34
+ the removal of `diskcache`.
35
+
10
36
  ## [0.26.0] - 2026-10-08
11
37
 
12
38
  **SEC Form 59 arrives: directors' and executives' trades, with the report behind each one.**
@@ -549,7 +549,7 @@ See [`CHANGELOG.md`](CHANGELOG.md) for the full, versioned release history — t
549
549
  - **The stock-info endpoint has NO language dimension, and its timestamp is nanosecond-precision:** `?lang=en` and `?lang=th` return byte-identical payloads (both `nameEN` and `nameTH` always present), so `fetch_stock_info()` deliberately takes **no** `lang` argument — do not add one "for consistency" (same shape as the analyst-consensus table endpoint). Separately, `marketDateTime` carries **9 fractional digits** (`...783886192+07:00`) — unique among the SET endpoints here; pydantic and `datetime.fromisoformat` both truncate to microseconds on 3.11+, and a test pins that it is truncated rather than rejected.
550
550
  - **Over plain HTTP the quote block serves the BEST bid/offer level only:** `bids`/`offers` come back with one entry each, not the 5-level ladder shown on the set.or.th page (that depth is websocket-fed). Ladder `price` arrives as a **string** (`"44.75"`) and is coerced by the shared `BidOffer` model, which `stock/info.py` imports from `index/composition.py` rather than duplicating.
551
551
  - **A stock-info 404 means "unknown symbol" — unlike the DR-profile 404:** `/api/set/stock/{sym}/info` answers an unknown ticker with HTTP 404 `{"message": "Invalid Stock Name"}` and resolves every real one, so `SymbolNotFoundError` is raised **with** the "did you mean?" suggester enabled (the opposite of `/api/set/dr/{sym}/profile`, which 404s for valid non-DRs and must pass `suggest=False`).
552
- - **The session cache dir is forced to `0700`, and a caller-supplied one is deliberately NOT — do not "make it consistent":** `diskcache` reads cached values back with **pickle**, so write access to `~/.settfex/cache` is code execution in the caller's process (CVE-2025-69872 / PYSEC-2026-2447 — **no fixed release**, upstream's last release was 2023; it is the only advisory `pip-audit` still reports and the only one on a *runtime* dependency). `_secure_cache_dir()` therefore creates the directory `0700` and **chmods an existing default one in place** (a real side effect on the user's filesystem, logged at INFO). A directory the caller passed is created `0700` when new but, if it already exists group/other-writable, is **reported and left alone** — a shared cache can be deliberate, and silently breaking a multi-account layout is worse than the warning. Windows is skipped outright (POSIX bits do not express this), and every permission step is inside `try/except OSError` because a cache that cannot be chmod'd is still a usable cache. Tests pin all four behaviours in `tests/utils/test_session_cache.py`.
552
+ - **The session cache no longer unpickles (0.26.1) — and the `0700` rule stays, for a different reason:** until 0.26.1 the store was `diskcache`, which reads values back with **pickle** (CVE-2025-69872 / PYSEC-2026-2447, **no fixed release**, upstream dormant since 2023), so write access to `~/.settfex/cache` was code execution in the caller's process, and every consumer's pip-audit flagged it on a *runtime* dependency. `settfex/utils/session_cache.py` is now stdlib **`sqlite3` holding JSON text** (`sessions.sqlite3`, WAL, created `0600`): `json.loads` on read, a row that is not a JSON object is a miss, and the old `cache.db` is **never opened** (one extra warm-up after upgrading; an INFO line says it can be deleted). The public API is unchanged. **Do not reintroduce a pickling store.** The directory rule is unchanged because the cookies are credentials: `_secure_cache_dir()` creates it `0700` and **chmods an existing default one in place** (logged at INFO); a directory the caller passed is created `0700` when new but, if it already exists group/other-writable, is **reported and left alone** — a shared cache can be deliberate. Windows is skipped (POSIX bits do not express this), and every permission step is inside `try/except OSError`. Tests pin the permissions, the no-unpickle property and the store's behaviour in `tests/utils/test_session_cache.py`.
553
553
  - **SEC serves FOUR download-URL shapes; two of them were silently dropping live filings:** `classify_download_href` knew `Download?FILEID=<path>` and `ipos/Common/IPOSGetFile.aspx?id=`. **Key Financial Ratio** rows use two more, and both fell through to "not a download link" and made the row vanish — live-probed 2026-09-20 on CPALL, both real downloads: (1) recent rows (2026 Q1/Q2) link through `/public/idisc/Views/FinancialStatementDownload?query=<opaque base64>`, answering `application/zip` — a **134 KB zip of the real PDFs**; (2) older rows (2018, 2019) link through `/public/idisc/views/viewdoc?…&TransId=<id>&FileSeq=<n>`, answering `application/.tif` — a **36 KB scan of the original**. Mapped now as `file_id="fsdl:<blob>"` / `"viewdoc:<id>-<n>"`, both `file_kind=None` (neither URL states a type; `Content-Disposition` names the file at download time). After the fix, CPALL 2015-2026 returns **177 documents with `completeness()` exact in all five categories** — `key_financial_ratio` was `(13, 15)`. ⚠️ **Three things this should teach the next reader.** (a) **It falsifies the reporting issue's own finding:** its corpus scan of 39 captured pages found *zero* natural instances of a dropped row and stated P1 "has not been observed in the wild"; the first two live runs of the new accounting found four. A captured corpus can only show what was captured. (b) **The window is load-bearing:** a narrow recent window passed clean while the 2018/2019 rows were being dropped — **old filings use old URL shapes**, so a probe that only checks recent data cannot see them. (c) **Do NOT generalise the allowlist** into "any link under /public/idisc/ with a query is a download": the "display all results" ViewMore link has exactly that shape, and would become a phantom filing in every truncated section. Expect the list to grow; `ListingAccounting.no_link` is the mechanism that makes the next one visible, and a test pins the ViewMore link as not-a-download. Note `_fallback_filename` keys on the **colon**, not on an `ipos:` prefix: a real FILEID path never contains one, so every synthetic id is safely excluded from being sliced like a path.
554
554
  - **The SEC LISTING path checks its HTTP status now — and a ViewMore failure DEGRADES rather than raising (issue #131):** `_run_search` used to return `post_resp.text` without ever reading `status_code`, so an error page parsed to zero rows and came back as `[]` with `has_losses == False` — indistinguishable from an issuer that filed nothing, and intermittent, so a backfill recorded the window as covered. Evidenced by a real **HTTP 505** from the listing host (2026-09-20). Two rules: non-2xx → `FetchError`; a 2xx body with **no result table and no section heading** → `ParseError`. The content rule is deliberately the weakest one that separates the populations, because the case it must never trip is a **genuinely empty listing** — a test pins it against every page this repo holds. It does **not** key on `ctl00_CPH_pnlControl`: the real ViewMore capture has that id and the trimmed `FS_VIEWMORE_HTML` constant does not, so a panel-id rule would call a legitimate page an error. ⚠️ **The severity ladder differs by leg, on purpose.** The POST leg raises (a total loss — nothing to fall back on). A **ViewMore** page is different: it *replaces* its section's inline rows, so 0.22.0's unconditional replacement meant a failing page **deleted the whole section, inline rows included, in silence**. A broken ViewMore is a *partial* loss, so it now keeps the inline rows, warns, and lets `completeness()` show the shortfall. `_map_rows` additionally refuses a **zero-row** input: within this package that can only be a non-listing body, since every captured page carries at least the placeholder row an empty section serves. ⚠️ **U-7 (0.25.0): a ViewMore page must never REDUCE the row set — and until 0.25.0 a *successful* one could.** Two ways, both silent: (1) a page that mapped to fewer documents than the inline rows replaced them anyway (no comparison), so a short or placeholder-only page deleted rows with `has_losses` still `False`; (2) a page whose `_map_rows` raised (`ParseError`/`IncompleteListingError`) escaped the loop, and the per-code gather then failed the **whole report code**, taking every sibling category's inline rows with it. Both now degrade exactly like an unusable page (`DegradedSection`, inline rows kept). The rule is a **count** (ViewMore docs for the category ≥ inline docs), **not identity** — a superset check by file id would flip the synthetic test pairs and misfire on `fsdl:` ids, which may be minted per request. On the degrade path `reported.update`/`supersede` are skipped, so `is_balanced` holds and the inline page's reported count stays for `completeness()`; on the replace path only the replaced category's reported count moves.
555
555
  - **The live Thai `fs-kf` ViewMore page has been answering HTTP 500 since at least 2026-09-20 — an UPSTREAM bug, and it is why the degrade path exists:** probed 3× each, `th/fs-kf` returned 500 every time while `th/fs-norm`, `th/fs-mda` and all three `en/*` returned 200. On 0.22.0 this silently removed CPALL's entire Thai Key Financial Ratio section; now the listing returns 177 documents with `key_financial_ratio: (10, 15)` stated in `completeness()` plus two WARNINGs. **If you see a Thai KFR shortfall, check whether SEC has fixed their page before looking in settfex.**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: settfex
3
- Version: 0.26.0
3
+ Version: 0.26.1
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
@@ -27,7 +27,6 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
27
  Classifier: Typing :: Typed
28
28
  Requires-Python: >=3.11
29
29
  Requires-Dist: curl-cffi>=0.6.0
30
- Requires-Dist: diskcache>=5.6.0
31
30
  Requires-Dist: loguru>=0.7.0
32
31
  Requires-Dist: pydantic>=2.0.0
33
32
  Provides-Extra: dataframe
@@ -40,9 +40,9 @@ The `settfex` library uses **disk-based session caching** to dramatically improv
40
40
  │
41
41
  ▼
42
42
  ┌─────────────────────────────────────────────────┐
43
- │ SessionCache (diskcache) │
44
- │ Location: ~/.settfex/cache/ │
45
- │ Format: SQLite + pickled data │
43
+ │ SessionCache (stdlib sqlite3) │
44
+ │ Location: ~/.settfex/cache/sessions.sqlite3 │
45
+ │ Format: SQLite rows holding JSON text │
46
46
  │ TTL: 1 hour (configurable) │
47
47
  │ Keys: │
48
48
  │ - set_session_chrome120 │
@@ -269,16 +269,18 @@ await manager.ensure_initialized()
269
269
  The cache directory is created **`0700`** (owner only), and an existing `~/.settfex/cache` left
270
270
  looser by an earlier version is tightened in place on the next run, with a log line saying so.
271
271
 
272
- Two reasons, and the second is the sharp one:
272
+ The reason: the cached cookies are credentials. Anything that can read them can impersonate your
273
+ session, and anything that can write them can plant cookies your process will send.
273
274
 
274
- 1. The cached cookies are credentials — anything that can read them can impersonate your session.
275
- 2. `diskcache` reads cached values back with **pickle**. Anyone who can *write* into this
276
- directory can therefore execute code inside your process the next time it reads the cache
277
- ([CVE-2025-69872](https://nvd.nist.gov/vuln/detail/CVE-2025-69872)). The advisory has no fixed
278
- release — upstream's last release was 2023 — so the directory mode is the mitigation.
275
+ **Nothing in the cache is unpickled (since 0.26.1).** Before 0.26.1 the store was `diskcache`,
276
+ which reads values back with pickle
277
+ ([CVE-2025-69872](https://nvd.nist.gov/vuln/detail/CVE-2025-69872), no fixed release upstream), so
278
+ write access to this directory meant code execution in your process. The store is now stdlib
279
+ `sqlite3` holding JSON text: a row that is not a JSON object is a cache miss, never code. The old
280
+ `cache.db` is never opened; delete it if you like. The session warms up once after the upgrade.
279
281
 
280
282
  A directory **you** passed as `cache_dir` is treated differently: it is created `0700` if new, but
281
- if it already exists and is group/other-**writable**, settfex logs a warning naming the CVE and
283
+ if it already exists and is group/other-**writable**, settfex logs a warning and
282
284
  **leaves the mode alone**. A shared cache may be deliberate, and silently breaking that layout
283
285
  would be worse than telling you about it.
284
286
 
@@ -496,9 +498,8 @@ Single request comparison:
496
498
 
497
499
  ## Dependencies
498
500
 
499
- Session caching requires:
500
- - `diskcache >= 5.6.0` - Disk-based cache with SQLite backend
501
- - Automatically installed with `settfex`
501
+ Session caching uses only the standard library (`sqlite3`, `json`). There is no third-party
502
+ cache dependency since 0.26.1, which dropped `diskcache` and its pickle advisory.
502
503
 
503
504
  ## Summary
504
505
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "settfex"
3
- version = "0.26.0"
3
+ version = "0.26.1"
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"
@@ -8,7 +8,6 @@ dependencies = [
8
8
  "curl-cffi>=0.6.0",
9
9
  "pydantic>=2.0.0",
10
10
  "loguru>=0.7.0",
11
- "diskcache>=5.6.0",
12
11
  ]
13
12
  authors = [
14
13
  { name = "batt", email = "b@candythink.com" }
@@ -64,7 +64,7 @@ Usage:
64
64
  >>> asyncio.run(main())
65
65
  """
66
66
 
67
- __version__ = "0.26.0"
67
+ __version__ = "0.26.1"
68
68
  __author__ = "batt"
69
69
  __license__ = "MIT"
70
70
 
@@ -1,5 +1,5 @@
1
1
  """
2
- Session cache for persistent cookie storage using diskcache.
2
+ Session cache for persistent cookie storage: stdlib ``sqlite3`` holding JSON values.
3
3
 
4
4
  This module provides disk-based caching for curl_cffi session cookies,
5
5
  enabling fast session reuse without warming up every time.
@@ -7,25 +7,43 @@ enabling fast session reuse without warming up every time.
7
7
  Key features:
8
8
  - Persistent storage across program restarts
9
9
  - Automatic expiration and refresh
10
- - Thread-safe disk cache
10
+ - Thread-safe, and safe for several processes sharing one cache directory (SQLite WAL)
11
11
  - Fallback to warm-up on cache miss or failure
12
+
13
+ **Nothing here unpickles.** Until 0.26.1 the store was ``diskcache``, which reads values back with
14
+ pickle (CVE-2025-69872 / PYSEC-2026-2447, no fixed release upstream): write access to the cache
15
+ directory meant code execution in the reading process. Values are now JSON text, decoded with
16
+ ``json.loads``; a row that does not decode to a JSON object is a cache miss, never code. The old
17
+ diskcache database (``cache.db``) is never opened, so it is never unpickled either.
12
18
  """
13
19
 
14
20
  import asyncio
21
+ import json
15
22
  import os
23
+ import sqlite3
16
24
  import stat
25
+ import threading
17
26
  import time
18
27
  from pathlib import Path
19
28
  from typing import Any
20
29
 
21
- import diskcache # type: ignore[import-untyped]
22
30
  from loguru import logger
23
31
 
24
- # diskcache deserializes cached values with pickle (CVE-2025-69872 / PYSEC-2026-2447, which has
25
- # NO fixed release upstream), so any account that can write into the cache directory executes
26
- # code inside this process the next time the cache is read. The cached cookies are credentials
27
- # in their own right. 0700 removes both reaches for every other account on the machine.
32
+ # The cached cookies are credentials in their own right: another account that can read the
33
+ # directory can replay the session, and one that can write it can plant cookies. 0700 on the
34
+ # directory and 0600 on the database remove both reaches for every other account on the machine.
28
35
  _CACHE_DIR_MODE = 0o700
36
+ _CACHE_FILE_MODE = 0o600
37
+ _DB_NAME = "sessions.sqlite3"
38
+ # The pre-0.26.1 diskcache database. Never opened: diskcache would unpickle it.
39
+ _LEGACY_DISKCACHE_DB = "cache.db"
40
+ _SCHEMA = (
41
+ "CREATE TABLE IF NOT EXISTS sessions ("
42
+ " key TEXT PRIMARY KEY,"
43
+ " value TEXT NOT NULL,"
44
+ " stored_at REAL NOT NULL,"
45
+ " expires_at REAL NOT NULL)"
46
+ )
29
47
 
30
48
 
31
49
  def _secure_cache_dir(cache_dir: Path, *, owned: bool) -> None:
@@ -43,7 +61,7 @@ def _secure_cache_dir(cache_dir: Path, *, owned: bool) -> None:
43
61
  module's contract is to degrade rather than fail (see the try/except in every method below).
44
62
 
45
63
  Args:
46
- cache_dir: Directory that will hold the diskcache database.
64
+ cache_dir: Directory that will hold the session database.
47
65
  owned: True when settfex chose the location, False when the caller passed one.
48
66
  """
49
67
  cache_dir.mkdir(mode=_CACHE_DIR_MODE, parents=True, exist_ok=True)
@@ -63,20 +81,34 @@ def _secure_cache_dir(cache_dir: Path, *, owned: bool) -> None:
63
81
  cache_dir.chmod(_CACHE_DIR_MODE)
64
82
  logger.info(
65
83
  f"Tightened cache directory permissions: {cache_dir} {mode:04o} -> 0700 "
66
- f"(cached cookies are credentials, and diskcache reads them back via pickle)"
84
+ f"(cached cookies are credentials)"
67
85
  )
68
86
  elif mode & 0o022:
69
87
  logger.warning(
70
- f"Cache directory {cache_dir} is group/other-writable ({mode:04o}). diskcache "
71
- f"deserializes with pickle, so anyone who can write here can execute code in "
72
- f"this process (CVE-2025-69872). Leaving the mode alone because you chose this "
73
- f"path -- pass a private directory, or chmod 700 it, if the sharing is not "
74
- f"deliberate."
88
+ f"Cache directory {cache_dir} is group/other-writable ({mode:04o}). The cached "
89
+ f"session cookies are credentials: anyone who can write here can plant cookies "
90
+ f"this process will send. Leaving the mode alone because you chose this path -- "
91
+ f"pass a private directory, or chmod 700 it, if the sharing is not deliberate."
75
92
  )
76
93
  except OSError as e:
77
94
  logger.warning(f"Could not verify permissions on cache directory {cache_dir}: {e}")
78
95
 
79
96
 
97
+ def _secure_db_file(path: Path) -> None:
98
+ """Create the database file ``0600`` before SQLite opens it; tighten a loose existing one.
99
+
100
+ SQLite creates its ``-wal`` / ``-shm`` files with the main database's permissions, so getting
101
+ this file right covers all three. Never raises (same contract as :func:`_secure_cache_dir`).
102
+ """
103
+ try:
104
+ fd = os.open(path, os.O_CREAT | os.O_WRONLY, _CACHE_FILE_MODE)
105
+ os.close(fd)
106
+ if os.name != "nt" and stat.S_IMODE(path.stat().st_mode) & 0o077:
107
+ path.chmod(_CACHE_FILE_MODE)
108
+ except OSError as e:
109
+ logger.warning(f"Could not verify permissions on cache file {path}: {e}")
110
+
111
+
80
112
  class SessionCache:
81
113
  """
82
114
  Disk-based cache for session cookies and metadata.
@@ -86,6 +118,9 @@ class SessionCache:
86
118
  cookies are available and valid, they're used immediately (fast path).
87
119
  Otherwise, a new session is warmed up and cached.
88
120
 
121
+ Values must be JSON-serializable dicts (cookies as ``{name: value}`` strings, the browser
122
+ name, timestamps, counts). They are stored as JSON text in SQLite; nothing is unpickled.
123
+
89
124
  Architecture:
90
125
  1. Check cache for valid cookies → Use if found (fast)
91
126
  2. If not found or expired → Warm up → Cache it
@@ -116,7 +151,8 @@ class SessionCache:
116
151
  Args:
117
152
  cache_dir: Directory for cache storage (default: ~/.settfex/cache)
118
153
  default_ttl: Default time-to-live for cached items in seconds (default: 1 hour)
119
- size_limit: Maximum cache size in bytes (default: 100MB)
154
+ size_limit: Maximum total size of stored values in bytes (default: 100MB); the
155
+ oldest entries are evicted beyond it
120
156
  """
121
157
  # The `is None` test is repeated rather than reusing `is_default`, because mypy narrows
122
158
  # the parameter on the literal check but not through the flag.
@@ -126,9 +162,26 @@ class SessionCache:
126
162
  # Create the cache directory 0700 -- see _secure_cache_dir for why the mode matters.
127
163
  _secure_cache_dir(cache_dir, owned=is_default)
128
164
 
129
- # Initialize diskcache
130
- self.cache = diskcache.Cache(str(cache_dir), size_limit=size_limit)
165
+ self.path = cache_dir / _DB_NAME
166
+ _secure_db_file(self.path)
131
167
  self.default_ttl = default_ttl
168
+ self.size_limit = size_limit
169
+ self._lock = threading.Lock()
170
+ # Autocommit (isolation_level=None) with a busy timeout: several processes on one host
171
+ # may share this directory, and WAL lets their readers and the one writer coexist.
172
+ self._db = sqlite3.connect(
173
+ str(self.path), timeout=10.0, isolation_level=None, check_same_thread=False
174
+ )
175
+ with self._lock:
176
+ self._db.execute("PRAGMA journal_mode=WAL")
177
+ self._db.execute(_SCHEMA)
178
+
179
+ if (cache_dir / _LEGACY_DISKCACHE_DB).exists():
180
+ logger.info(
181
+ f"A pre-0.26.1 diskcache database is present at {cache_dir / _LEGACY_DISKCACHE_DB}"
182
+ f" and is NOT read (diskcache unpickles its values). It can be deleted; the "
183
+ f"session will warm up once and be cached in {_DB_NAME}."
184
+ )
132
185
 
133
186
  logger.info(
134
187
  f"SessionCache initialized at {cache_dir} "
@@ -143,7 +196,7 @@ class SessionCache:
143
196
  key: Cache key (e.g., "set_session", "tfex_session")
144
197
 
145
198
  Returns:
146
- Cached session data dict or None if not found
199
+ Cached session data dict or None if not found, expired, or unreadable
147
200
 
148
201
  Example:
149
202
  >>> cached = cache.get("set_session")
@@ -152,14 +205,23 @@ class SessionCache:
152
205
  >>> browser = cached["browser"]
153
206
  """
154
207
  try:
155
- value = self.cache.get(key)
156
- if value is not None:
157
- logger.debug(f"Cache HIT: {key}")
158
- return value # type: ignore[no-any-return]
159
- else:
208
+ with self._lock:
209
+ row = self._db.execute(
210
+ "SELECT value, expires_at FROM sessions WHERE key = ?", (key,)
211
+ ).fetchone()
212
+ if row is not None and row[1] <= time.time():
213
+ self._db.execute("DELETE FROM sessions WHERE key = ?", (key,))
214
+ row = None
215
+ if row is None:
160
216
  logger.debug(f"Cache MISS: {key}")
161
217
  return None
162
- except Exception as e:
218
+ value = json.loads(row[0])
219
+ if not isinstance(value, dict):
220
+ logger.warning(f"Cache entry {key!r} is not a JSON object; treating it as a miss")
221
+ return None
222
+ logger.debug(f"Cache HIT: {key}")
223
+ return value
224
+ except (sqlite3.Error, ValueError) as e:
163
225
  logger.error(f"Failed to read from cache: {e}")
164
226
  return None
165
227
 
@@ -174,15 +236,16 @@ class SessionCache:
174
236
 
175
237
  Args:
176
238
  key: Cache key (e.g., "set_session")
177
- value: Session data to cache (must be dict with "cookies", "browser", etc.)
239
+ value: Session data to cache: a JSON-serializable dict with "cookies", "browser",
240
+ etc.
178
241
  ttl: Time-to-live in seconds (uses default_ttl if None)
179
242
 
180
243
  Returns:
181
- True if successfully cached, False otherwise
244
+ True if successfully cached, False otherwise (including a value JSON cannot encode)
182
245
 
183
246
  Example:
184
247
  >>> cache.set("set_session", {
185
- >>> "cookies": "charlot=abc; incap_ses=xyz",
248
+ >>> "cookies": {"incap_ses": "xyz"},
186
249
  >>> "browser": "chrome120",
187
250
  >>> "warmup_time": time.time(),
188
251
  >>> "cookie_count": 12
@@ -190,17 +253,39 @@ class SessionCache:
190
253
  """
191
254
  try:
192
255
  ttl = ttl or self.default_ttl
256
+ now = time.time()
193
257
  # Add metadata
194
- value["cached_at"] = time.time()
258
+ value["cached_at"] = now
195
259
  value["ttl"] = ttl
196
-
197
- self.cache.set(key, value, expire=ttl)
260
+ encoded = json.dumps(value)
261
+
262
+ with self._lock:
263
+ self._db.execute(
264
+ "INSERT OR REPLACE INTO sessions (key, value, stored_at, expires_at)"
265
+ " VALUES (?, ?, ?, ?)",
266
+ (key, encoded, now, now + ttl),
267
+ )
268
+ self._db.execute("DELETE FROM sessions WHERE expires_at <= ?", (now,))
269
+ self._evict_over_limit()
198
270
  logger.success(f"✓ Cached session: {key} (ttl={ttl}s)")
199
271
  return True
200
- except Exception as e:
272
+ except (sqlite3.Error, TypeError, ValueError) as e:
201
273
  logger.error(f"Failed to write to cache: {e}")
202
274
  return False
203
275
 
276
+ def _evict_over_limit(self) -> None:
277
+ """Drop the oldest entries while the stored values exceed ``size_limit``. Lock held."""
278
+ while True:
279
+ total = self._db.execute(
280
+ "SELECT COALESCE(SUM(LENGTH(value)), 0), COUNT(*) FROM sessions"
281
+ ).fetchone()
282
+ if total[0] <= self.size_limit or total[1] <= 1:
283
+ return
284
+ self._db.execute(
285
+ "DELETE FROM sessions WHERE key ="
286
+ " (SELECT key FROM sessions ORDER BY stored_at ASC LIMIT 1)"
287
+ )
288
+
204
289
  def delete(self, key: str) -> bool:
205
290
  """
206
291
  Delete cached session data.
@@ -212,11 +297,12 @@ class SessionCache:
212
297
  True if deleted, False otherwise
213
298
  """
214
299
  try:
215
- result = self.cache.delete(key)
216
- if result:
300
+ with self._lock:
301
+ deleted = self._db.execute("DELETE FROM sessions WHERE key = ?", (key,)).rowcount
302
+ if deleted:
217
303
  logger.debug(f"Deleted from cache: {key}")
218
- return bool(result)
219
- except Exception as e:
304
+ return bool(deleted)
305
+ except sqlite3.Error as e:
220
306
  # "delete from" below is log text, not SQL (bandit B608 false positive).
221
307
  logger.error(f"Failed to delete from cache: {e}") # nosec B608
222
308
  return False
@@ -251,9 +337,10 @@ class SessionCache:
251
337
  def clear(self) -> None:
252
338
  """Clear all cached data."""
253
339
  try:
254
- self.cache.clear()
340
+ with self._lock:
341
+ self._db.execute("DELETE FROM sessions")
255
342
  logger.info("Cache cleared")
256
- except Exception as e:
343
+ except sqlite3.Error as e:
257
344
  logger.error(f"Failed to clear cache: {e}")
258
345
 
259
346
  def stats(self) -> dict[str, Any]:
@@ -261,23 +348,25 @@ class SessionCache:
261
348
  Get cache statistics.
262
349
 
263
350
  Returns:
264
- Dict with cache statistics (size, volume, hits, etc.)
351
+ Dict with ``size`` (bytes of stored values) and ``count`` (number of entries)
265
352
  """
266
353
  try:
267
- return {
268
- "size": self.cache.volume(), # Bytes used
269
- "count": len(self.cache), # Number of items
270
- }
271
- except Exception as e:
354
+ with self._lock:
355
+ size, count = self._db.execute(
356
+ "SELECT COALESCE(SUM(LENGTH(value)), 0), COUNT(*) FROM sessions"
357
+ ).fetchone()
358
+ return {"size": int(size), "count": int(count)}
359
+ except sqlite3.Error as e:
272
360
  logger.error(f"Failed to get cache stats: {e}")
273
361
  return {"size": 0, "count": 0}
274
362
 
275
363
  def close(self) -> None:
276
364
  """Close the cache."""
277
365
  try:
278
- self.cache.close()
366
+ with self._lock:
367
+ self._db.close()
279
368
  logger.debug("Cache closed")
280
- except Exception as e:
369
+ except sqlite3.Error as e:
281
370
  logger.error(f"Failed to close cache: {e}")
282
371
 
283
372
  def __enter__(self) -> "SessionCache":
@@ -319,8 +408,8 @@ async def get_global_cache(
319
408
 
320
409
  async with _cache_lock:
321
410
  if _global_cache is None:
322
- # SessionCache.__init__ does blocking disk I/O (mkdir + opening the diskcache
323
- # SQLite DB); run it off the event loop so cold cache init never blocks the loop.
411
+ # SessionCache.__init__ does blocking disk I/O (mkdir + opening the SQLite
412
+ # database); run it off the event loop so cold cache init never blocks the loop.
324
413
  _global_cache = await asyncio.to_thread(
325
414
  SessionCache, cache_dir=cache_dir, default_ttl=default_ttl
326
415
  )