fmp-data 2.2.0__tar.gz → 2.2.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 (201) hide show
  1. {fmp_data-2.2.0 → fmp_data-2.2.1}/PKG-INFO +47 -1
  2. {fmp_data-2.2.0 → fmp_data-2.2.1}/README.md +44 -0
  3. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/__init__.py +2 -0
  4. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/base.py +188 -32
  5. fmp_data-2.2.1/fmp_data/cache/__init__.py +48 -0
  6. fmp_data-2.2.1/fmp_data/cache/base.py +56 -0
  7. fmp_data-2.2.1/fmp_data/cache/config.py +58 -0
  8. fmp_data-2.2.1/fmp_data/cache/file.py +83 -0
  9. fmp_data-2.2.1/fmp_data/cache/memory.py +68 -0
  10. fmp_data-2.2.1/fmp_data/cache/redis_backend.py +83 -0
  11. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/company/models.py +46 -16
  12. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/config.py +13 -1
  13. {fmp_data-2.2.0 → fmp_data-2.2.1}/pyproject.toml +6 -0
  14. fmp_data-2.2.1/tests/unit/test_cache_backends.py +250 -0
  15. fmp_data-2.2.1/tests/unit/test_cache_integration.py +302 -0
  16. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_company.py +164 -0
  17. {fmp_data-2.2.0 → fmp_data-2.2.1}/.gitignore +0 -0
  18. {fmp_data-2.2.0 → fmp_data-2.2.1}/LICENSE +0 -0
  19. {fmp_data-2.2.0 → fmp_data-2.2.1}/NOTICE +0 -0
  20. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/alternative/__init__.py +0 -0
  21. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/alternative/async_client.py +0 -0
  22. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/alternative/client.py +0 -0
  23. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/alternative/endpoints.py +0 -0
  24. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/alternative/mapping.py +0 -0
  25. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/alternative/models.py +0 -0
  26. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/alternative/schema.py +0 -0
  27. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/async_client.py +0 -0
  28. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/batch/__init__.py +0 -0
  29. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/batch/_csv_utils.py +0 -0
  30. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/batch/async_client.py +0 -0
  31. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/batch/client.py +0 -0
  32. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/batch/endpoints.py +0 -0
  33. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/batch/mapping.py +0 -0
  34. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/batch/models.py +0 -0
  35. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/client.py +0 -0
  36. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/company/__init__.py +0 -0
  37. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/company/async_client.py +0 -0
  38. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/company/client.py +0 -0
  39. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/company/endpoints.py +0 -0
  40. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/company/hints.py +0 -0
  41. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/company/mapping.py +0 -0
  42. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/company/schema.py +0 -0
  43. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/economics/__init__.py +0 -0
  44. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/economics/async_client.py +0 -0
  45. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/economics/client.py +0 -0
  46. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/economics/endpoints.py +0 -0
  47. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/economics/mapping.py +0 -0
  48. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/economics/models.py +0 -0
  49. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/economics/schema.py +0 -0
  50. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/exceptions.py +0 -0
  51. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/fundamental/__init__.py +0 -0
  52. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/fundamental/async_client.py +0 -0
  53. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/fundamental/client.py +0 -0
  54. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/fundamental/endpoints.py +0 -0
  55. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/fundamental/mapping.py +0 -0
  56. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/fundamental/models.py +0 -0
  57. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/fundamental/schema.py +0 -0
  58. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/helpers.py +0 -0
  59. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/index/__init__.py +0 -0
  60. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/index/async_client.py +0 -0
  61. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/index/client.py +0 -0
  62. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/index/endpoints.py +0 -0
  63. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/index/mapping.py +0 -0
  64. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/index/models.py +0 -0
  65. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/institutional/__init__.py +0 -0
  66. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/institutional/async_client.py +0 -0
  67. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/institutional/client.py +0 -0
  68. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/institutional/endpoints.py +0 -0
  69. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/institutional/mapping.py +0 -0
  70. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/institutional/models.py +0 -0
  71. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/institutional/schema.py +0 -0
  72. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/intelligence/__init__.py +0 -0
  73. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/intelligence/async_client.py +0 -0
  74. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/intelligence/client.py +0 -0
  75. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/intelligence/endpoints.py +0 -0
  76. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/intelligence/mapping.py +0 -0
  77. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/intelligence/models.py +0 -0
  78. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/intelligence/schema.py +0 -0
  79. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/investment/__init__.py +0 -0
  80. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/investment/async_client.py +0 -0
  81. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/investment/client.py +0 -0
  82. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/investment/endpoints.py +0 -0
  83. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/investment/mapping.py +0 -0
  84. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/investment/models.py +0 -0
  85. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/investment/schema.py +0 -0
  86. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/lc/__init__.py +0 -0
  87. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/lc/config.py +0 -0
  88. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/lc/embedding.py +0 -0
  89. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/lc/hints.py +0 -0
  90. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/lc/mapping.py +0 -0
  91. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/lc/models.py +0 -0
  92. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/lc/registry.py +0 -0
  93. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/lc/utils.py +0 -0
  94. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/lc/validation.py +0 -0
  95. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/lc/vector_store.py +0 -0
  96. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/logger.py +0 -0
  97. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/market/__init__.py +0 -0
  98. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/market/async_client.py +0 -0
  99. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/market/client.py +0 -0
  100. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/market/endpoints.py +0 -0
  101. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/market/hints.py +0 -0
  102. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/market/mapping.py +0 -0
  103. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/market/models.py +0 -0
  104. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/market/schema.py +0 -0
  105. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/mcp/__init__.py +0 -0
  106. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/mcp/__main__.py +0 -0
  107. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/mcp/cli.py +0 -0
  108. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/mcp/discovery.py +0 -0
  109. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/mcp/server.py +0 -0
  110. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/mcp/setup.py +0 -0
  111. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/mcp/tool_loader.py +0 -0
  112. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/mcp/tools_manifest.py +0 -0
  113. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/mcp/utils.py +0 -0
  114. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/models.py +0 -0
  115. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/py.typed +0 -0
  116. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/rate_limit.py +0 -0
  117. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/schema.py +0 -0
  118. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/sec/__init__.py +0 -0
  119. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/sec/async_client.py +0 -0
  120. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/sec/client.py +0 -0
  121. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/sec/endpoints.py +0 -0
  122. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/sec/mapping.py +0 -0
  123. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/sec/models.py +0 -0
  124. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/technical/__init__.py +0 -0
  125. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/technical/async_client.py +0 -0
  126. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/technical/client.py +0 -0
  127. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/technical/endpoints.py +0 -0
  128. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/technical/mapping.py +0 -0
  129. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/technical/models.py +0 -0
  130. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/technical/schema.py +0 -0
  131. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/transcripts/__init__.py +0 -0
  132. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/transcripts/async_client.py +0 -0
  133. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/transcripts/client.py +0 -0
  134. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/transcripts/endpoints.py +0 -0
  135. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/transcripts/mapping.py +0 -0
  136. {fmp_data-2.2.0 → fmp_data-2.2.1}/fmp_data/transcripts/models.py +0 -0
  137. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/__init__.py +0 -0
  138. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/examples/__init__.py +0 -0
  139. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/examples/test_examples_smoke.py +0 -0
  140. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/__init__.py +0 -0
  141. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/base.py +0 -0
  142. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/conftest.py +0 -0
  143. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/test_alternative.py +0 -0
  144. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/test_batch.py +0 -0
  145. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/test_company.py +0 -0
  146. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/test_economics.py +0 -0
  147. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/test_fundamental.py +0 -0
  148. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/test_index.py +0 -0
  149. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/test_institutional.py +0 -0
  150. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/test_intelligence.py +0 -0
  151. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/test_investment.py +0 -0
  152. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/test_lc.py +0 -0
  153. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/test_market.py +0 -0
  154. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/test_sec.py +0 -0
  155. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/test_technical.py +0 -0
  156. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/integration/test_transcripts.py +0 -0
  157. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/__init__.py +0 -0
  158. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/conftest.py +0 -0
  159. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/lc/__init__.py +0 -0
  160. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/lc/conftest.py +0 -0
  161. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/lc/test_config.py +0 -0
  162. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/lc/test_embedding.py +0 -0
  163. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/lc/test_endpoint_based_rules.py +0 -0
  164. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/lc/test_imports.py +0 -0
  165. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/lc/test_mapping.py +0 -0
  166. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/lc/test_models.py +0 -0
  167. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/lc/test_utils.py +0 -0
  168. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/lc/test_validation_registry.py +0 -0
  169. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/lc/test_vector_store.py +0 -0
  170. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_alternative.py +0 -0
  171. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_alternative_coverage.py +0 -0
  172. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_async_clients.py +0 -0
  173. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_base.py +0 -0
  174. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_base_async.py +0 -0
  175. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_base_coverage.py +0 -0
  176. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_batch.py +0 -0
  177. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_cassette_contracts.py +0 -0
  178. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_client.py +0 -0
  179. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_company_coverage.py +0 -0
  180. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_config.py +0 -0
  181. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_economics.py +0 -0
  182. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_exceptions.py +0 -0
  183. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_fundamental.py +0 -0
  184. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_helpers.py +0 -0
  185. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_index.py +0 -0
  186. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_institutional.py +0 -0
  187. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_intelligence.py +0 -0
  188. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_investment.py +0 -0
  189. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_logger.py +0 -0
  190. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_market.py +0 -0
  191. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_mcp.py +0 -0
  192. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_mcp_cli.py +0 -0
  193. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_models.py +0 -0
  194. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_rate_limit.py +0 -0
  195. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_rate_limit_async.py +0 -0
  196. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_rate_limit_coverage.py +0 -0
  197. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_sec.py +0 -0
  198. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_technical.py +0 -0
  199. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_technical_coverage.py +0 -0
  200. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_transcripts.py +0 -0
  201. {fmp_data-2.2.0 → fmp_data-2.2.1}/tests/unit/test_vcr_sanitization.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fmp-data
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: Python client for the Financial Modeling Prep API
5
5
  Project-URL: Homepage, https://github.com/MehdiZare/fmp-data
