classifyre-cli 0.4.9__tar.gz → 0.4.11__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 (186) hide show
  1. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/.turbo/turbo-build.log +1 -1
  2. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/PKG-INFO +1 -1
  3. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/package.json +1 -1
  4. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/pyproject.toml +14 -2
  5. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/custom/runners/_base.py +28 -0
  6. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/custom/runners/_image_classification.py +41 -31
  7. classifyre_cli-0.4.11/src/detectors/custom/runners/_llm.py +295 -0
  8. classifyre_cli-0.4.11/src/detectors/custom/runners/_object_detection.py +121 -0
  9. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/models/generated_detectors.py +147 -5
  10. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/outputs/rest.py +4 -0
  11. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/pipeline/detector_pipeline.py +13 -32
  12. classifyre_cli-0.4.11/src/sandbox/runner.py +308 -0
  13. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/object_storage/base.py +81 -5
  14. classifyre_cli-0.4.11/src/utils/embedded_images.py +222 -0
  15. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/utils/file_parser.py +65 -38
  16. classifyre_cli-0.4.11/src/utils/file_to_images.py +134 -0
  17. classifyre_cli-0.4.11/tests/detectors/custom/test_llm_runner.py +236 -0
  18. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/custom/test_transformer_runners.py +3 -3
  19. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_outputs.py +2 -1
  20. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_s3_compatible_storage_source.py +68 -0
  21. classifyre_cli-0.4.11/tests/test_sandbox_runner.py +214 -0
  22. classifyre_cli-0.4.11/tests/utils/test_embedded_images.py +129 -0
  23. classifyre_cli-0.4.11/tests/utils/test_file_to_images.py +99 -0
  24. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/uv.lock +481 -172
  25. classifyre_cli-0.4.9/src/detectors/custom/runners/_llm.py +0 -22
  26. classifyre_cli-0.4.9/src/detectors/custom/runners/_object_detection.py +0 -107
  27. classifyre_cli-0.4.9/src/sandbox/runner.py +0 -145
  28. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/.gitignore +0 -0
  29. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/.python-version +0 -0
  30. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/README.md +0 -0
  31. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/main.py +0 -0
  32. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/scripts/generate_models.py +0 -0
  33. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/__init__.py +0 -0
  34. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/__init__.py +0 -0
  35. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/base.py +0 -0
  36. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/broken_links/__init__.py +0 -0
  37. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/broken_links/detector.py +0 -0
  38. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/config.py +0 -0
  39. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/content/__init__.py +0 -0
  40. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/custom/__init__.py +0 -0
  41. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/custom/detector.py +0 -0
  42. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/custom/extractor.py +0 -0
  43. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/custom/runners/__init__.py +0 -0
  44. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/custom/runners/_factory.py +0 -0
  45. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/custom/runners/_feature_extraction.py +0 -0
  46. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/custom/runners/_gliner2.py +0 -0
  47. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/custom/runners/_regex.py +0 -0
  48. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/custom/runners/_text_classification.py +0 -0
  49. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/custom/trainer.py +0 -0
  50. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/dependencies.py +0 -0
  51. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/pii/__init__.py +0 -0
  52. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/pii/detector.py +0 -0
  53. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/secrets/__init__.py +0 -0
  54. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/secrets/detector.py +0 -0
  55. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/threat/__init__.py +0 -0
  56. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/threat/code_security_detector.py +0 -0
  57. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/detectors/threat/yara_detector.py +0 -0
  58. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/main.py +0 -0
  59. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/models/generated_input.py +0 -0
  60. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/models/generated_single_asset_scan_results.py +0 -0
  61. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/outputs/__init__.py +0 -0
  62. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/outputs/base.py +0 -0
  63. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/outputs/console.py +0 -0
  64. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/outputs/factory.py +0 -0
  65. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/outputs/file.py +0 -0
  66. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/pipeline/__init__.py +0 -0
  67. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/pipeline/content_provider.py +0 -0
  68. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/pipeline/parsed_content_provider.py +0 -0
  69. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/pipeline/worker_pool.py +0 -0
  70. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sandbox/__init__.py +0 -0
  71. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/__init__.py +0 -0
  72. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/atlassian_common.py +0 -0
  73. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/azure_blob_storage/__init__.py +0 -0
  74. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/azure_blob_storage/source.py +0 -0
  75. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/base.py +0 -0
  76. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/confluence/__init__.py +0 -0
  77. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/confluence/source.py +0 -0
  78. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/databricks/__init__.py +0 -0
  79. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/databricks/source.py +0 -0
  80. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/dependencies.py +0 -0
  81. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/google_cloud_storage/__init__.py +0 -0
  82. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/google_cloud_storage/source.py +0 -0
  83. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/hive/__init__.py +0 -0
  84. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/hive/source.py +0 -0
  85. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/jira/__init__.py +0 -0
  86. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/jira/source.py +0 -0
  87. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/mongodb/__init__.py +0 -0
  88. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/mongodb/source.py +0 -0
  89. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/mssql/__init__.py +0 -0
  90. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/mssql/source.py +0 -0
  91. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/mysql/__init__.py +0 -0
  92. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/mysql/source.py +0 -0
  93. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/neo4j/__init__.py +0 -0
  94. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/neo4j/source.py +0 -0
  95. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/oracle/__init__.py +0 -0
  96. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/oracle/source.py +0 -0
  97. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/postgresql/__init__.py +0 -0
  98. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/postgresql/source.py +0 -0
  99. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/powerbi/__init__.py +0 -0
  100. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/powerbi/source.py +0 -0
  101. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/recipe_normalizer.py +0 -0
  102. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/s3_compatible_storage/README.md +0 -0
  103. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/s3_compatible_storage/__init__.py +0 -0
  104. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/s3_compatible_storage/source.py +0 -0
  105. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/servicedesk/__init__.py +0 -0
  106. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/servicedesk/source.py +0 -0
  107. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/slack/__init__.py +0 -0
  108. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/slack/source.py +0 -0
  109. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/snowflake/__init__.py +0 -0
  110. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/snowflake/source.py +0 -0
  111. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/sqlite/__init__.py +0 -0
  112. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/sqlite/source.py +0 -0
  113. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/tableau/__init__.py +0 -0
  114. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/tableau/source.py +0 -0
  115. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/tabular_base.py +0 -0
  116. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/tabular_utils.py +0 -0
  117. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/wordpress/__init__.py +0 -0
  118. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/sources/wordpress/source.py +0 -0
  119. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/telemetry.py +0 -0
  120. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/utils/__init__.py +0 -0
  121. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/utils/content_extraction.py +0 -0
  122. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/utils/hashing.py +0 -0
  123. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/utils/uv_sync.py +0 -0
  124. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/src/utils/validation.py +0 -0
  125. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/__init__.py +0 -0
  126. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/conftest.py +0 -0
  127. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/__init__.py +0 -0
  128. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
  129. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/conftest.py +0 -0
  130. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/content/__init__.py +0 -0
  131. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/custom/__init__.py +0 -0
  132. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/custom/conftest.py +0 -0
  133. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/custom/test_invoice_extraction.py +0 -0
  134. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/custom/test_pipeline_integration.py +0 -0
  135. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/custom/test_regex_runner.py +0 -0
  136. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/pii/__init__.py +0 -0
  137. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/pii/conftest.py +0 -0
  138. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/pii/sample_invoice.pdf +0 -0
  139. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/pii/test_pii_detector.py +0 -0
  140. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
  141. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/secrets/__init__.py +0 -0
  142. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/secrets/test_secrets_detector.py +0 -0
  143. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
  144. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/test_base_detector.py +0 -0
  145. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
  146. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/test_detector_catalog_commercial.py +0 -0
  147. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/test_detector_pipeline_types.py +0 -0
  148. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/test_detector_schema_examples.py +0 -0
  149. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/test_detector_types.py +0 -0
  150. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/test_phase2_detectors.py +0 -0
  151. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/test_registry.py +0 -0
  152. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/threat/__init__.py +0 -0
  153. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/threat/test_code_security_detector.py +0 -0
  154. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/detectors/threat/test_yara_detector.py +0 -0
  155. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
  156. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/integration/test_wordpress_links_assets.py +0 -0
  157. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/pipeline/test_detector_pipeline.py +0 -0
  158. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/pipeline/test_worker_pool.py +0 -0
  159. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_azure_blob_storage_source.py +0 -0
  160. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_base_source_attachment.py +0 -0
  161. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_base_source_sampling.py +0 -0
  162. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_confluence_source.py +0 -0
  163. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_custom_extractor.py +0 -0
  164. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_databricks_source.py +0 -0
  165. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_google_cloud_storage_source.py +0 -0
  166. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_hashing.py +0 -0
  167. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_hive_source.py +0 -0
  168. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_jira_source.py +0 -0
  169. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_mongodb_source.py +0 -0
  170. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_mssql_source.py +0 -0
  171. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_mysql_source.py +0 -0
  172. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_neo4j_source.py +0 -0
  173. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_oracle_source.py +0 -0
  174. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_postgresql_source.py +0 -0
  175. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_powerbi_source.py +0 -0
  176. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_recipe_normalizer.py +0 -0
  177. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_servicedesk_source.py +0 -0
  178. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_slack_source.py +0 -0
  179. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_snowflake_source.py +0 -0
  180. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_source_dependency_groups.py +0 -0
  181. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_sqlite_source.py +0 -0
  182. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_tableau_source.py +0 -0
  183. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_tabular_utils.py +0 -0
  184. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/test_wordpress_source.py +0 -0
  185. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/utils/test_content_extraction.py +0 -0
  186. {classifyre_cli-0.4.9 → classifyre_cli-0.4.11}/tests/utils/test_file_parser.py +0 -0
