classifyre-cli 0.5.4__tar.gz → 0.5.5__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 (353) hide show
  1. classifyre_cli-0.5.5/.turbo/turbo-build.log +3 -0
  2. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/PKG-INFO +2 -2
  3. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/package.json +1 -1
  4. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/pyproject.toml +7 -2
  5. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/custom/runners/_llm.py +143 -4
  6. classifyre_cli-0.5.5/src/detectors/errors.py +19 -0
  7. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/main.py +20 -0
  8. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/models/generated_detectors.py +59 -0
  9. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/models/generated_input.py +85 -37
  10. classifyre_cli-0.5.5/src/notebook/cohort.py +194 -0
  11. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/notebook/execute.py +2 -0
  12. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/notebook/sdk.py +267 -1
  13. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/outputs/rest.py +10 -2
  14. classifyre_cli-0.5.5/src/pipeline/detector_breaker.py +207 -0
  15. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/pipeline/detector_pipeline.py +127 -12
  16. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/pipeline/parsed_content_provider.py +3 -0
  17. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/pipeline/scan_cache.py +15 -1
  18. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/base.py +10 -0
  19. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/custom/runner.py +46 -1
  20. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/custom/source.py +190 -16
  21. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/custom/test_llm_runner.py +213 -1
  22. classifyre_cli-0.5.5/tests/pipeline/test_detector_breaker.py +342 -0
  23. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/pipeline/test_detector_pipeline.py +36 -0
  24. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_custom_source.py +336 -1
  25. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_dependency_groups.py +17 -0
  26. classifyre_cli-0.5.5/tests/test_notebook_asset_extract.py +46 -0
  27. classifyre_cli-0.5.5/tests/test_notebook_cohort.py +125 -0
  28. classifyre_cli-0.5.5/tests/test_notebook_query_assets.py +105 -0
  29. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_scan_cache.py +32 -0
  30. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/uv.lock +293 -827
  31. classifyre_cli-0.5.4/.turbo/turbo-build.log +0 -3
  32. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/.gitignore +0 -0
  33. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/.python-version +0 -0
  34. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/README.md +0 -0
  35. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/main.py +0 -0
  36. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/scripts/generate_models.py +0 -0
  37. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/scripts/generate_runtime_packages.py +0 -0
  38. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/scripts/generate_sdk_completions.py +0 -0
  39. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/__init__.py +0 -0
  40. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/augmentation/__init__.py +0 -0
  41. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/augmentation/contract.py +0 -0
  42. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/augmentation/preview.py +0 -0
  43. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/augmentation/runner.py +0 -0
  44. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/augmentation/sdk.py +0 -0
  45. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/augmentation/session.py +0 -0
  46. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/config.py +0 -0
  47. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/__init__.py +0 -0
  48. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/base.py +0 -0
  49. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/broken_links/__init__.py +0 -0
  50. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/broken_links/detector.py +0 -0
  51. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/config.py +0 -0
  52. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/content/__init__.py +0 -0
  53. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/custom/__init__.py +0 -0
  54. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/custom/detector.py +0 -0
  55. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/custom/extractor.py +0 -0
  56. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/custom/runners/__init__.py +0 -0
  57. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/custom/runners/_base.py +0 -0
  58. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/custom/runners/_factory.py +0 -0
  59. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/custom/runners/_gliner2.py +0 -0
  60. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/custom/runners/_image_classification.py +0 -0
  61. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/custom/runners/_object_detection.py +0 -0
  62. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/custom/runners/_regex.py +0 -0
  63. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/custom/runners/_tag.py +0 -0
  64. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/custom/runners/_text_classification.py +0 -0
  65. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/custom/trainer.py +0 -0
  66. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/dependencies.py +0 -0
  67. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/engine_version.py +0 -0
  68. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/pii/__init__.py +0 -0
  69. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/pii/detector.py +0 -0
  70. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/secrets/__init__.py +0 -0
  71. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/secrets/detector.py +0 -0
  72. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/threat/__init__.py +0 -0
  73. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/threat/code_security_detector.py +0 -0
  74. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/detectors/threat/yara_detector.py +0 -0
  75. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/file_evaluation/__init__.py +0 -0
  76. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/file_evaluation/runner.py +0 -0
  77. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/graph/__init__.py +0 -0
  78. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/graph/edges.py +0 -0
  79. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/models/generated_single_asset_scan_results.py +0 -0
  80. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/notebook/__init__.py +0 -0
  81. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/notebook/child.py +0 -0
  82. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/notebook/cli.py +0 -0
  83. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/notebook/contract.py +0 -0
  84. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/notebook/files.py +0 -0
  85. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/notebook/groups.py +0 -0
  86. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/notebook/outputs.py +0 -0
  87. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/notebook/packages.py +0 -0
  88. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/notebook/protocol.py +0 -0
  89. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/notebook/redact.py +0 -0
  90. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/notebook/scaffold.py +0 -0
  91. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/notebook/serialize.py +0 -0
  92. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/outputs/__init__.py +0 -0
  93. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/outputs/base.py +0 -0
  94. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/outputs/console.py +0 -0
  95. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/outputs/factory.py +0 -0
  96. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/outputs/file.py +0 -0
  97. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/pipeline/__init__.py +0 -0
  98. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/pipeline/content_provider.py +0 -0
  99. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/pipeline/payload_window.py +0 -0
  100. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/pipeline/text_artifact.py +0 -0
  101. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/pipeline/worker_pool.py +0 -0
  102. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/__init__.py +0 -0
  103. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/asset_metadata.py +0 -0
  104. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/atlassian_common.py +0 -0
  105. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/azure_blob_storage/__init__.py +0 -0
  106. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/azure_blob_storage/source.py +0 -0
  107. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/confluence/__init__.py +0 -0
  108. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/confluence/source.py +0 -0
  109. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/custom/__init__.py +0 -0
  110. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/custom/env.py +0 -0
  111. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/databricks/__init__.py +0 -0
  112. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/databricks/source.py +0 -0
  113. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/delta_lake/__init__.py +0 -0
  114. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/delta_lake/source.py +0 -0
  115. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/dependencies.py +0 -0
  116. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/dropbox/__init__.py +0 -0
  117. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/dropbox/auth.py +0 -0
  118. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/dropbox/source.py +0 -0
  119. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/elasticsearch/__init__.py +0 -0
  120. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/elasticsearch/source.py +0 -0
  121. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/email/__init__.py +0 -0
  122. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/email/source.py +0 -0
  123. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/git/__init__.py +0 -0
  124. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/git/source.py +0 -0
  125. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/google_cloud_storage/__init__.py +0 -0
  126. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/google_cloud_storage/source.py +0 -0
  127. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/google_workspace/__init__.py +0 -0
  128. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/google_workspace/source.py +0 -0
  129. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/hive/__init__.py +0 -0
  130. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/hive/source.py +0 -0
  131. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/hugging_face/__init__.py +0 -0
  132. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/hugging_face/source.py +0 -0
  133. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/iceberg/__init__.py +0 -0
  134. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/iceberg/source.py +0 -0
  135. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/jira/__init__.py +0 -0
  136. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/jira/source.py +0 -0
  137. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/kafka/__init__.py +0 -0
  138. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/kafka/rest.py +0 -0
  139. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/kafka/source.py +0 -0
  140. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/lakehouse_base.py +0 -0
  141. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/local_folder/__init__.py +0 -0
  142. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/local_folder/source.py +0 -0
  143. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/meilisearch/__init__.py +0 -0
  144. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/meilisearch/source.py +0 -0
  145. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/microsoft_365/__init__.py +0 -0
  146. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/microsoft_365/source.py +0 -0
  147. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/mongodb/__init__.py +0 -0
  148. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/mongodb/source.py +0 -0
  149. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/mssql/__init__.py +0 -0
  150. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/mssql/source.py +0 -0
  151. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/mysql/__init__.py +0 -0
  152. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/mysql/source.py +0 -0
  153. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/neo4j/__init__.py +0 -0
  154. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/neo4j/source.py +0 -0
  155. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/notion/__init__.py +0 -0
  156. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/notion/client.py +0 -0
  157. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/notion/source.py +0 -0
  158. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/object_storage/base.py +0 -0
  159. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/opensearch/__init__.py +0 -0
  160. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/opensearch/source.py +0 -0
  161. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/oracle/__init__.py +0 -0
  162. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/oracle/source.py +0 -0
  163. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/postgresql/__init__.py +0 -0
  164. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/postgresql/source.py +0 -0
  165. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/powerbi/__init__.py +0 -0
  166. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/powerbi/source.py +0 -0
  167. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/recipe_normalizer.py +0 -0
  168. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/reddit/__init__.py +0 -0
  169. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/reddit/source.py +0 -0
  170. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/s3_client.py +0 -0
  171. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/s3_compatible_storage/README.md +0 -0
  172. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/s3_compatible_storage/__init__.py +0 -0
  173. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/s3_compatible_storage/source.py +0 -0
  174. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/sandbox/__init__.py +0 -0
  175. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/sandbox/source.py +0 -0
  176. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/search_engine_base.py +0 -0
  177. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/servicedesk/__init__.py +0 -0
  178. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/servicedesk/source.py +0 -0
  179. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/slack/__init__.py +0 -0
  180. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/slack/source.py +0 -0
  181. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/snowflake/__init__.py +0 -0
  182. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/snowflake/source.py +0 -0
  183. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/sqlite/__init__.py +0 -0
  184. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/sqlite/source.py +0 -0
  185. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/tableau/__init__.py +0 -0
  186. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/tableau/source.py +0 -0
  187. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/tabular_base.py +0 -0
  188. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/tabular_utils.py +0 -0
  189. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/wordpress/__init__.py +0 -0
  190. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/wordpress/source.py +0 -0
  191. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/youtube/__init__.py +0 -0
  192. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/sources/youtube/source.py +0 -0
  193. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/telemetry.py +0 -0
  194. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/__init__.py +0 -0
  195. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/archive_extraction.py +0 -0
  196. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/content_extraction.py +0 -0
  197. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/dependency_groups.py +0 -0
  198. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/embedded_files.py +0 -0
  199. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/external_video.py +0 -0
  200. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/file_metadata.py +0 -0
  201. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/file_parser.py +0 -0
  202. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/file_to_images.py +0 -0
  203. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/hashing.py +0 -0
  204. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/legacy_office.py +0 -0
  205. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/payload.py +0 -0
  206. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/range_reader.py +0 -0
  207. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/resources.py +0 -0
  208. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/source_files.py +0 -0
  209. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/sql_lineage.py +0 -0
  210. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/torch_runtime.py +0 -0
  211. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/transcription.py +0 -0
  212. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/urn.py +0 -0
  213. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/uv_sync.py +0 -0
  214. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/validation.py +0 -0
  215. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/src/utils/video_processing.py +0 -0
  216. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/__init__.py +0 -0
  217. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/_lakehouse_fakes.py +0 -0
  218. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/conftest.py +0 -0
  219. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/__init__.py +0 -0
  220. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
  221. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/conftest.py +0 -0
  222. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/content/__init__.py +0 -0
  223. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/custom/__init__.py +0 -0
  224. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/custom/conftest.py +0 -0
  225. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/custom/test_gliner2_classification.py +0 -0
  226. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/custom/test_invoice_extraction.py +0 -0
  227. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/custom/test_pipeline_integration.py +0 -0
  228. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/custom/test_regex_runner.py +0 -0
  229. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/custom/test_runner_hardening.py +0 -0
  230. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/custom/test_transformer_runners.py +0 -0
  231. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/pii/__init__.py +0 -0
  232. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/pii/conftest.py +0 -0
  233. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/pii/sample_invoice.pdf +0 -0
  234. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/pii/test_pii_chunking.py +0 -0
  235. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/pii/test_pii_detector.py +0 -0
  236. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
  237. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/pii/test_pii_regional_entities.py +0 -0
  238. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/pii/test_pii_severity.py +0 -0
  239. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/secrets/__init__.py +0 -0
  240. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/secrets/test_secrets_detector.py +0 -0
  241. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
  242. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/test_base_detector.py +0 -0
  243. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
  244. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/test_detector_catalog_commercial.py +0 -0
  245. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/test_detector_pipeline_types.py +0 -0
  246. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/test_detector_schema_examples.py +0 -0
  247. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/test_detector_types.py +0 -0
  248. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/test_phase2_detectors.py +0 -0
  249. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/test_registry.py +0 -0
  250. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/test_tag_runner.py +0 -0
  251. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/threat/__init__.py +0 -0
  252. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/threat/test_code_security_detector.py +0 -0
  253. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/detectors/threat/test_yara_detector.py +0 -0
  254. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
  255. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/integration/test_wordpress_links_assets.py +0 -0
  256. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/pipeline/test_detector_outcomes.py +0 -0
  257. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/pipeline/test_worker_pool.py +0 -0
  258. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_arrow_ipc_payloads.py +0 -0
  259. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_assets_metadata_catalog.py +0 -0
  260. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_augmentation_contract.py +0 -0
  261. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_augmentation_edges.py +0 -0
  262. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_augmentation_patch.py +0 -0
  263. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_augmentation_scan_cache.py +0 -0
  264. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_augmentation_sdk.py +0 -0
  265. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_augmentation_session.py +0 -0
  266. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_augmentation_tags_end_to_end.py +0 -0
  267. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_azure_blob_storage_source.py +0 -0
  268. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_base_source_attachment.py +0 -0
  269. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_base_source_sampling.py +0 -0
  270. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_byte_valued_fields.py +0 -0
  271. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_config.py +0 -0
  272. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_confluence_source.py +0 -0
  273. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_custom_extractor.py +0 -0
  274. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_custom_source_relationships.py +0 -0
  275. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_custom_source_tags_end_to_end.py +0 -0
  276. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_databricks_source.py +0 -0
  277. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_delta_lake_source.py +0 -0
  278. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_detector_scope.py +0 -0
  279. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_dropbox_auth_command.py +0 -0
  280. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_dropbox_source.py +0 -0
  281. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_elasticsearch_source.py +0 -0
  282. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_email_source.py +0 -0
  283. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_file_evaluation_runner.py +0 -0
  284. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_git_source.py +0 -0
  285. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_google_cloud_storage_source.py +0 -0
  286. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_google_workspace_source.py +0 -0
  287. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_graph_edges.py +0 -0
  288. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_hashing.py +0 -0
  289. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_hive_source.py +0 -0
  290. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_hugging_face_source.py +0 -0
  291. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_iceberg_source.py +0 -0
  292. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_jira_source.py +0 -0
  293. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_kafka_source.py +0 -0
  294. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_lineage_loss_reporting.py +0 -0
  295. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_local_folder_source.py +0 -0
  296. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_main.py +0 -0
  297. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_meilisearch_source.py +0 -0
  298. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_microsoft_365_source.py +0 -0
  299. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_mongodb_source.py +0 -0
  300. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_mssql_source.py +0 -0
  301. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_mysql_source.py +0 -0
  302. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_neo4j_source.py +0 -0
  303. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_notebook_asset_tags.py +0 -0
  304. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_notebook_execute.py +0 -0
  305. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_notebook_files.py +0 -0
  306. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_notebook_groups.py +0 -0
  307. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_notebook_packages.py +0 -0
  308. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_notebook_serialize.py +0 -0
  309. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_notebook_templates.py +0 -0
  310. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_notion_source.py +0 -0
  311. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_object_storage_download_efficiency.py +0 -0
  312. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_opensearch_source.py +0 -0
  313. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_oracle_source.py +0 -0
  314. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_outputs.py +0 -0
  315. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_payload_window.py +0 -0
  316. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_payload_window_parquet.py +0 -0
  317. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_postgresql_source.py +0 -0
  318. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_powerbi_source.py +0 -0
  319. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_range_reading.py +0 -0
  320. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_recipe_normalizer.py +0 -0
  321. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_reddit_source.py +0 -0
  322. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_rest_backpressure.py +0 -0
  323. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_runtime_packages.py +0 -0
  324. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_s3_compatible_storage_source.py +0 -0
  325. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_sampling_automatic.py +0 -0
  326. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_sandbox_source.py +0 -0
  327. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_sdk_completions.py +0 -0
  328. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_servicedesk_source.py +0 -0
  329. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_slack_source.py +0 -0
  330. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_snowflake_source.py +0 -0
  331. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_source_dependency_groups.py +0 -0
  332. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_source_strategy_conformance.py +0 -0
  333. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_sqlite_source.py +0 -0
  334. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_tableau_source.py +0 -0
  335. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_tabular_automatic_sampling.py +0 -0
  336. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_tabular_lineage.py +0 -0
  337. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_tabular_utils.py +0 -0
  338. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_torch_runtime.py +0 -0
  339. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_urn.py +0 -0
  340. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_uv_sync.py +0 -0
  341. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_wordpress_source.py +0 -0
  342. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_youtube_source.py +0 -0
  343. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/test_youtube_source_integration.py +0 -0
  344. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/utils/test_archive_extraction.py +0 -0
  345. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/utils/test_content_extraction.py +0 -0
  346. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/utils/test_embedded_files.py +0 -0
  347. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/utils/test_file_metadata.py +0 -0
  348. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/utils/test_file_parser.py +0 -0
  349. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/utils/test_file_to_images.py +0 -0
  350. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/utils/test_legacy_office.py +0 -0
  351. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/utils/test_payload.py +0 -0
  352. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/utils/test_transcription.py +0 -0
  353. {classifyre_cli-0.5.4 → classifyre_cli-0.5.5}/tests/utils/test_video_processing.py +0 -0