6
6
  Project-URL: Repository, https://github.com/MehdiZare/fmp-data
@@ -30,6 +30,8 @@ Requires-Python: <3.15,>=3.10
30
30
  Requires-Dist: httpx>=0.28.1
31
31
  Requires-Dist: pydantic>=2.12.5
32
32
  Requires-Dist: tenacity>=9.1.2
33
+ Provides-Extra: cache-redis
34
+ Requires-Dist: redis>=5.0.0; extra == 'cache-redis'
33
35
  Provides-Extra: dev
34
36
  Requires-Dist: bandit[toml]>=1.9.3; extra == 'dev'
35
37
  Requires-Dist: coverage>=7.13.1; extra == 'dev'
@@ -95,6 +97,7 @@ This project uses UV as the primary package management tool for several key bene
95
97
  - Built-in rate limiting
96
98
  - Comprehensive logging
97
99
  - Async support
100
+ - Opt-in response caching with memory, file, and Redis backends
98
101
  - Type hints and validation with Pydantic
99
102
  - Automatic retries with exponential backoff
100
103
  - 85%+ test coverage with comprehensive test suite
@@ -121,6 +124,9 @@ To use this library, you'll need an API key from Financial Modeling Prep (FMP).
121
124
  # Basic installation
122
125
  uv pip install fmp-data
