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.
- {settfex-0.26.0 → settfex-0.26.1}/.github/workflows/ci.yml +2 -2
- {settfex-0.26.0 → settfex-0.26.1}/.github/workflows/dependency-drift.yml +1 -9
- {settfex-0.26.0 → settfex-0.26.1}/.github/workflows/live-smoke.yml +1 -1
- {settfex-0.26.0 → settfex-0.26.1}/.github/workflows/pre-commit.yml +1 -1
- {settfex-0.26.0 → settfex-0.26.1}/.github/workflows/release.yml +2 -2
- {settfex-0.26.0 → settfex-0.26.1}/.github/workflows/security.yml +3 -2
- {settfex-0.26.0 → settfex-0.26.1}/CHANGELOG.md +26 -0
- {settfex-0.26.0 → settfex-0.26.1}/CLAUDE.md +1 -1
- {settfex-0.26.0 → settfex-0.26.1}/PKG-INFO +1 -2
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/utils/session_caching.md +14 -13
- {settfex-0.26.0 → settfex-0.26.1}/pyproject.toml +1 -2
- {settfex-0.26.0 → settfex-0.26.1}/settfex/__init__.py +1 -1
- {settfex-0.26.0 → settfex-0.26.1}/settfex/utils/session_cache.py +136 -47
- settfex-0.26.1/tests/utils/test_session_cache.py +240 -0
- {settfex-0.26.0 → settfex-0.26.1}/uv.lock +327 -271
- settfex-0.26.0/tests/utils/test_session_cache.py +0 -132
- {settfex-0.26.0 → settfex-0.26.1}/.editorconfig +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/.github/dependabot.yml +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/.github/instructions/core-architectrual-principles.instructions.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/.github/instructions/documentation-standards.instructions.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/.github/instructions/file-organization.instructions.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/.github/instructions/git-commit.instructions.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/.github/instructions/python-dependency-management.instructions.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/.github/prompts/Coding.prompt.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/.github/prompts/Git-Commit-Reviewer.prompt.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/.github/prompts/Prompt-Engineer.prompt.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/.github/prompts/Python-Architect.prompt.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/.gitignore +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/.pre-commit-config.yaml +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/.python-version +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/AGENTS.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/CODE_OF_CONDUCT.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/COMPREHENSIVE_AUDIT.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/CONTRIBUTING.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/LICENSE +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/README.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/RELEASING.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/SECURITY.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/guide/PYTHON_LIBRARY_BEST_PRACTICES.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/index.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/sec/executive_trades.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/sec/financial_report.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/API_PROTECTION_NOTE.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/analyst_consensus.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/board_of_director.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/chart_quotation.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/corporate_action.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/dr_indicative_price.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/earnings_call.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/financial.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/highlight_data.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/holiday.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/index.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/info.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/latest_historical_trading.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/list.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/news.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/nvdr_holder.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/price_performance.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/profile_company.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/profile_dr.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/profile_stock.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/shareholder.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/set/trading_stat.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/tfex/list.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/tfex/trading_statistics.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/services/thaibma/yield_curve.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/settfex/utils/data_fetcher.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/solution/FINDINGS.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/solution/Performance Boost Session Caching Implementation.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/solution/SESSION_CACHE_SUMMARY.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/docs/solution/SOLUTION_100_PERCENT.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/README.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/sec/01_financial_report.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/sec/02_executive_trades.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/01_stock_list.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/02_highlight_data.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/03_stock_profile.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/04_company_profile.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/05_corporate_action.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/06_shareholder.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/07_nvdr_holder.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/08_board_of_director.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/09_trading_statistics.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/10_price_performance.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/11_financial.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/12_earnings_call.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/13_chart_quotation.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/14_latest_historical_trading.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/15_market_index.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/16_news.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/17_holiday.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/18_dr_and_asset_type.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/19_analyst_consensus.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/20_stock_info.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/README.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/cpall_balance_sheet.csv +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/market_dashboard_20251005_173221.csv +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/market_dashboard_20251005_210737.csv +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/market_dashboard_20251006_091440.csv +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/thailand_banking_stocks.csv +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/thailand_set_main_board.csv +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/set/thailand_stock_universe.csv +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/tfex/01_series_list.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/tfex/02_trading_statistics.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/tfex/03_underlying_price.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/examples/thaibma/01_government_yield_curve.ipynb +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/deprecations.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/exceptions.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/py.typed +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/sec/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/sec/company.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/sec/constants.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/sec/download.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/sec/executive_trades.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/sec/financial_report.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/sec/sec.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/sec/utils.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/asset_type.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/constants.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/earnings_call.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/holiday.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/index/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/index/chart_quotation.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/index/composition.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/index/index.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/index/info.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/index/list.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/index/utils.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/list.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/news.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/analyst_consensus.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/board_of_director.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/chart_quotation.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/corporate_action.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/dr_indicative_price.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/financial/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/financial/financial.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/highlight_data.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/info.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/latest_historical_trading.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/nvdr_holder.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/price_performance.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/profile_company.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/profile_dr.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/profile_stock.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/shareholder.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/stock.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/trading_stat.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/set/stock/utils.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/tfex/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/tfex/constants.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/tfex/list.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/tfex/trading_statistics.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/tfex/underlying_price.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/thaibma/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/thaibma/availability.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/thaibma/constants.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/thaibma/history.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/thaibma/thaibma.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/thaibma/utils.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/services/thaibma/yield_curve.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/utils/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/utils/data_fetcher.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/utils/http.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/utils/logging.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/utils/parsing.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/utils/session_manager.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/settfex/utils/youtube_transcript.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/conftest.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/api_surface.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/analyst_consensus.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/dr_profile.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/earnings_call_item.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/highlight_data.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/holiday_calendar.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/index_list_response.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/news_search_response.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/sec_company_match.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/sec_executive_trade.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/sec_executive_trade_report.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/stock_info.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/stock_list_response.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/tfex_series_list_response.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/tfex_trading_statistics.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/tfex_underlying_price.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/golden/model_contract/yield_curve.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/integration/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/integration/conftest.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/integration/test_live_endpoints.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/derive_r59_fixtures.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/derive_th_fixtures.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/README.md +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/capital_indirection_en.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/en_mother_fs_h1_2025.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/en_ptt_56_1.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/en_ptt_56_2.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/en_ptt_fs_h1_2025.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/en_ptt_viewmore_56_2.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/idisc_505_error_page.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000082610_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000092610_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000102610_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000142610_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000152610_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000392610_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000452610_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000522610_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000532609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592000542609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592001242610_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592001262610_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592001352609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592001352610_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592001372609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592001432610_en.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592001432610_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592002622609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592002632609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592002772609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592002782609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592002882609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592002892609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592003112609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592003122609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592003452609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592003462609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592003632609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592003652609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004042609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004052609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004192609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004202609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004532609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004542609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004802609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004812609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592004992609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592005002609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592005172609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592005182609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/detail_592005512609_th.json +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/en_default_20261007.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/th_capped_rcv_20261002.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/th_default_20261007.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/th_edge_rows.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/th_empty.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/th_kcg_txn_202609.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/th_sep_pairs.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/r59/th_spali_txn_202609.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/th_cpall_viewmore_56_1.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/th_mother_fs_h1_2025.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/th_ptt_56_1.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/th_ptt_56_2.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/fixtures_sec/th_ptt_fs_h1_2025.html +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_code_isolation.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_company.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_download.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_download_evidence.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_download_indirection.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_download_result.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_executive_trades.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_financial_report.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_financial_report_accounting.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_financial_report_th.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_listing_transport.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_sec.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_utils.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/sec/test_view_more_slugs.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/index/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/index/test_chart_quotation.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/index/test_composition.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/index/test_index.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/index/test_info.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/index/test_list.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/financial/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/financial/test_financial.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/test_analyst_consensus.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/test_chart_quotation.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/test_dr_indicative_price.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/test_info.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/test_latest_historical_trading.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/stock/test_profile_dr.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_asset_type.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_board_of_director.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_client.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_corporate_action.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_earnings_call.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_historical.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_holiday.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_list.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_listed_symbol.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_news.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_profile_company.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_realtime.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_shareholder.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/set/test_symbol_suggestion.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/test_fault_matrix.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/tfex/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/tfex/test_client.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/tfex/test_historical.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/tfex/test_list.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/tfex/test_realtime.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/tfex/test_trading_statistics.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/tfex/test_underlying_price.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/thaibma/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/thaibma/fixtures.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/thaibma/test_availability.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/thaibma/test_history.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/thaibma/test_thaibma.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/thaibma/test_utils.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/services/thaibma/test_yield_curve.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/test_deprecations.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/test_documented_exceptions.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/test_impersonate_target.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/test_model_contract.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/test_not_found_taxonomy.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/test_public_api_surface.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/test_readme_links.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/test_typing_surface.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/utils/__init__.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_blocked_error.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_data_fetcher.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_formatting.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_http.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_logging.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_logging_neighbourliness.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_parsing.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_session_manager.py +0 -0
- {settfex-0.26.0 → settfex-0.26.1}/tests/utils/test_validation.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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 (
|
|
44
|
-
│ Location: ~/.settfex/cache/
|
|
45
|
-
│ Format: SQLite
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
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
|
|
500
|
-
|
|
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.
|
|
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" }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Session cache for persistent cookie storage
|
|
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
|
|
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
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
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
|
|
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
|
|
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}).
|
|
71
|
-
f"
|
|
72
|
-
f"this process
|
|
73
|
-
f"
|
|
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
|
|
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
|
-
|
|
130
|
-
self.
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
|
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": "
|
|
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"] =
|
|
258
|
+
value["cached_at"] = now
|
|
195
259
|
value["ttl"] = ttl
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
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
|
-
|
|
216
|
-
|
|
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(
|
|
219
|
-
except
|
|
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.
|
|
340
|
+
with self._lock:
|
|
341
|
+
self._db.execute("DELETE FROM sessions")
|
|
255
342
|
logger.info("Cache cleared")
|
|
256
|
-
except
|
|
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
|
|
351
|
+
Dict with ``size`` (bytes of stored values) and ``count`` (number of entries)
|
|
265
352
|
"""
|
|
266
353
|
try:
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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.
|
|
366
|
+
with self._lock:
|
|
367
|
+
self._db.close()
|
|
279
368
|
logger.debug("Cache closed")
|
|
280
|
-
except
|
|
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
|
|
323
|
-
#
|
|
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
|
)
|