@@ -0,0 +1,3 @@
1
+ $ uv sync
2
+ Resolved 323 packages in 136ms
3
+ Checked 51 packages in 4ms
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: classifyre-cli
3
- Version: 0.5.4
3
+ Version: 0.5.5
4
4
  Summary: Classifyre CLI — scan and classify unstructured data sources
5
5
  License: MIT
6
6
  Keywords: data,ingestion,metadata,pii,secrets,unstructured
7
- Requires-Python: >=3.12
7
+ Requires-Python: <3.15,>=3.12
8
8
  Requires-Dist: beautifulsoup4>=4.12.0
9
9
  Requires-Dist: classifyre-schemas
10
10
  Requires-Dist: email-validator>=2.3.0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classifyre/cli",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "build": "uv sync",
@@ -1,9 +1,14 @@
1
1
  [project]
2
2
  name = "classifyre-cli"
3
- version = "0.5.4"
3
+ version = "0.5.5"
4
4
  description = "Classifyre CLI — scan and classify unstructured data sources"
5
5
  readme = "README.md"
6
- requires-python = ">=3.12"
6
+ # Upper bound is load-bearing: onnxruntime publishes no cp315 wheels (notably
7
+ # linux/aarch64, which required-environments forces uv to resolve), so an
8
+ # unbounded range makes `uv lock` fail for the python>=3.15 split and breaks
9
+ # the release workflow and Dependabot's uv updates. Lift to <3.16 once
10
+ # onnxruntime ships cp315 wheels for all required-environments platforms.
11
+ requires-python = ">=3.12,<3.15"
7
12
  license = { text = "MIT" }