123
126
 
127
+ # With Redis cache backend support
128
+ uv pip install "fmp-data[cache-redis]"
129
+
124
130
  # With Langchain integration
125
131
  uv pip install "fmp-data[langchain]"
126
132
 
@@ -139,12 +145,52 @@ LangChain integration requires LangChain v1 (`langchain-core`, `langchain-openai
139
145
  # Basic installation
140
146
  pip install fmp-data
141
147
 
148
+ # With Redis cache backend support
149
+ pip install "fmp-data[cache-redis]"
150
+
142
151
  # With extras
143
152
  pip install fmp-data[langchain]
144
153
  pip install fmp-data[mcp]
145
154
  pip install fmp-data[langchain,mcp]
146
155
  ```
147
156
 
157
+ ## Response Caching
158
+
159
+ Response caching is optional and works for both sync and async clients.
160
+
161
+ ```python
162
+ from fmp_data import CacheConfig, ClientConfig, FMPDataClient
163
+
164
+ config = ClientConfig(
165
+ api_key="YOUR_FMP_API_KEY", # pragma: allowlist secret
166
+ cache=CacheConfig(
167
+ backend="memory",
168
+ default_ttl=300,
169
+ ttl_overrides={"get_quote": 30},
170
+ ),
171
+ )
172
+
173
+ with FMPDataClient(config=config) as client:
174
+ quote = client.company.get_quote("AAPL")
175
+ fresh_quote = client.company.get_quote("AAPL", force_refresh=True)
176
+ ```
177
+
178
+ You can also configure caching from the environment:
179
+
180
+ ```bash
181
+ export FMP_CACHE_ENABLED=true
182
+ export FMP_CACHE_BACKEND=file
183
+ export FMP_CACHE_TTL=300
184
+ export FMP_CACHE_DIR=.cache/fmp-data
185
+ ```
186
+
187
+ For Redis-backed caching, install the optional extra and set:
188
+
189
+ ```bash
190
+ export FMP_CACHE_BACKEND=redis
191
+ export FMP_CACHE_REDIS_URL=redis://localhost:6379/0
192
+ ```
193
+
148
194
  ## MCP Server (Claude Desktop Integration)
149
195
 
150
196
  Model Context Protocol (MCP) server provides financial data access through a standardized protocol, enabling Claude Desktop to query FMP data seamlessly.
@@ -27,6 +27,7 @@ This project uses UV as the primary package management tool for several key bene
27
27
  - Built-in rate limiting
28
28
  - Comprehensive logging
29
29
  - Async support
30
+ - Opt-in response caching with memory, file, and Redis backends
30
31
  - Type hints and validation with Pydantic
31
32
  - Automatic retries with exponential backoff
32
33
  - 85%+ test coverage with comprehensive test suite
@@ -53,6 +54,9 @@ To use this library, you'll need an API key from Financial Modeling Prep (FMP).
53
54
  # Basic installation
54
55
  uv pip install fmp-data
55
56
 
57
+ # With Redis cache backend support
58
+ uv pip install "fmp-data[cache-redis]"
59
+
56
60
  # With Langchain integration
57
61
  uv pip install "fmp-data[langchain]"
58
62
 
@@ -71,12 +75,52 @@ LangChain integration requires LangChain v1 (`langchain-core`, `langchain-openai
71
75
  # Basic installation
72
76
  pip install fmp-data
73
77
 
78
+ # With Redis cache backend support
79
+ pip install "fmp-data[cache-redis]"
80
+
74
81
  # With extras
75
82
  pip install fmp-data[langchain]
76
83
  pip install fmp-data[mcp]
77
84
  pip install fmp-data[langchain,mcp]
78
85
  ```
79
86
 
87
+ ## Response Caching
88
+
89
+ Response caching is optional and works for both sync and async clients.
90
+
91
+ ```python
92
+ from fmp_data import CacheConfig, ClientConfig, FMPDataClient
93
+
94
+ config = ClientConfig(
95
+ api_key="YOUR_FMP_API_KEY", # pragma: allowlist secret
96
+ cache=CacheConfig(
97
+ backend="memory",
98
+ default_ttl=300,
99
+ ttl_overrides={"get_quote": 30},
100
+ ),
101
+ )
102
+
103
+ with FMPDataClient(config=config) as client:
104
+ quote = client.company.get_quote("AAPL")
105
+ fresh_quote = client.company.get_quote("AAPL", force_refresh=True)
106
+ ```
107
+
108
+ You can also configure caching from the environment:
109
+
110
+ ```bash
111
+ export FMP_CACHE_ENABLED=true
112
+ export FMP_CACHE_BACKEND=file
113
+ export FMP_CACHE_TTL=300
114
+ export FMP_CACHE_DIR=.cache/fmp-data
115
+ ```
116
+
117
+ For Redis-backed caching, install the optional extra and set:
118
+
119
+ ```bash
120
+ export FMP_CACHE_BACKEND=redis
121
+ export FMP_CACHE_REDIS_URL=redis://localhost:6379/0
122
+ ```
123
+
80
124
  ## MCP Server (Claude Desktop Integration)
81
125
 
82
126
  Model Context Protocol (MCP) server provides financial data access through a standardized protocol, enabling Claude Desktop to query FMP data seamlessly.
@@ -19,6 +19,7 @@ import types as _types
19
19
  from typing import Any
20
20
 
21
21
  from fmp_data.async_client import AsyncFMPDataClient
22
+ from fmp_data.cache.config import CacheConfig
22
23
  from fmp_data.client import FMPDataClient
23
24
  from fmp_data.config import (
24
25
  ClientConfig,
@@ -48,6 +49,7 @@ except ImportError:
48
49
  __all__ = [
49
50
  "AsyncFMPDataClient",
50
51
  "AuthenticationError",
52
+ "CacheConfig",
51
53
  "ClientConfig",
52
54
  "ConfigError",
53
55
  "FMPDataClient",
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  import asyncio
5
5
  from contextvars import ContextVar
6
+ import copy
6
7
  import json
7
8
  import logging
8
9
  import time
@@ -22,6 +23,7 @@ from tenacity import (
22
23
  wait_exponential,
23
24
  )
24
25
 
26
+ from fmp_data.cache.base import CacheBackend
25
27
  from fmp_data.config import ClientConfig
26
28
  from fmp_data.exceptions import (
27
29
  AuthenticationError,
@@ -83,6 +85,22 @@ class BaseClient:
83
85
  # Async client (lazily initialized)
84
86
  self._async_client: httpx.AsyncClient | None = None
85
87
 
88
+ # Response cache (optional)
89
+ self._cache: CacheBackend | None = None
90
+ cache_cfg = getattr(config, "cache", None)
91
+ if cache_cfg is not None and getattr(cache_cfg, "enabled", False) is True:
92
+ from fmp_data.cache import create_backend
93
+ from fmp_data.cache.config import CacheConfig
94
+
95
+ if isinstance(cache_cfg, CacheConfig):
96
+ self._cache = create_backend(cache_cfg)
97
+ self._cache_ttl_overrides = cache_cfg.ttl_overrides
98
+ self._cache_default_ttl = cache_cfg.default_ttl
99
+ self.logger.info(
100
+ "Response caching enabled (backend=%s)",
101
+ cache_cfg.backend,
102
+ )
103
+
86
104
  def _setup_http_client(self) -> None:
87
105
  """
88
106
  Setup HTTP client with default configuration.
@@ -137,6 +155,85 @@ class BaseClient:
137
155
  # Also close sync client
138
156
  self.close()
139
157
 
158
+ @staticmethod
159
+ def _build_cache_key(endpoint_name: str, params: dict[str, Any]) -> str:
160
+ """Build a deterministic cache key from endpoint name and params.
161
+
162
+ Excludes 'apikey' to avoid leaking secrets in keys.
163
+ Uses urllib.parse.urlencode to properly escape special characters
164
+ and prevent key collisions from values containing '&' or '='.
165
+ """
166
+ import hashlib
167
+ import urllib.parse
168
+
169
+ filtered = {k: v for k, v in sorted(params.items()) if k != "apikey"}
170
+ params_str = urllib.parse.urlencode(filtered, doseq=True)
171
+
172
+ # codeql[py/weak-sensitive-data-hashing]
173
+ # This hash is only used to shorten cache keys, not for credential storage.
174
+ params_hash = hashlib.sha256(
175
+ params_str.encode(), usedforsecurity=False
176
+ ).hexdigest()[:16]
177
+ return f"{endpoint_name}:{params_hash}"
178
+
179
+ def _get_cache_ttl(self, endpoint_name: str) -> int:
180
+ """Get TTL for a given endpoint, checking overrides first."""
181
+ if not hasattr(self, "_cache_ttl_overrides"):
182
+ return 300
183
+ return self._cache_ttl_overrides.get(endpoint_name, self._cache_default_ttl)
184
+
185
+ def _cache_get(
186
+ self, endpoint_name: str, cache_key: str
187
+ ) -> dict[str, Any] | list[Any] | None:
188
+ """Read from cache, returning None on any failure."""
189
+ assert self._cache is not None
190
+ try:
191
+ cached = self._cache.get(cache_key)
192
+ return None if cached is None else copy.deepcopy(cached)
193
+ except Exception:
194
+ self.logger.warning(
195
+ "Cache read failed for %s", endpoint_name, exc_info=True
196
+ )
197
+ return None
198
+
199
+ async def _cache_aget(
200
+ self, endpoint_name: str, cache_key: str
201
+ ) -> dict[str, Any] | list[Any] | None:
202
+ """Async read from cache, returning None on any failure."""
203
+ assert self._cache is not None
204
+ try:
205
+ cached = await self._cache.aget(cache_key)
206
+ return None if cached is None else copy.deepcopy(cached)
207
+ except Exception:
208
+ self.logger.warning(
209
+ "Cache read failed for %s", endpoint_name, exc_info=True
210
+ )
211
+ return None
212
+
213
+ def _cache_set(
214
+ self, endpoint_name: str, cache_key: str, data: Any, ttl: int
215
+ ) -> None:
216
+ """Write to cache, logging on failure."""
217
+ assert self._cache is not None
218
+ try:
219
+ self._cache.set(cache_key, copy.deepcopy(data), ttl)
220
+ except Exception:
221
+ self.logger.warning(
222
+ "Cache write failed for %s", endpoint_name, exc_info=True
223
+ )
224
+
225
+ async def _cache_aset(
226
+ self, endpoint_name: str, cache_key: str, data: Any, ttl: int
227
+ ) -> None:
228
+ """Async write to cache, logging on failure."""
229
+ assert self._cache is not None
230
+ try:
231
+ await self._cache.aset(cache_key, copy.deepcopy(data), ttl)
232
+ except Exception:
233
+ self.logger.warning(
234
+ "Cache write failed for %s", endpoint_name, exc_info=True
235
+ )
236
+
140
237
  def _handle_rate_limit(self, wait_time: float) -> None:
141
238
  """
142
239
  Handle rate limiting by waiting or raising an exception based on retry count.
@@ -225,18 +322,12 @@ class BaseClient:
225
322
  Returns:
226
323
  Either a single Pydantic model of type T or a list of T.
227
324
  """
228
- # Check rate limit before making request
229
- if not self._rate_limiter.should_allow_request():
230
- wait_time = self._rate_limiter.get_wait_time()
231
- self._handle_rate_limit(wait_time)
232
-
233
325
  request_start = time.perf_counter()
234
326
  status_code = 0
235
327
  success = False
328
+ force_refresh = kwargs.pop("force_refresh", False)
236
329
 
237
330
  try:
238
- self._rate_limiter.record_request()
239
-
240
331
  # Validate and process parameters
241
332
  validated_params = endpoint.validate_params(
242
333
  kwargs,
@@ -250,6 +341,35 @@ class BaseClient:
250
341
  query_params = endpoint.get_query_params(validated_params)
251
342
  query_params["apikey"] = self.config.api_key
252
343
 
344
+ # Check cache before rate limiting — cache hits are free
345
+ cache_key: str | None = None
346
+ if (
347
+ self._cache is not None
348
+ and not force_refresh
349
+ and endpoint.response_model is not bytes
350
+ ):
351
+ cache_key = self._build_cache_key(endpoint.name, query_params)
352
+ cached = self._cache_get(endpoint.name, cache_key)
353
+ if cached is not None:
354
+ self.logger.debug(
355
+ "Cache hit for %s",
356
+ endpoint.name,
357
+ extra={"endpoint": endpoint.name, "cache_key": cache_key},
358
+ )
359
+ success = True
360
+ return self._process_response(
361
+ endpoint,
362
+ cached,
363
+ validation_mode=self.config.validation_mode,
364
+ )
365
+
366
+ # Check rate limit only when making an actual HTTP request
367
+ if not self._rate_limiter.should_allow_request():
368
+ wait_time = self._rate_limiter.get_wait_time()
369
+ self._handle_rate_limit(wait_time)
370
+
371
+ self._rate_limiter.record_request()
372
+
253
373
  self.logger.debug(
254
374
  f"Making request to {endpoint.name}",
255
375
  extra={
@@ -277,6 +397,12 @@ class BaseClient:
277
397
  validation_mode=self.config.validation_mode,
278
398
  )
279
399
  success = True
400
+
401
+ # Store in cache
402
+ if self._cache is not None and cache_key is not None:
403
+ ttl = self._get_cache_ttl(endpoint.name)
404
+ self._cache_set(endpoint.name, cache_key, data, ttl)
405
+
280
406
  return result
281
407
  finally:
282
408
  response.close()
@@ -697,32 +823,9 @@ class BaseClient:
697
823
  Returns:
698
824
  Either a single Pydantic model of type T or a list of T.
699
825
  """
700
- # Check rate limit using async rate limiter (concurrency-safe)
701
- if not await self._async_rate_limiter.should_allow_request():
702
- wait_time = self._async_rate_limiter.get_wait_time()
703
- current_count = _rate_limit_retry_count.get() + 1
704
- _rate_limit_retry_count.set(current_count)
705
-
706
- if current_count > self.max_rate_limit_retries:
707
- _rate_limit_retry_count.set(0)
708
- raise RateLimitError(
709
- f"Rate limit exceeded after "
710
- f"{self.max_rate_limit_retries} retries. "
711
- f"Please wait {wait_time:.1f} seconds",
712
- retry_after=wait_time,
713
- )
714
-
715
- self.logger.warning(
716
- f"Rate limit reached "
717
- f"(attempt {current_count}/"
718
- f"{self.max_rate_limit_retries}), "
719
- f"waiting {wait_time:.1f} seconds before retrying"
720
- )
721
- await asyncio.sleep(wait_time)
826
+ force_refresh = kwargs.pop("force_refresh", False)
722
827
 
723
828
  try:
724
- await self._async_rate_limiter.record_request()
725
-
726
829
  # Validate and process parameters
727
830
  validated_params = endpoint.validate_params(
728
831
  kwargs,
@@ -736,6 +839,52 @@ class BaseClient:
736
839
  query_params = endpoint.get_query_params(validated_params)
737
840
  query_params["apikey"] = self.config.api_key
738
841
 
842
+ # Check cache before rate limiting — cache hits are free
843
+ cache_key: str | None = None
844
+ if (
845
+ self._cache is not None
846
+ and not force_refresh
847
+ and endpoint.response_model is not bytes
848
+ ):
849
+ cache_key = self._build_cache_key(endpoint.name, query_params)
850
+ cached = await self._cache_aget(endpoint.name, cache_key)
851
+ if cached is not None:
852
+ self.logger.debug(
853
+ "Cache hit for %s",
854
+ endpoint.name,
855
+ extra={"endpoint": endpoint.name, "cache_key": cache_key},
856
+ )
857
+ return self._process_response(
858
+ endpoint,
859
+ cached,
860
+ validation_mode=self.config.validation_mode,
861
+ )
862
+
863
+ # Check rate limit only when making an actual HTTP request
864
+ if not await self._async_rate_limiter.should_allow_request():
865
+ wait_time = self._async_rate_limiter.get_wait_time()
866
+ current_count = _rate_limit_retry_count.get() + 1
867
+ _rate_limit_retry_count.set(current_count)
868
+
869
+ if current_count > self.max_rate_limit_retries:
870
+ _rate_limit_retry_count.set(0)
871
+ raise RateLimitError(
872
+ f"Rate limit exceeded after "
873
+ f"{self.max_rate_limit_retries} retries. "
874
+ f"Please wait {wait_time:.1f} seconds",
875
+ retry_after=wait_time,
876
+ )
877
+
878
+ self.logger.warning(
879
+ f"Rate limit reached "
880
+ f"(attempt {current_count}/"
881
+ f"{self.max_rate_limit_retries}), "
882
+ f"waiting {wait_time:.1f} seconds before retrying"
883
+ )
884
+ await asyncio.sleep(wait_time)
885
+
886
+ await self._async_rate_limiter.record_request()
887
+
739
888
  self.logger.debug(
740
889
  f"Making async request to {endpoint.name}",
741
890
  extra={
@@ -757,11 +906,18 @@ class BaseClient:
757
906
  data = response.content
758
907
  else:
759
908
  data = self.handle_response(endpoint, response)
760
- return self._process_response(
909
+ result = self._process_response(
761
910
  endpoint,
762
911
  data,
763
912
  validation_mode=self.config.validation_mode,
764
913
  )
914
+
915
+ # Store in cache
916
+ if self._cache is not None and cache_key is not None:
917
+ ttl = self._get_cache_ttl(endpoint.name)
918
+ await self._cache_aset(endpoint.name, cache_key, data, ttl)
919
+
920
+ return result
765
921
  finally:
766
922
  await response.aclose()
767
923
 
@@ -0,0 +1,48 @@
1
+ """Response caching for FMP Data API client.
2
+
3
+ Provides pluggable cache backends (memory, file, redis) to reduce
4
+ redundant API calls and improve latency for repeated queries.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from fmp_data.cache.base import CacheBackend
10
+ from fmp_data.cache.config import CacheConfig
11
+ from fmp_data.cache.file import FileCache
12
+ from fmp_data.cache.memory import MemoryCache
13
+
14
+ __all__ = [
15
+ "CacheBackend",
16
+ "CacheConfig",
17
+ "FileCache",
18
+ "MemoryCache",
19
+ "create_backend",
20
+ ]
21
+
22
+
23
+ def create_backend(config: CacheConfig) -> CacheBackend:
24
+ """Instantiate a cache backend from configuration.
25
+
26
+ Args:
27
+ config: Cache configuration
28
+
29
+ Returns:
30
+ A ready-to-use CacheBackend instance
31
+
32
+ Raises:
33
+ ValueError: If the backend type is unknown
34
+ ImportError: If redis backend is requested but redis is not installed
35
+ """
36
+ if config.backend == "memory":
37
+ return MemoryCache(default_ttl=config.default_ttl)
38
+ if config.backend == "file":
39
+ from pathlib import Path
40
+
41
+ cache_dir = config.cache_dir or Path.home() / ".cache" / "fmp-data"
42
+ return FileCache(cache_dir=cache_dir, default_ttl=config.default_ttl)
43
+ if config.backend == "redis":
44
+ from fmp_data.cache.redis_backend import RedisCache
45
+
46
+ redis_url = config.redis_url or "redis://localhost:6379/0"
47
+ return RedisCache(redis_url=redis_url, default_ttl=config.default_ttl)
48
+ raise ValueError(f"Unknown cache backend: {config.backend!r}")
@@ -0,0 +1,56 @@
1
+ """Abstract cache backend interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ import asyncio
7
+ from typing import Any
8
+
9
+
10
+ class CacheBackend(ABC):
11
+ """Abstract base class for cache backends."""
12
+
13
+ @abstractmethod
14
+ def get(self, key: str) -> Any | None:
15
+ """Retrieve a value from the cache.
16
+
17
+ Args:
18
+ key: Cache key
19
+
20
+ Returns:
21
+ Cached value or None if not found / expired
22
+ """
23
+
24
+ @abstractmethod
25
+ def set(self, key: str, value: Any, ttl: int | None = None) -> None:
26
+ """Store a value in the cache.
27
+
28
+ Args:
29
+ key: Cache key
30
+ value: Value to cache
31
+ ttl: Time-to-live in seconds (None = use backend default)
32
+ """
33
+
34
+ @abstractmethod
35
+ def delete(self, key: str) -> None:
36
+ """Remove a key from the cache."""
37
+
38
+ @abstractmethod
39
+ def clear(self) -> None:
40
+ """Remove all entries from the cache."""
41
+
42
+ async def aget(self, key: str) -> Any | None:
43
+ """Async variant of get. Offloads to a thread by default."""
44
+ return await asyncio.to_thread(self.get, key)
45
+
46
+ async def aset(self, key: str, value: Any, ttl: int | None = None) -> None:
47
+ """Async variant of set. Offloads to a thread by default."""
48
+ await asyncio.to_thread(self.set, key, value, ttl)
49
+
50
+ async def adelete(self, key: str) -> None:
51
+ """Async variant of delete. Offloads to a thread by default."""
52
+ await asyncio.to_thread(self.delete, key)
53
+
54
+ async def aclear(self) -> None:
55
+ """Async variant of clear. Offloads to a thread by default."""
56
+ await asyncio.to_thread(self.clear)
@@ -0,0 +1,58 @@
1
+ """Cache configuration models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Literal
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class CacheConfig(BaseModel):
13
+ """Configuration for the response cache."""
14
+
15
+ enabled: bool = Field(default=True, description="Whether caching is enabled")
16
+ backend: Literal["memory", "file", "redis"] = Field(
17
+ default="memory", description="Cache backend type"
18
+ )
19
+ default_ttl: int = Field(default=300, gt=0, description="Default TTL in seconds")
20
+ ttl_overrides: dict[str, int] = Field(
21
+ default_factory=dict,
22
+ description="Per-endpoint TTL overrides (endpoint_name -> seconds)",
23
+ )
24
+ cache_dir: Path | None = Field(
25
+ default=None, description="Directory for file-based cache"
26
+ )
27
+ redis_url: str | None = Field(default=None, description="Redis connection URL")
28
+
29
+ @classmethod
30
+ def from_env(cls) -> CacheConfig | None:
31
+ """Create cache config from environment variables.
32
+
33
+ Returns None if FMP_CACHE_ENABLED is not set or is 'false'.
34
+ """
35
+ enabled_str = os.getenv("FMP_CACHE_ENABLED", "").lower()
36
+ if enabled_str not in ("true", "1", "yes"):
37
+ return None
38
+
39
+ cache_dir_str = os.getenv("FMP_CACHE_DIR")
40
+ cache_dir = Path(cache_dir_str) if cache_dir_str else None
41
+
42
+ try:
43
+ ttl = int(os.getenv("FMP_CACHE_TTL", "300"))
44
+ ttl = max(1, ttl)
45
+ except (ValueError, TypeError):
46
+ ttl = 300
47
+
48
+ backend_str = os.getenv("FMP_CACHE_BACKEND", "memory")
49
+ if backend_str not in ("memory", "file", "redis"):
50
+ backend_str = "memory"
51
+
52
+ return cls(
53
+ enabled=True,
54
+ backend=backend_str,
55
+ default_ttl=ttl,
56
+ cache_dir=cache_dir,
57
+ redis_url=os.getenv("FMP_CACHE_REDIS_URL"),
58
+ )