@@ -1,3 +1,3 @@
1
1
  $ uv sync
2
- Resolved 256 packages in 201ms
2
+ Resolved 265 packages in 157ms
3
3
  Checked 50 packages in 1ms
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: classifyre-cli
3
- Version: 0.4.9
3
+ Version: 0.4.11
4
4
  Summary: Classifyre CLI — scan and classify unstructured data sources
5
5
  License: MIT
6
6
  Keywords: data,ingestion,metadata,pii,secrets,unstructured
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classifyre/cli",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "build": "uv sync",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "classifyre-cli"
3
- version = "0.4.9"
3
+ version = "0.4.11"
4
4
  description = "Classifyre CLI — scan and classify unstructured data sources"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -47,7 +47,7 @@ privacy = [
47
47
  # mid-run in frozen/venv contexts. 8.x eagerly loads all data at import time,
48
48
  # avoiding ModuleNotFoundError during Presidio phone number analysis.
49
49
  "phonenumbers>=8.13.0,<10.0.0",
50
- "numpy>=1.26.0,<2.0.0",
50
+ "numpy>=1.26.0,<3.0.0",
51
51
  ]
52
52
  security = [
53
53
  "detect-secrets>=1.5.0",
@@ -91,6 +91,13 @@ custom = [
91
91
  regex = [
92
92
  "google-re2>=1.1",
93
93
  ]
94
+ llm = [
95
+ "litellm>=1.86.2",
96
+ # Pure-wheel PDF renderer (permissive license, no system binaries) used to
97
+ # rasterise PDF pages to images for vision-capable LLM detectors.
98
+ "pypdfium2>=4.30.0",
99
+ "pillow>=12.2.0",
100
+ ]
94
101
  detectors = [
95
102
  { include-group = "file-processing" },
96
103
  { include-group = "privacy" },
@@ -101,6 +108,7 @@ detectors = [
101
108
  { include-group = "classification" },
102
109
  { include-group = "custom" },
103
110
  { include-group = "regex" },
111
+ { include-group = "llm" },
104
112
  ]
105
113
  file-processing = [
106
114
  "filetype>=1.2.0",
@@ -264,6 +272,10 @@ module = [
264
272
  "datasets",
265
273
  "setfit.*",
266
274
  "setfit",
275
+ "litellm.*",
276
+ "litellm",
277
+ "pypdfium2.*",
278
+ "pypdfium2",
267
279
  "sklearn.*",
268
280
  "sklearn",
269
281
  "numpy",
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import io
6
+ import logging
5
7
  import re
6
8
  from abc import ABC, abstractmethod
7
9
  from datetime import UTC, datetime
@@ -38,6 +40,32 @@ _IMAGE_CONTENT_TYPES = [
38
40
  "image/bmp",
39
41
  "image/tiff",
40
42
  ]
43
+ # Content types HuggingFace image detectors accept. Non-image renderable files
44
+ # (PDFs) are rasterised page-by-page via render_to_images before classification,
45
+ # mirroring the vision LLM detector's input handling.
46
+ _IMAGE_INPUT_CONTENT_TYPES = [*_IMAGE_CONTENT_TYPES, "application/pdf"]
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+
51
+ def _load_input_images(content: bytes, content_type: str, pil: Any) -> list[tuple[int, Any]]:
52
+ """Return ``(page_index, PIL.Image)`` tuples for an image or renderable file.
53
+
54
+ Image MIME types open directly; PDFs (and any type ``render_to_images`` supports)
55
+ are rasterised to one image per page. Unsupported types return ``[]``.
56
+ """
57
+ from ....utils.file_to_images import render_to_images, supported_mime_type
58
+
59
+ normalized = content_type.split(";", 1)[0].strip().lower()
60
+ try:
61
+ if normalized.startswith("image/"):
62
+ return [(0, pil.open(io.BytesIO(content)))]
63
+ if supported_mime_type(content_type):
64
+ pages = render_to_images(content, content_type)
65
+ return [(idx, pil.open(io.BytesIO(png))) for idx, png in enumerate(pages)]
66
+ except Exception as exc: # pragma: no cover - defensive
67
+ logger.warning("Failed to load input images (%s): %s", normalized, exc)
68
+ return []
41
69
 
42
70
 
43
71
  def _resolve_pipeline_severity(
@@ -2,7 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import io
6
5
  import logging
7
6
  from typing import Any
8
7
 
@@ -11,8 +10,9 @@ from ....models.generated_single_asset_scan_results import DetectionResult
11
10
  from ...dependencies import ensure_torch, require_module
12
11
  from ._base import (
13
12
  _DEFAULT_IMAGE_CLASSIFICATION_MODEL,
14
- _IMAGE_CONTENT_TYPES,
13
+ _IMAGE_INPUT_CONTENT_TYPES,
15
14
  BaseRunner,
15
+ _load_input_images,
16
16
  _resolve_pipeline_severity,
17
17
  )
18
18
 
@@ -54,45 +54,55 @@ class ImageClassificationRunner(BaseRunner):
54
54
  raise NotImplementedError("ImageClassificationRunner uses detect() directly")
55
55
 
56
56
  def detect(self, content: str | bytes, content_type: str) -> list[DetectionResult]:
57
- if not content_type.startswith("image/"):
58
- return []
59
57
  if isinstance(content, str):
60
58
  logger.warning("image_classification: received string content, expected bytes")
61
59
  return []
62
60
 
61
+ # image/* opens directly; PDFs are rasterised to one image per page.
62
+ images = _load_input_images(content, content_type, self._pil)
63
+ if not images:
64
+ return []
65
+
63
66
  schema = self._schema
64
67
  threshold = schema.confidence_threshold if schema.confidence_threshold is not None else 0.0
68
+ multi_page = len(images) > 1
65
69
  results: list[DetectionResult] = []
66
- try:
67
- image = self._pil.open(io.BytesIO(content))
68
- predictions: list[dict[str, Any]] = self._pipe(image) or []
69
- for pred in predictions:
70
- label: str = pred.get("label", "unknown")
71
- score: float = float(pred.get("score", 0.0))
72
- if score < threshold:
73
- continue
74
- severity = _resolve_pipeline_severity(label, schema.severity_map)
75
- results.append(
76
- self._make_result(
77
- finding_type=f"classification:{label}",
78
- category="CONTENT",
79
- severity=severity,
80
- confidence=score,
81
- matched_content=f"Image classified as: {label} ({score:.3f})",
82
- location=None,
83
- metadata={
84
- "image_size": f"{image.size[0]}x{image.size[1]}",
85
- "image_mode": image.mode,
86
- "model": self._model_id,
87
- },
70
+ for page_index, image in images:
71
+ try:
72
+ predictions: list[dict[str, Any]] = self._pipe(image) or []
73
+ for pred in predictions:
74
+ label: str = pred.get("label", "unknown")
75
+ score: float = float(pred.get("score", 0.0))
76
+ if score < threshold:
77
+ continue
78
+ severity = _resolve_pipeline_severity(label, schema.severity_map)
79
+ page_suffix = f" (page {page_index + 1})" if multi_page else ""
80
+ metadata: dict[str, Any] = {
81
+ "image_size": f"{image.size[0]}x{image.size[1]}",
82
+ "image_mode": image.mode,
83
+ "model": self._model_id,
84
+ }
85
+ if multi_page:
86
+ metadata["page"] = page_index + 1
87
+ results.append(
88
+ self._make_result(
89
+ finding_type=f"classification:{label}",
90
+ category="CONTENT",
91
+ severity=severity,
92
+ confidence=score,
93
+ matched_content=(
94
+ f"Image classified as: {label} ({score:.3f}){page_suffix}"
95
+ ),
96
+ location=None,
97
+ metadata=metadata,
98
+ )
88
99
  )
100
+ except Exception as exc:
101
+ logger.error(
102
+ "image_classification error (model=%s): %s", self._model_id, exc, exc_info=True
89
103
  )
90
- except Exception as exc:
91
- logger.error(
92
- "image_classification error (model=%s): %s", self._model_id, exc, exc_info=True
93
- )
94
104
  results.sort(key=lambda r: r.confidence, reverse=True)
95
105
  return results
96
106
 
97
107
  def get_supported_content_types(self) -> list[str]:
98
- return list(_IMAGE_CONTENT_TYPES)
108
+ return list(_IMAGE_INPUT_CONTENT_TYPES)
@@ -0,0 +1,295 @@
1
+ """AI/LLM pipeline runner — prompt-driven classification and field extraction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import json
7
+ import logging
8
+ import os
9
+ from datetime import UTC, datetime
10
+ from typing import Any
11
+
12
+ # Quiet litellm's import-time provider preload warnings (bedrock/sagemaker need
13
+ # botocore, which we don't install) before the library is ever imported.
14
+ os.environ.setdefault("LITELLM_LOG", "ERROR")
15
+
16
+ from ....models.generated_detectors import LLMPipelineSchema, Severity
17
+ from ....models.generated_single_asset_scan_results import (
18
+ DetectionResult,
19
+ DetectorType,
20
+ )
21
+ from ....utils.file_to_images import render_to_images, supported_mime_type
22
+ from ...dependencies import require_module
23
+ from ._base import _IMAGE_CONTENT_TYPES, _TEXT_CONTENT_TYPES, BaseRunner, _resolve_pipeline_severity
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # Map the stored AI provider type onto the litellm model-string convention.
28
+ _PROVIDER_PREFIX: dict[str, str] = {
29
+ "CLAUDE": "anthropic",
30
+ "GEMINI": "gemini",
31
+ "OPENAI_COMPATIBLE": "openai",
32
+ }
33
+
34
+ # Content types a vision-capable LLM detector renders to images and sends to the
35
+ # model directly. PDFs are rasterised page-by-page; images pass through.
36
+ _VISION_CONTENT_TYPES = [*_IMAGE_CONTENT_TYPES, "application/pdf"]
37
+
38
+ # Cap the number of rendered page images sent in a single completion to bound
39
+ # token cost and request size for multi-page PDFs.
40
+ _MAX_VISION_IMAGES = 20
41
+
42
+
43
+ class LLMRunner(BaseRunner):
44
+ """AI detector — sends content to a configured LLM provider for classification + extraction."""
45
+
46
+ def __init__(
47
+ self, schema: LLMPipelineSchema, detector_key: str = "", detector_name: str = ""
48
+ ) -> None:
49
+ self._schema = schema
50
+ self._detector_key = detector_key
51
+ self._detector_name = detector_name
52
+
53
+ runtime = schema.provider_runtime
54
+ if runtime is None:
55
+ raise ValueError(
56
+ f"AI detector '{detector_key}' is missing provider_runtime — the API must "
57
+ "inject resolved provider credentials before dispatch."
58
+ )
59
+ self._runtime = runtime
60
+ self._litellm = require_module("litellm", "llm", ["llm"])
61
+ # Let litellm silently drop params an endpoint doesn't support (e.g.
62
+ # response_format / temperature on some OpenAI-compatible gateways)
63
+ # instead of raising. Keep its own logging quiet.
64
+ self._litellm.drop_params = True
65
+ self._litellm.suppress_debug_info = True
66
+ logging.getLogger("LiteLLM").setLevel(logging.ERROR)
67
+
68
+ def run(self, text: str) -> None: # type: ignore[override] # pragma: no cover
69
+ raise NotImplementedError("LLMRunner uses detect() directly")
70
+
71
+ def detect(self, content: str | bytes, content_type: str) -> list[DetectionResult]:
72
+ if isinstance(content, bytes):
73
+ return self._detect_vision(content, content_type)
74
+ if content_type not in _TEXT_CONTENT_TYPES:
75
+ return []
76
+ text = content.strip()
77
+ if not text:
78
+ return []
79
+
80
+ schema = self._schema
81
+ content_limit = schema.content_limit or 8000
82
+ snippet = text[:content_limit]
83
+
84
+ messages = [
85
+ {"role": "system", "content": self._build_system_prompt()},
86
+ {"role": "user", "content": snippet},
87
+ ]
88
+ return self._complete_and_parse(messages, snippet)
89
+
90
+ def _detect_vision(self, content: bytes, content_type: str) -> list[DetectionResult]:
91
+ """Render a binary file (image/PDF) to images and classify via the model."""
92
+ if not self._vision_enabled():
93
+ return []
94
+ if not supported_mime_type(content_type):
95
+ return []
96
+
97
+ images = render_to_images(
98
+ content,
99
+ content_type,
100
+ max_pages=_MAX_VISION_IMAGES,
101
+ )
102
+ if not images:
103
+ return []
104
+
105
+ image_blocks = [
106
+ {
107
+ "type": "image_url",
108
+ "image_url": {
109
+ "url": f"data:image/png;base64,{base64.b64encode(png).decode('ascii')}"
110
+ },
111
+ }
112
+ for png in images[:_MAX_VISION_IMAGES]
113
+ ]
114
+ messages = [
115
+ {"role": "system", "content": self._build_system_prompt()},
116
+ {"role": "user", "content": image_blocks},
117
+ ]
118
+ # matched_content fallback descriptor — there is no text snippet for files.
119
+ descriptor = f"[{content_type}, {len(image_blocks)} page image(s)]"
120
+ return self._complete_and_parse(messages, descriptor, vision_pages=len(image_blocks))
121
+
122
+ def _complete_and_parse(
123
+ self,
124
+ messages: list[dict[str, Any]],
125
+ snippet: str,
126
+ *,
127
+ vision_pages: int | None = None,
128
+ ) -> list[DetectionResult]:
129
+ schema = self._schema
130
+ try:
131
+ response = self._litellm.completion(
132
+ model=self._model_string(),
133
+ api_key=self._runtime.api_key,
134
+ api_base=self._runtime.base_url or None,
135
+ temperature=schema.temperature if schema.temperature is not None else 0.0,
136
+ max_tokens=self._max_tokens(),
137
+ messages=messages,
138
+ response_format={"type": "json_object"},
139
+ )
140
+ raw = response.choices[0].message.content or "{}"
141
+ parsed = self._parse_json(raw)
142
+ except Exception as exc:
143
+ logger.error(
144
+ "llm detector error (detector=%s, model=%s): %s",
145
+ self._detector_key,
146
+ self._runtime.model,
147
+ exc,
148
+ exc_info=True,
149
+ )
150
+ return []
151
+
152
+ return self._results_from_payload(snippet, parsed, vision_pages=vision_pages)
153
+
154
+ def _vision_enabled(self) -> bool:
155
+ return bool(getattr(self._runtime, "supports_vision", False))
156
+
157
+ def get_supported_content_types(self) -> list[str]:
158
+ types = list(_TEXT_CONTENT_TYPES)
159
+ if self._vision_enabled():
160
+ types.extend(_VISION_CONTENT_TYPES)
161
+ return types
162
+
163
+ # ── Internals ────────────────────────────────────────────────────────────
164
+
165
+ def _max_tokens(self) -> int | None:
166
+ # `max_tokens` is generated as a RootModel[int] wrapper, so unwrap `.root`
167
+ # before handing it to litellm — passing the model object serialises to an
168
+ # invalid request body and fails the whole completion.
169
+ raw = self._schema.max_tokens
170
+ if raw is None:
171
+ return None
172
+ return getattr(raw, "root", raw)
173
+
174
+ def _model_string(self) -> str:
175
+ prefix = _PROVIDER_PREFIX.get(self._runtime.provider.value, "openai")
176
+ return f"{prefix}/{self._runtime.model}"
177
+
178
+ def _build_system_prompt(self) -> str:
179
+ schema = self._schema
180
+ parts: list[str] = [schema.system_prompt.strip()]
181
+
182
+ labels = schema.labels or []
183
+ if labels:
184
+ label_lines = "\n".join(
185
+ f"- {lbl.name}: {lbl.description}" if lbl.description else f"- {lbl.name}"
186
+ for lbl in labels
187
+ )
188
+ parts.append(
189
+ "Classify the content using these labels:\n"
190
+ + label_lines
191
+ + (
192
+ "\nMultiple labels may apply."
193
+ if schema.multi_label
194
+ else "\nChoose the single best label."
195
+ )
196
+ )
197
+
198
+ fields = schema.output_fields or []
199
+ if fields:
200
+ field_lines = "\n".join(
201
+ f"- {f.name} ({f.type.value if f.type else 'string'}): {f.description}"
202
+ if f.description
203
+ else f"- {f.name} ({f.type.value if f.type else 'string'})"
204
+ for f in fields
205
+ )
206
+ parts.append("Also extract these fields:\n" + field_lines)
207
+
208
+ parts.append(
209
+ "Respond with a JSON object of the form: "
210
+ '{"labels": [{"name": "<label>", "confidence": <0-1>, '
211
+ '"matched_content": "<relevant snippet>"}], "fields": {<field name>: <value>}}. '
212
+ "Use only the labels listed above. Return an empty labels array when none apply."
213
+ )
214
+
215
+ if schema.response_example:
216
+ parts.append("Example response:\n" + schema.response_example.strip())
217
+
218
+ return "\n\n".join(parts)
219
+
220
+ @staticmethod
221
+ def _parse_json(raw: str) -> dict[str, Any]:
222
+ try:
223
+ parsed = json.loads(raw)
224
+ except json.JSONDecodeError:
225
+ start = raw.find("{")
226
+ end = raw.rfind("}")
227
+ if start == -1 or end == -1 or end <= start:
228
+ return {}
229
+ try:
230
+ parsed = json.loads(raw[start : end + 1])
231
+ except json.JSONDecodeError:
232
+ return {}
233
+ return parsed if isinstance(parsed, dict) else {}
234
+
235
+ def _results_from_payload(
236
+ self,
237
+ snippet: str,
238
+ payload: dict[str, Any],
239
+ *,
240
+ vision_pages: int | None = None,
241
+ ) -> list[DetectionResult]:
242
+ schema = self._schema
243
+ threshold = schema.confidence_threshold if schema.confidence_threshold is not None else 0.5
244
+ default_severity = schema.severity or Severity.info
245
+ extracted = self._coerce_fields(payload.get("fields"))
246
+
247
+ raw_labels = payload.get("labels")
248
+ label_entries: list[dict[str, Any]] = (
249
+ [lbl for lbl in raw_labels if isinstance(lbl, dict)]
250
+ if isinstance(raw_labels, list)
251
+ else []
252
+ )
253
+
254
+ results: list[DetectionResult] = []
255
+ for entry in label_entries:
256
+ label = str(entry.get("name", "")).strip()
257
+ if not label:
258
+ continue
259
+ confidence = float(entry.get("confidence", 1.0) or 0.0)
260
+ if confidence < threshold:
261
+ continue
262
+ severity = _resolve_pipeline_severity(label, schema.severity_map, default_severity)
263
+ matched = str(entry.get("matched_content") or "").strip() or snippet[:320]
264
+ results.append(
265
+ DetectionResult(
266
+ detector_type=DetectorType.CUSTOM,
267
+ finding_type=label,
268
+ category="CLASSIFICATION",
269
+ severity=severity,
270
+ confidence=min(0.99, confidence),
271
+ matched_content=matched,
272
+ location=None,
273
+ custom_detector_key=self._detector_key,
274
+ custom_detector_name=self._detector_name,
275
+ detected_at=datetime.now(UTC),
276
+ metadata={
277
+ "runner": "LLM",
278
+ "provider": self._runtime.provider.value,
279
+ "model": self._runtime.model,
280
+ "label": label,
281
+ "fields": extracted,
282
+ "input": "vision" if vision_pages is not None else "text",
283
+ **({"vision_pages": vision_pages} if vision_pages is not None else {}),
284
+ },
285
+ extracted_data=extracted or None,
286
+ extraction_method="LLM",
287
+ )
288
+ )
289
+
290
+ results.sort(key=lambda r: r.confidence, reverse=True)
291
+ return results
292
+
293
+ @staticmethod
294
+ def _coerce_fields(raw: Any) -> dict[str, Any]:
295
+ return {str(k): v for k, v in raw.items()} if isinstance(raw, dict) else {}
@@ -0,0 +1,121 @@
1
+ """Object detection pipeline runner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any
7
+
8
+ from ....models.generated_detectors import ObjectDetectionPipelineSchema
9
+ from ....models.generated_single_asset_scan_results import DetectionResult, Location
10
+ from ...dependencies import MissingDependencyError, ensure_torch, require_module
11
+ from ._base import (
12
+ _IMAGE_INPUT_CONTENT_TYPES,
13
+ BaseRunner,
14
+ _load_input_images,
15
+ _resolve_pipeline_severity,
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class ObjectDetectionRunner(BaseRunner):
22
+ """Object detection via a single HuggingFace object-detection pipeline."""
23
+
24
+ def __init__(
25
+ self,
26
+ schema: ObjectDetectionPipelineSchema,
27
+ detector_key: str = "",
28
+ detector_name: str = "",
29
+ ) -> None:
30
+ self._schema = schema
31
+ self._detector_key = detector_key
32
+ self._detector_name = detector_name
33
+ ensure_torch("object_detection", ["custom", "detectors"])
34
+ transformers = require_module("transformers", "object_detection", ["custom", "detectors"])
35
+ self._pil = require_module("PIL.Image", "object_detection", ["custom", "detectors"])
36
+ pipeline_kwargs: dict[str, Any] = {
37
+ "model": schema.model,
38
+ "device": schema.device or "cpu",
39
+ }
40
+ if schema.model_revision:
41
+ pipeline_kwargs["revision"] = schema.model_revision
42
+ nms = getattr(schema.nms_threshold, "root", schema.nms_threshold)
43
+ if nms is not None:
44
+ pipeline_kwargs["threshold"] = nms
45
+ try:
46
+ self._pipe: Any = transformers.pipeline("object-detection", **pipeline_kwargs)
47
+ except ImportError as exc:
48
+ raise MissingDependencyError(
49
+ "object_detection",
50
+ ["custom", "detectors"],
51
+ f"ObjectDetectionRunner requires additional dependencies: {exc}",
52
+ ) from exc
53
+
54
+ def run(self, text: str) -> None: # type: ignore[override] # pragma: no cover
55
+ raise NotImplementedError("ObjectDetectionRunner uses detect() directly")
56
+
57
+ def detect(self, content: str | bytes, content_type: str) -> list[DetectionResult]:
58
+ if isinstance(content, str):
59
+ logger.warning("object_detection: received string content, expected bytes")
60
+ return []
61
+
62
+ # image/* opens directly; PDFs are rasterised to one image per page.
63
+ images = _load_input_images(content, content_type, self._pil)
64
+ if not images:
65
+ return []
66
+
67
+ schema = self._schema
68
+ threshold = schema.confidence_threshold if schema.confidence_threshold is not None else 0.5
69
+ multi_page = len(images) > 1
70
+ results: list[DetectionResult] = []
71
+ for page_index, image in images:
72
+ try:
73
+ detections: list[dict[str, Any]] = self._pipe(image) or []
74
+ for det in detections:
75
+ label: str = det.get("label", "unknown")
76
+ score: float = float(det.get("score", 0.0))
77
+ box: dict[str, int] = det.get("box", {})
78
+ if score < threshold:
79
+ continue
80
+ if schema.min_box_area is not None:
81
+ w = max(0, box.get("xmax", 0) - box.get("xmin", 0))
82
+ h = max(0, box.get("ymax", 0) - box.get("ymin", 0))
83
+ if w * h < schema.min_box_area:
84
+ continue
85
+ severity = _resolve_pipeline_severity(label, schema.severity_map)
86
+ page_prefix = f"page {page_index + 1} " if multi_page else ""
87
+ metadata: dict[str, Any] = {
88
+ "box": box,
89
+ "score": score,
90
+ "image_size": f"{image.size[0]}x{image.size[1]}",
91
+ "model": schema.model,
92
+ }
93
+ if multi_page:
94
+ metadata["page"] = page_index + 1
95
+ results.append(
96
+ self._make_result(
97
+ finding_type=label,
98
+ category="CONTENT",
99
+ severity=severity,
100
+ confidence=score,
101
+ matched_content=label,
102
+ location=Location(
103
+ description=(
104
+ f"{page_prefix}box xmin={box.get('xmin')} ymin={box.get('ymin')}"
105
+ f" xmax={box.get('xmax')} ymax={box.get('ymax')}"
106
+ ),
107
+ ),
108
+ metadata=metadata,
109
+ )
110
+ )
111
+ except Exception as exc:
112
+ logger.error(
113
+ "object_detection error (model=%s): %s", schema.model, exc, exc_info=True
114
+ )
115
+ results.sort(key=lambda r: r.confidence, reverse=True)
116
+ if schema.top_k is not None:
117
+ results = results[: schema.top_k]
118
+ return results
119
+
120
+ def get_supported_content_types(self) -> list[str]:
121
+ return list(_IMAGE_INPUT_CONTENT_TYPES)