8
13
  keywords = ["data", "metadata", "ingestion", "unstructured", "pii", "secrets"]
9
14
  dependencies = [
@@ -3,12 +3,15 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import base64
6
+ import functools
6
7
  import json
7
8
  import logging
8
9
  import os
9
10
  import random
11
+ import re
10
12
  import time
11
13
  from datetime import UTC, datetime
14
+ from pathlib import Path
12
15
  from typing import Any
13
16
 
14
17
  # Quiet litellm's import-time provider preload warnings (bedrock/sagemaker need
@@ -22,6 +25,7 @@ from ....models.generated_single_asset_scan_results import (
22
25
  )
23
26
  from ....utils.file_to_images import render_to_images, supported_mime_type
24
27
  from ...dependencies import require_module
28
+ from ...errors import ProviderRefusedError
25
29
  from ._base import _IMAGE_CONTENT_TYPES, _TEXT_CONTENT_TYPES, BaseRunner, _resolve_pipeline_severity
26
30
 
27
31
  logger = logging.getLogger(__name__)
@@ -48,11 +52,61 @@ _COMPLETION_TIMEOUT_SECONDS = float(os.environ.get("CLASSIFYRE_LLM_TIMEOUT_SECON
48
52
  # Retry budget for transient provider failures (429 rate limits, 5xx, timeouts).
49
53
  # Exponential backoff with jitter, honouring Retry-After when the provider
50
54
  # sends one, so a saturated endpoint is backed off from instead of hammered.
51
- _MAX_COMPLETION_ATTEMPTS = max(1, int(os.environ.get("CLASSIFYRE_LLM_MAX_ATTEMPTS", "4")))
55
+ # The attempt count is read per call (see `_max_attempts`), so a detector's own
56
+ # `budget.max_attempts` can override it.
57
+ _DEFAULT_MAX_COMPLETION_ATTEMPTS = 4
52
58
  _RETRY_BACKOFF_BASE_SECONDS = float(os.environ.get("CLASSIFYRE_LLM_RETRY_BASE_SECONDS", "2"))
53
59
  _RETRY_BACKOFF_MAX_SECONDS = 60.0
54
60
  _RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}
55
61
 
62
+ # Used only if the shared marker list cannot be read, so a packaging mistake
63
+ # degrades to "the known worst offender still fails fast" rather than to "every
64
+ # quota refusal is retried on every asset again".
65
+ _FALLBACK_QUOTA_MARKERS = (
66
+ "insufficient_credit",
67
+ "insufficient balance",
68
+ "out of credits",
69
+ "resource_exhausted",
70
+ "resource exhausted",
71
+ "quota exceeded",
72
+ "free-models-per-day",
73
+ "insufficient_quota",
74
+ )
75
+
76
+ # Billing-exhaustion signals checked before the generic substring scan, with
77
+ # word-boundary anchors so "billing" inside an unrelated word cannot trip them
78
+ # while "you have insufficient_credit" always does. Mirrors the head of the
79
+ # shared quota list in provider_refusal_markers.json.
80
+ _ANCHORED_BILLING_EXHAUSTION_MARKERS = (
81
+ "insufficient_credit",
82
+ "insufficient balance",
83
+ "out of credits",
84
+ )
85
+
86
+
87
+ def _billing_exhaustion_match(text: str) -> str | None:
88
+ """The anchored billing marker in *text*, or None when absent."""
89
+ for marker in _ANCHORED_BILLING_EXHAUSTION_MARKERS:
90
+ if re.search(rf"(?<![a-z0-9_]){re.escape(marker)}(?![a-z0-9_])", text):
91
+ return marker
92
+ return None
93
+
94
+
95
+ @functools.lru_cache(maxsize=1)
96
+ def _quota_markers() -> tuple[str, ...]:
97
+ """Substrings that make a refusal non-retryable; shared with the API."""
98
+ try:
99
+ import schemas
100
+
101
+ path = Path(schemas.__file__).parent / "provider_refusal_markers.json"
102
+ markers = json.loads(path.read_text(encoding="utf-8")).get("quota", [])
103
+ loaded = tuple(str(m).strip().lower() for m in markers if str(m).strip())
104
+ if loaded:
105
+ return loaded
106
+ except Exception as exc: # pragma: no cover - packaging failure
107
+ logger.warning("provider_refusal_markers.json unavailable (%s); using fallback", exc)
108
+ return _FALLBACK_QUOTA_MARKERS
109
+
56
110
 
57
111
  class LLMCompletionError(RuntimeError):
58
112
  """Provider call failed after retries.
@@ -63,6 +117,14 @@ class LLMCompletionError(RuntimeError):
63
117
  """
64
118
 
65
119
 
120
+ class LLMProviderRefusedError(LLMCompletionError, ProviderRefusedError):
121
+ """The provider refused in a way no retry can fix (quota, auth, billing).
122
+
123
+ Raised on the first such response, never after retries. The pipeline
124
+ disables the detector for the rest of the run when it sees one.
125
+ """
126
+
127
+
66
128
  class LLMRunner(BaseRunner):
67
129
  """AI detector — sends content to a configured LLM provider for classification + extraction."""
68
130
 
@@ -153,6 +215,10 @@ class LLMRunner(BaseRunner):
153
215
  response = self._complete_with_backoff(messages)
154
216
  raw = response.choices[0].message.content or "{}"
155
217
  parsed = self._parse_json(raw)
218
+ except LLMProviderRefusedError:
219
+ # Already names the detector, the model and the refusal. No stack
220
+ # trace: it is expected, and it will be the same on every asset.
221
+ raise
156
222
  except Exception as exc:
157
223
  logger.error(
158
224
  "llm detector error (detector=%s, model=%s): %s",
@@ -173,7 +239,8 @@ class LLMRunner(BaseRunner):
173
239
 
174
240
  def _complete_with_backoff(self, messages: list[dict[str, Any]]) -> Any:
175
241
  schema = self._schema
176
- for attempt in range(1, _MAX_COMPLETION_ATTEMPTS + 1):
242
+ max_attempts = self._max_attempts()
243
+ for attempt in range(1, max_attempts + 1):
177
244
  try:
178
245
  return self._litellm.completion(
179
246
  model=self._model_string(),
@@ -184,9 +251,26 @@ class LLMRunner(BaseRunner):
184
251
  messages=messages,
185
252
  response_format={"type": "json_object"},
186
253
  timeout=_COMPLETION_TIMEOUT_SECONDS,
254
+ # One retry layer, and it is this loop. litellm hands the
255
+ # OpenAI client its own default of 2 retries, which hid
256
+ # ~30 s of backoff inside every single attempt: a daily-quota
257
+ # refusal measured 30.2 s before this loop ever saw it.
258
+ max_retries=0,
187
259
  )
188
260
  except Exception as exc:
189
- if attempt >= _MAX_COMPLETION_ATTEMPTS or not self._is_retryable(exc):
261
+ refusal = self._refusal_reason(exc)
262
+ if refusal is not None:
263
+ logger.warning(
264
+ "llm provider refused (detector=%s, model=%s): %s — not retrying",
265
+ self._detector_key,
266
+ self._runtime.model,
267
+ refusal,
268
+ )
269
+ raise LLMProviderRefusedError(
270
+ f"LLM provider refused detector '{self._detector_key}' "
271
+ f"(model={self._runtime.model}, {refusal}): {exc}"
272
+ ) from None
273
+ if attempt >= max_attempts or not self._is_retryable(exc):
190
274
  raise
191
275
  delay = min(
192
276
  _RETRY_BACKOFF_MAX_SECONDS,
@@ -202,13 +286,55 @@ class LLMRunner(BaseRunner):
202
286
  self._detector_key,
203
287
  self._runtime.model,
204
288
  attempt,
205
- _MAX_COMPLETION_ATTEMPTS,
289
+ max_attempts,
206
290
  delay,
207
291
  exc,
208
292
  )
209
293
  time.sleep(delay)
210
294
  raise LLMCompletionError("unreachable") # pragma: no cover
211
295
 
296
+ def _max_attempts(self) -> int:
297
+ """The detector's own budget first, then the deployment default."""
298
+ budget = getattr(self._schema, "budget", None)
299
+ configured = getattr(budget, "max_attempts", None)
300
+ if isinstance(configured, int) and configured >= 1:
301
+ return configured
302
+ raw = os.environ.get("CLASSIFYRE_LLM_MAX_ATTEMPTS", "")
303
+ try:
304
+ return max(1, int(raw)) if raw.strip() else _DEFAULT_MAX_COMPLETION_ATTEMPTS
305
+ except ValueError:
306
+ return _DEFAULT_MAX_COMPLETION_ATTEMPTS
307
+
308
+ def _refusal_reason(self, exc: Exception) -> str | None:
309
+ """Why retrying this error cannot help, or None when it might.
310
+
311
+ A 429 is usually "slow down" and is retried. It is a refusal only when the
312
+ provider says the allowance itself is gone — a daily or credit quota — and
313
+ then every further call until the reset is the same answer, paid for again.
314
+ """
315
+ status = getattr(exc, "status_code", None)
316
+ if status in (401, 403):
317
+ return "authentication refused"
318
+ if status == 402:
319
+ return "payment required"
320
+ for type_name, reason in (
321
+ ("AuthenticationError", "authentication refused"),
322
+ ("PermissionDeniedError", "permission denied"),
323
+ ("BudgetExceededError", "budget exceeded"),
324
+ ):
325
+ error_type = getattr(self._litellm, type_name, None)
326
+ if isinstance(error_type, type) and isinstance(exc, error_type):
327
+ return reason
328
+
329
+ text = _error_text(exc).lower()
330
+ # Anchored billing-exhaustion markers first: a spent balance is the
331
+ # most specific "retrying cannot help" signal in the shared list.
332
+ if _billing_exhaustion_match(text) is not None:
333
+ return "quota exhausted"
334
+ if any(marker in text for marker in _quota_markers()):
335
+ return "quota exhausted"
336
+ return None
337
+
212
338
  def _is_retryable(self, exc: Exception) -> bool:
213
339
  retryable_types = tuple(
214
340
  t
@@ -223,6 +349,9 @@ class LLMRunner(BaseRunner):
223
349
  )
224
350
  if retryable_types and isinstance(exc, retryable_types):
225
351
  return True
352
+ # Only the listed statuses are retried: an unknown 4xx (e.g. a Gemini
353
+ # safety-block 400) is a final answer for this input, not a busy
354
+ # provider. Retry stays for 5xx, plain 429s and network errors.
226
355
  return getattr(exc, "status_code", None) in _RETRYABLE_STATUS_CODES
227
356
 
228
357
  def _vision_enabled(self) -> bool:
@@ -391,6 +520,16 @@ class LLMRunner(BaseRunner):
391
520
  return {str(k): v for k, v in raw.items()} if isinstance(raw, dict) else {}
392
521
 
393
522
 
523
+ def _error_text(exc: Exception) -> str:
524
+ """Everything a provider exception says about itself, for marker matching."""
525
+ parts = [str(exc), str(getattr(exc, "message", "") or "")]
526
+ response = getattr(exc, "response", None)
527
+ body = getattr(response, "text", None)
528
+ if isinstance(body, str):
529
+ parts.append(body)
530
+ return " ".join(parts)
531
+
532
+
394
533
  def _retry_after_seconds(exc: Exception) -> float | None:
395
534
  """Best-effort extraction of a Retry-After hint from a provider exception."""
396
535
  response = getattr(exc, "response", None)
@@ -0,0 +1,19 @@
1
+ """Detector errors whose *type* the pipeline acts on.
2
+
3
+ Kept free of heavy imports so the pipeline can recognise them without loading
4
+ any engine, and kept to a single string argument so they survive pickling
5
+ across the detector worker-pool process boundary with their class intact.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+
11
+ class ProviderRefusedError(RuntimeError):
12
+ """A provider refused the request in a way retrying cannot fix.
13
+
14
+ Quota exhausted for the day, a rejected key, billing. The next asset will be
15
+ refused the same way, so the pipeline disables the detector for the rest of
16
+ the run on the first one instead of paying for the same answer on every
17
+ asset — which is what held the only runner slot for 10 hours a day when a
18
+ free-tier LLM provider started refusing its key.
19
+ """
@@ -768,6 +768,22 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
768
768
  f" asset(s) after retries: {preview}"
769
769
  )
770
770
 
771
+ # A detector the breaker disabled is not a crash and not a
772
+ # clean run; the API reports it from the per-asset
773
+ # `breaker_open[...]` outcomes, and this is the log line an
774
+ # operator reads first.
775
+ for degraded in pipeline.breaker_summary():
776
+ logger.warning(
777
+ "Detector %s was disabled mid-run (%s): %s. %d payload(s) "
778
+ "attempted, %d skipped; the skipped assets keep their "
779
+ "findings and are retried on the next run.",
780
+ degraded["label"],
781
+ degraded["cause"],
782
+ degraded["reason"],
783
+ degraded["attempted_payloads"],
784
+ degraded["skipped_payloads"],
785
+ )
786
+
771
787
  # Persist the advanced AUTOMATIC sampling cursor (no-op for
772
788
  # other strategies, which return None). Only on the normal
773
789
  # completion path — a timed-out run must not advance it.
@@ -777,6 +793,10 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
777
793
  # A connector that covered a slice must say so before
778
794
  # finish(), which is what decides whether absence retires
779
795
  # an asset.
796
+ cohort_stats = getattr(source, "cohort_stats", None)
797
+ if cohort_stats and hasattr(sink, "set_cohort_stats"):
798
+ sink.set_cohort_stats(cohort_stats)
799
+
780
800
  if getattr(source, "partial_coverage", False) and hasattr(
781
801
  sink, "set_partial_coverage"
782
802
  ):
@@ -966,6 +966,37 @@ class DetectorScope(BaseModel):
966
966
  )
967
967
 
968
968
 
969
+ class DetectorBudget(BaseModel):
970
+ """
971
+ How much of a run this detector may spend failing.
972
+
973
+ Without a budget a detector keeps being called after it has stopped working: an LLM detector on a provider that refused its key retried every asset for 15-40 seconds and held the only runner slot for 10 hours a day, failing on 451 of 451 assets. Every run already disables a detector after a non-retryable provider refusal (quota, auth, billing) and after 10 consecutive failures; these fields tighten or loosen that per detector.
974
+
975
+ A disabled detector records an ERROR outcome on the assets it skipped, so their existing findings are kept and the assets are retried on the next run. Changing the budget never invalidates the scan cache.
976
+ """
977
+
978
+ model_config = ConfigDict(
979
+ extra='forbid',
980
+ )
981
+ max_attempts: int | None = Field(
982
+ None,
983
+ description='LLM detectors only: provider calls per asset, including the first. Overrides CLASSIFYRE_LLM_MAX_ATTEMPTS. A non-retryable refusal is never retried regardless.',
984
+ ge=1,
985
+ le=10,
986
+ )
987
+ max_consecutive_failures: int | None = Field(
988
+ None,
989
+ description='Disable the detector for the rest of the run after this many consecutive failed payloads. Defaults to 10.',
990
+ ge=1,
991
+ le=10000,
992
+ )
993
+ max_wall_clock_seconds: int | None = Field(
994
+ None,
995
+ description='Disable the detector for the rest of the run once it has spent this long, summed over every payload it ran on. Unset means no limit.',
996
+ ge=1,
997
+ )
998
+
999
+
969
1000
  class Type1(StrEnum):
970
1001
  GLINER2 = 'GLINER2'
971
1002
 
@@ -978,6 +1009,10 @@ class GLiNER2PipelineSchema(BaseModel):
978
1009
  None,
979
1010
  description='Restrict this detector to part of a source (asset kind, content type, or an asset-metadata predicate). Null runs it on everything the source produces.',
980
1011
  )
1012
+ budget: DetectorBudget | None = Field(
1013
+ None,
1014
+ description='Failure and time budget for this detector within one run. Null uses the defaults: stop after a non-retryable provider refusal or 10 consecutive failures.',
1015
+ )
981
1016
  type: Literal['GLINER2'] = 'GLINER2'
982
1017
  entities: dict[str, PipelineEntityDefinition] | None = None
983
1018
  classification: dict[str, PipelineClassificationDefinition] | None = None
@@ -997,6 +1032,10 @@ class RegexPipelineSchema(BaseModel):
997
1032
  None,
998
1033
  description='Restrict this detector to part of a source (asset kind, content type, or an asset-metadata predicate). Null runs it on everything the source produces.',
999
1034
  )
1035
+ budget: DetectorBudget | None = Field(
1036
+ None,
1037
+ description='Failure and time budget for this detector within one run. Null uses the defaults: stop after a non-retryable provider refusal or 10 consecutive failures.',
1038
+ )
1000
1039
  type: Literal['REGEX'] = 'REGEX'
1001
1040
  patterns: dict[str, RegexPatternDefinition] | None = None
1002
1041
  validation: PipelineValidationConfig | None = None
@@ -1105,6 +1144,10 @@ class LLMPipelineSchema(BaseModel):
1105
1144
  None,
1106
1145
  description='Restrict this detector to part of a source (asset kind, content type, or an asset-metadata predicate). Null runs it on everything the source produces.',
1107
1146
  )
1147
+ budget: DetectorBudget | None = Field(
1148
+ None,
1149
+ description='Failure and time budget for this detector within one run. Null uses the defaults: stop after a non-retryable provider refusal or 10 consecutive failures.',
1150
+ )
1108
1151
  type: Literal['LLM'] = 'LLM'
1109
1152
  system_prompt: str = Field(
1110
1153
  ...,
@@ -1205,6 +1248,10 @@ class TextClassificationPipelineSchema(BaseModel):
1205
1248
  None,
1206
1249
  description='Restrict this detector to part of a source (asset kind, content type, or an asset-metadata predicate). Null runs it on everything the source produces.',
1207
1250
  )
1251
+ budget: DetectorBudget | None = Field(
1252
+ None,
1253
+ description='Failure and time budget for this detector within one run. Null uses the defaults: stop after a non-retryable provider refusal or 10 consecutive failures.',
1254
+ )
1208
1255
  type: Literal['TEXT_CLASSIFICATION']
1209
1256
  model: str = Field(
1210
1257
  ...,
@@ -1279,6 +1326,10 @@ class ImageClassificationPipelineSchema(BaseModel):
1279
1326
  None,
1280
1327
  description='Restrict this detector to part of a source (asset kind, content type, or an asset-metadata predicate). Null runs it on everything the source produces.',
1281
1328
  )
1329
+ budget: DetectorBudget | None = Field(
1330
+ None,
1331
+ description='Failure and time budget for this detector within one run. Null uses the defaults: stop after a non-retryable provider refusal or 10 consecutive failures.',
1332
+ )
1282
1333
  type: Literal['IMAGE_CLASSIFICATION']
1283
1334
  model: str | None = Field(
1284
1335
  None,
@@ -1335,6 +1386,10 @@ class ObjectDetectionPipelineSchema(BaseModel):
1335
1386
  None,
1336
1387
  description='Restrict this detector to part of a source (asset kind, content type, or an asset-metadata predicate). Null runs it on everything the source produces.',
1337
1388
  )
1389
+ budget: DetectorBudget | None = Field(
1390
+ None,
1391
+ description='Failure and time budget for this detector within one run. Null uses the defaults: stop after a non-retryable provider refusal or 10 consecutive failures.',
1392
+ )
1338
1393
  type: Literal['OBJECT_DETECTION']
1339
1394
  model: str = Field(
1340
1395
  ...,
@@ -1399,6 +1454,10 @@ class TagPipelineSchema(BaseModel):
1399
1454
  None,
1400
1455
  description='Restrict this detector to part of a source (asset kind, content type, or an asset-metadata predicate). Null runs it on everything the source produces.',
1401
1456
  )
1457
+ budget: DetectorBudget | None = Field(
1458
+ None,
1459
+ description='Failure and time budget for this detector within one run. Null uses the defaults: stop after a non-retryable provider refusal or 10 consecutive failures.',
1460
+ )
1402
1461
  type: Literal['TAG'] = 'TAG'
1403
1462
  label: str | None = Field(
1404
1463
  None,
@@ -4631,6 +4631,24 @@ class Variables(RootModel[str]):
4631
4631
  root: str = Field(..., max_length=8192)
4632
4632
 
4633
4633
 
4634
+ class CohortWeightsMode(StrEnum):
4635
+ """
4636
+ auto: measured from what each band yields. fixed: the declared or fixed split, always.
4637
+ """
4638
+
4639
+ auto = 'auto'
4640
+ fixed = 'fixed'
4641
+
4642
+
4643
+ class CustomCohortBandWeights(BaseModel):
4644
+ model_config = ConfigDict(
4645
+ extra='forbid',
4646
+ )
4647
+ newest: float | None = Field(None, ge=0.0)
4648
+ oldest: float | None = Field(None, ge=0.0)
4649
+ random: float | None = Field(None, ge=0.0)
4650
+
4651
+
4634
4652
  class NotebookPackage(BaseModel):
4635
4653
  """
4636
4654
  A Python distribution installed before the notebook runs.
@@ -5688,38 +5706,40 @@ class GitInput(CoreInput):
5688
5706
  augmentation: AugmentationConfig | None = None
5689
5707
 
5690
5708
 
5691
- class CustomOptional(BaseModel):
5709
+ class CustomCohortWeights(BaseModel):
5710
+ """
5711
+ How ctx.cohort() splits each run's budget across its bands. auto shifts the split toward the bands whose keys produce HIGH/CRITICAL findings, within guards: the declared split stands until every band has 200 visits, no band drops below the floor, and no band moves more than 15 points per run. fixed always uses the bands the notebook declares, or the ones given here.
5712
+ """
5713
+
5692
5714
  model_config = ConfigDict(
5693
5715
  extra='forbid',
5694
5716
  )
5695
- variables: (
5696
- dict[constr(pattern=r'^[A-Za-z_][A-Za-z0-9_]{0,62}$'), Variables] | None
5697
- ) = Field(
5698
- {},
5699
- description='Non-secret values the notebook reads with ctx.var("name"), such as a base URL or an account id. Stored in clear text. Keys must be valid Python identifiers.',
5700
- max_length=64,
5701
- validate_default=True,
5717
+ mode: CohortWeightsMode | None = Field(
5718
+ 'auto',
5719
+ description='auto: measured from what each band yields. fixed: the declared or fixed split, always.',
5720
+ title='CohortWeightsMode',
5702
5721
  )
5703
- limits: CustomOptionalLimits | None = None
5704
- packages: list[NotebookPackage] | None = Field(
5705
- [],
5706
- description="Python packages installed into the run environment before any cell executes. Installed with uv; the base image's own dependencies are always present and do not need listing.",
5707
- max_length=50,
5708
- validate_default=True,
5722
+ floor: float | None = Field(
5723
+ None,
5724
+ description='The least share any band keeps under auto, as a fraction. Never below 0.1.',
5725
+ ge=0.1,
5726
+ le=0.5,
5709
5727
  )
5710
- local_folders: list[NotebookLocalFolder] | None = Field(
5711
- [],
5712
- description='Folders the notebook reads with ctx.folder("name"). Not available in Kubernetes deployments, where files are uploaded to the source instead. This is a convenience, not a sandbox: the notebook process runs as you and can open any path you can.',
5713
- max_length=10,
5714
- validate_default=True,
5728
+ fixed: dict[str, CustomCohortBandWeights] | None = Field(
5729
+ None,
5730
+ description='Per cohort name, the band split to use under fixed, e.g. {"register": {"newest": 60, "oldest": 30, "random": 10}}.',
5731
+ )
5732
+ effective: dict[str, CustomCohortBandWeights] | None = Field(
5733
+ None,
5734
+ description="Set by the platform on each run's recipe: the split each cohort runs with. Not stored on the source.",
5715
5735
  )
5716
5736
 
5717
5737
 
5718
- class CustomInput(CoreInput):
5719
- type: Literal['CUSTOM'] = Field(..., description='Type of the asset or source')
5720
- required: CustomRequired
5721
- masked: CustomMasked | None = None
5722
- optional: CustomOptional | None = None
5738
+ class YouTubeInput(CoreInput):
5739
+ type: Literal['YOUTUBE'] = Field(..., description='Type of the asset or source')
5740
+ required: YouTubeRequired
5741
+ masked: YouTubeMasked | None = None
5742
+ optional: YouTubeOptional | None = None
5723
5743
  detectors: list[Detector] | None = Field(
5724
5744
  None, description='Detectors to run on ingested content'
5725
5745
  )
@@ -5737,11 +5757,15 @@ class CustomInput(CoreInput):
5737
5757
  augmentation: AugmentationConfig | None = None
5738
5758
 
5739
5759
 
5740
- class YouTubeInput(CoreInput):
5741
- type: Literal['YOUTUBE'] = Field(..., description='Type of the asset or source')
5742
- required: YouTubeRequired
5743
- masked: YouTubeMasked | None = None
5744
- optional: YouTubeOptional | None = None
5760
+ class RedditInput(CoreInput):
5761
+ type: Literal['REDDIT'] = Field(..., description='Type of the asset or source')
5762
+ required: (
5763
+ RedditRequiredReadOnly | RedditRequiredScript | RedditRequiredRefreshToken
5764
+ ) = Field(..., title='RedditRequired')
5765
+ masked: RedditMaskedReadOnly | RedditMaskedScript | RedditMaskedRefreshToken = (
5766
+ Field(..., title='RedditMasked')
5767
+ )
5768
+ optional: RedditOptional | None = None
5745
5769
  detectors: list[Detector] | None = Field(
5746
5770
  None, description='Detectors to run on ingested content'
5747
5771
  )
@@ -5759,15 +5783,39 @@ class YouTubeInput(CoreInput):
5759
5783
  augmentation: AugmentationConfig | None = None
5760
5784
 
5761
5785
 
5762
- class RedditInput(CoreInput):
5763
- type: Literal['REDDIT'] = Field(..., description='Type of the asset or source')
5764
- required: (
5765
- RedditRequiredReadOnly | RedditRequiredScript | RedditRequiredRefreshToken
5766
- ) = Field(..., title='RedditRequired')
5767
- masked: RedditMaskedReadOnly | RedditMaskedScript | RedditMaskedRefreshToken = (
5768
- Field(..., title='RedditMasked')
5786
+ class CustomOptional(BaseModel):
5787
+ model_config = ConfigDict(
5788
+ extra='forbid',
5769
5789
  )
5770
- optional: RedditOptional | None = None
5790
+ variables: (
5791
+ dict[constr(pattern=r'^[A-Za-z_][A-Za-z0-9_]{0,62}$'), Variables] | None
5792
+ ) = Field(
5793
+ {},
5794
+ description='Non-secret values the notebook reads with ctx.var("name"), such as a base URL or an account id. Stored in clear text. Keys must be valid Python identifiers.',
5795
+ max_length=64,
5796
+ validate_default=True,
5797
+ )
5798
+ limits: CustomOptionalLimits | None = None
5799
+ packages: list[NotebookPackage] | None = Field(
5800
+ [],
5801
+ description="Python packages installed into the run environment before any cell executes. Installed with uv; the base image's own dependencies are always present and do not need listing.",
5802
+ max_length=50,
5803
+ validate_default=True,
5804
+ )
5805
+ local_folders: list[NotebookLocalFolder] | None = Field(
5806
+ [],
5807
+ description='Folders the notebook reads with ctx.folder("name"). Not available in Kubernetes deployments, where files are uploaded to the source instead. This is a convenience, not a sandbox: the notebook process runs as you and can open any path you can.',
5808
+ max_length=10,
5809
+ validate_default=True,
5810
+ )
5811
+ cohort_weights: CustomCohortWeights | None = None
5812
+
5813
+
5814
+ class CustomInput(CoreInput):
5815
+ type: Literal['CUSTOM'] = Field(..., description='Type of the asset or source')
5816
+ required: CustomRequired
5817
+ masked: CustomMasked | None = None
5818
+ optional: CustomOptional | None = None
5771
5819
  detectors: list[Detector] | None = Field(
5772
5820
  None, description='Detectors to run on ingested content'
5773
5821
  )