classifyre-cli 0.4.70__tar.gz → 0.4.71__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 (261) hide show
  1. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/.turbo/turbo-build.log +1 -1
  2. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/PKG-INFO +1 -1
  3. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/package.json +1 -1
  4. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/pyproject.toml +1 -1
  5. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/main.py +6 -1
  6. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/models/generated_input.py +56 -27
  7. classifyre_cli-0.4.71/src/sources/dropbox/auth.py +254 -0
  8. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/dropbox/source.py +65 -9
  9. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/kafka/source.py +194 -15
  10. classifyre_cli-0.4.71/tests/test_dropbox_auth_command.py +172 -0
  11. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_dropbox_source.py +75 -6
  12. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_kafka_source.py +132 -9
  13. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/uv.lock +11 -11
  14. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/.gitignore +0 -0
  15. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/.python-version +0 -0
  16. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/README.md +0 -0
  17. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/main.py +0 -0
  18. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/scripts/generate_models.py +0 -0
  19. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/__init__.py +0 -0
  20. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/config.py +0 -0
  21. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/__init__.py +0 -0
  22. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/base.py +0 -0
  23. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/broken_links/__init__.py +0 -0
  24. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/broken_links/detector.py +0 -0
  25. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/config.py +0 -0
  26. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/content/__init__.py +0 -0
  27. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/custom/__init__.py +0 -0
  28. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/custom/detector.py +0 -0
  29. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/custom/extractor.py +0 -0
  30. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/custom/runners/__init__.py +0 -0
  31. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/custom/runners/_base.py +0 -0
  32. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/custom/runners/_factory.py +0 -0
  33. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/custom/runners/_gliner2.py +0 -0
  34. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/custom/runners/_image_classification.py +0 -0
  35. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/custom/runners/_llm.py +0 -0
  36. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/custom/runners/_object_detection.py +0 -0
  37. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/custom/runners/_regex.py +0 -0
  38. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/custom/runners/_text_classification.py +0 -0
  39. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/custom/trainer.py +0 -0
  40. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/dependencies.py +0 -0
  41. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/pii/__init__.py +0 -0
  42. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/pii/detector.py +0 -0
  43. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/secrets/__init__.py +0 -0
  44. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/secrets/detector.py +0 -0
  45. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/threat/__init__.py +0 -0
  46. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/threat/code_security_detector.py +0 -0
  47. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/detectors/threat/yara_detector.py +0 -0
  48. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/file_evaluation/__init__.py +0 -0
  49. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/file_evaluation/runner.py +0 -0
  50. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/models/generated_detectors.py +0 -0
  51. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/models/generated_single_asset_scan_results.py +0 -0
  52. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/outputs/__init__.py +0 -0
  53. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/outputs/base.py +0 -0
  54. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/outputs/console.py +0 -0
  55. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/outputs/factory.py +0 -0
  56. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/outputs/file.py +0 -0
  57. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/outputs/rest.py +0 -0
  58. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/pipeline/__init__.py +0 -0
  59. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/pipeline/content_provider.py +0 -0
  60. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/pipeline/detector_pipeline.py +0 -0
  61. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/pipeline/parsed_content_provider.py +0 -0
  62. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/pipeline/text_artifact.py +0 -0
  63. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/pipeline/worker_pool.py +0 -0
  64. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/__init__.py +0 -0
  65. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/asset_metadata.py +0 -0
  66. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/atlassian_common.py +0 -0
  67. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/azure_blob_storage/__init__.py +0 -0
  68. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/azure_blob_storage/source.py +0 -0
  69. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/base.py +0 -0
  70. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/confluence/__init__.py +0 -0
  71. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/confluence/source.py +0 -0
  72. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/databricks/__init__.py +0 -0
  73. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/databricks/source.py +0 -0
  74. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/delta_lake/__init__.py +0 -0
  75. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/delta_lake/source.py +0 -0
  76. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/dependencies.py +0 -0
  77. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/dropbox/__init__.py +0 -0
  78. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/elasticsearch/__init__.py +0 -0
  79. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/elasticsearch/source.py +0 -0
  80. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/email/__init__.py +0 -0
  81. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/email/source.py +0 -0
  82. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/google_cloud_storage/__init__.py +0 -0
  83. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/google_cloud_storage/source.py +0 -0
  84. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/google_workspace/__init__.py +0 -0
  85. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/google_workspace/source.py +0 -0
  86. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/hive/__init__.py +0 -0
  87. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/hive/source.py +0 -0
  88. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/iceberg/__init__.py +0 -0
  89. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/iceberg/source.py +0 -0
  90. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/jira/__init__.py +0 -0
  91. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/jira/source.py +0 -0
  92. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/kafka/__init__.py +0 -0
  93. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/kafka/rest.py +0 -0
  94. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/lakehouse_base.py +0 -0
  95. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/local_folder/__init__.py +0 -0
  96. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/local_folder/source.py +0 -0
  97. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/meilisearch/__init__.py +0 -0
  98. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/meilisearch/source.py +0 -0
  99. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/microsoft_365/__init__.py +0 -0
  100. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/microsoft_365/source.py +0 -0
  101. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/mongodb/__init__.py +0 -0
  102. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/mongodb/source.py +0 -0
  103. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/mssql/__init__.py +0 -0
  104. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/mssql/source.py +0 -0
  105. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/mysql/__init__.py +0 -0
  106. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/mysql/source.py +0 -0
  107. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/neo4j/__init__.py +0 -0
  108. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/neo4j/source.py +0 -0
  109. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/notion/__init__.py +0 -0
  110. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/notion/client.py +0 -0
  111. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/notion/source.py +0 -0
  112. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/object_storage/base.py +0 -0
  113. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/opensearch/__init__.py +0 -0
  114. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/opensearch/source.py +0 -0
  115. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/oracle/__init__.py +0 -0
  116. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/oracle/source.py +0 -0
  117. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/postgresql/__init__.py +0 -0
  118. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/postgresql/source.py +0 -0
  119. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/powerbi/__init__.py +0 -0
  120. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/powerbi/source.py +0 -0
  121. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/recipe_normalizer.py +0 -0
  122. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/s3_client.py +0 -0
  123. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/s3_compatible_storage/README.md +0 -0
  124. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/s3_compatible_storage/__init__.py +0 -0
  125. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/s3_compatible_storage/source.py +0 -0
  126. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/sandbox/__init__.py +0 -0
  127. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/sandbox/source.py +0 -0
  128. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/search_engine_base.py +0 -0
  129. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/servicedesk/__init__.py +0 -0
  130. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/servicedesk/source.py +0 -0
  131. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/slack/__init__.py +0 -0
  132. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/slack/source.py +0 -0
  133. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/snowflake/__init__.py +0 -0
  134. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/snowflake/source.py +0 -0
  135. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/sqlite/__init__.py +0 -0
  136. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/sqlite/source.py +0 -0
  137. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/tableau/__init__.py +0 -0
  138. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/tableau/source.py +0 -0
  139. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/tabular_base.py +0 -0
  140. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/tabular_utils.py +0 -0
  141. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/wordpress/__init__.py +0 -0
  142. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/wordpress/source.py +0 -0
  143. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/youtube/__init__.py +0 -0
  144. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/sources/youtube/source.py +0 -0
  145. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/telemetry.py +0 -0
  146. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/utils/__init__.py +0 -0
  147. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/utils/archive_extraction.py +0 -0
  148. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/utils/content_extraction.py +0 -0
  149. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/utils/dependency_groups.py +0 -0
  150. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/utils/embedded_images.py +0 -0
  151. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/utils/file_metadata.py +0 -0
  152. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/utils/file_parser.py +0 -0
  153. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/utils/file_to_images.py +0 -0
  154. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/utils/hashing.py +0 -0
  155. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/utils/legacy_office.py +0 -0
  156. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/utils/resources.py +0 -0
  157. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/utils/transcription.py +0 -0
  158. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/utils/uv_sync.py +0 -0
  159. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/utils/validation.py +0 -0
  160. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/src/utils/video_processing.py +0 -0
  161. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/__init__.py +0 -0
  162. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/_lakehouse_fakes.py +0 -0
  163. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/conftest.py +0 -0
  164. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/__init__.py +0 -0
  165. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
  166. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/conftest.py +0 -0
  167. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/content/__init__.py +0 -0
  168. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/custom/__init__.py +0 -0
  169. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/custom/conftest.py +0 -0
  170. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/custom/test_gliner2_classification.py +0 -0
  171. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/custom/test_invoice_extraction.py +0 -0
  172. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/custom/test_llm_runner.py +0 -0
  173. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/custom/test_pipeline_integration.py +0 -0
  174. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/custom/test_regex_runner.py +0 -0
  175. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/custom/test_runner_hardening.py +0 -0
  176. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/custom/test_transformer_runners.py +0 -0
  177. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/pii/__init__.py +0 -0
  178. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/pii/conftest.py +0 -0
  179. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/pii/sample_invoice.pdf +0 -0
  180. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/pii/test_pii_chunking.py +0 -0
  181. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/pii/test_pii_detector.py +0 -0
  182. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
  183. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/pii/test_pii_severity.py +0 -0
  184. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/secrets/__init__.py +0 -0
  185. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/secrets/test_secrets_detector.py +0 -0
  186. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
  187. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/test_base_detector.py +0 -0
  188. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
  189. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/test_detector_catalog_commercial.py +0 -0
  190. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/test_detector_pipeline_types.py +0 -0
  191. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/test_detector_schema_examples.py +0 -0
  192. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/test_detector_types.py +0 -0
  193. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/test_phase2_detectors.py +0 -0
  194. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/test_registry.py +0 -0
  195. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/threat/__init__.py +0 -0
  196. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/threat/test_code_security_detector.py +0 -0
  197. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/detectors/threat/test_yara_detector.py +0 -0
  198. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
  199. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/integration/test_wordpress_links_assets.py +0 -0
  200. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/pipeline/test_detector_outcomes.py +0 -0
  201. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/pipeline/test_detector_pipeline.py +0 -0
  202. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/pipeline/test_worker_pool.py +0 -0
  203. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_assets_metadata_catalog.py +0 -0
  204. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_azure_blob_storage_source.py +0 -0
  205. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_base_source_attachment.py +0 -0
  206. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_base_source_sampling.py +0 -0
  207. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_config.py +0 -0
  208. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_confluence_source.py +0 -0
  209. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_custom_extractor.py +0 -0
  210. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_databricks_source.py +0 -0
  211. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_delta_lake_source.py +0 -0
  212. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_dependency_groups.py +0 -0
  213. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_elasticsearch_source.py +0 -0
  214. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_email_source.py +0 -0
  215. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_file_evaluation_runner.py +0 -0
  216. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_google_cloud_storage_source.py +0 -0
  217. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_google_workspace_source.py +0 -0
  218. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_hashing.py +0 -0
  219. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_hive_source.py +0 -0
  220. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_iceberg_source.py +0 -0
  221. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_jira_source.py +0 -0
  222. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_local_folder_source.py +0 -0
  223. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_main.py +0 -0
  224. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_meilisearch_source.py +0 -0
  225. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_microsoft_365_source.py +0 -0
  226. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_mongodb_source.py +0 -0
  227. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_mssql_source.py +0 -0
  228. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_mysql_source.py +0 -0
  229. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_neo4j_source.py +0 -0
  230. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_notion_source.py +0 -0
  231. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_opensearch_source.py +0 -0
  232. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_oracle_source.py +0 -0
  233. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_outputs.py +0 -0
  234. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_postgresql_source.py +0 -0
  235. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_powerbi_source.py +0 -0
  236. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_recipe_normalizer.py +0 -0
  237. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_s3_compatible_storage_source.py +0 -0
  238. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_sampling_automatic.py +0 -0
  239. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_sandbox_source.py +0 -0
  240. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_servicedesk_source.py +0 -0
  241. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_slack_source.py +0 -0
  242. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_snowflake_source.py +0 -0
  243. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_source_dependency_groups.py +0 -0
  244. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_source_strategy_conformance.py +0 -0
  245. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_sqlite_source.py +0 -0
  246. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_tableau_source.py +0 -0
  247. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_tabular_automatic_sampling.py +0 -0
  248. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_tabular_utils.py +0 -0
  249. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_uv_sync.py +0 -0
  250. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_wordpress_source.py +0 -0
  251. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_youtube_source.py +0 -0
  252. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/test_youtube_source_integration.py +0 -0
  253. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/utils/test_archive_extraction.py +0 -0
  254. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/utils/test_content_extraction.py +0 -0
  255. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/utils/test_embedded_images.py +0 -0
  256. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/utils/test_file_metadata.py +0 -0
  257. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/utils/test_file_parser.py +0 -0
  258. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/utils/test_file_to_images.py +0 -0
  259. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/utils/test_legacy_office.py +0 -0
  260. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/utils/test_transcription.py +0 -0
  261. {classifyre_cli-0.4.70 → classifyre_cli-0.4.71}/tests/utils/test_video_processing.py +0 -0
@@ -1,3 +1,3 @@
1
1
  $ uv sync
2
- Resolved 302 packages in 146ms
2
+ Resolved 302 packages in 216ms
3
3
  Checked 51 packages in 1ms
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: classifyre-cli
3
- Version: 0.4.70
3
+ Version: 0.4.71
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.70",
3
+ "version": "0.4.71",
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.70"
3
+ version = "0.4.71"
4
4
  description = "Classifyre CLI — scan and classify unstructured data sources"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -10,6 +10,7 @@ from typing import Any, cast
10
10
 
11
11
  from .outputs import create_output_sink
12
12
  from .sources import get_source, list_available_sources
13
+ from .sources.dropbox.auth import add_dropbox_auth_arguments, run_dropbox_auth_command
13
14
  from .utils.validation import validate_input, validate_test_connection
14
15
 
15
16
  logger = logging.getLogger(__name__)
@@ -586,7 +587,7 @@ def main() -> None:
586
587
  parser = argparse.ArgumentParser(description="Classifyre Metadata Extraction CLI")
587
588
  parser.add_argument(
588
589
  "command",
589
- choices=["test", "extract", "discover", "evaluate-file", "train"],
590
+ choices=["test", "extract", "discover", "evaluate-file", "train", "dropbox-auth"],
590
591
  help="Command to run",
591
592
  )
592
593
  parser.add_argument(
@@ -667,6 +668,7 @@ def main() -> None:
667
668
  default=None,
668
669
  help="Max assets processed concurrently in Phase 2. Controls DB connection usage. Defaults to pool_workers*2 (env: CLASSIFYRE_MAX_CONCURRENT_ASSETS)",
669
670
  )
671
+ add_dropbox_auth_arguments(parser)
670
672
 
671
673
  args = parser.parse_args()
672
674
 
@@ -695,6 +697,9 @@ def main() -> None:
695
697
  if args.debug:
696
698
  logging.getLogger().setLevel(logging.DEBUG)
697
699
 
700
+ if args.command == "dropbox-auth":
701
+ sys.exit(run_dropbox_auth_command(args))
702
+
698
703
  if args.command == "evaluate-file":
699
704
  run_evaluate_file_command(args)
700
705
  return
@@ -3461,27 +3461,31 @@ class SASLUsernamePassword1(BaseModel):
3461
3461
  )
3462
3462
  sasl_username: str = Field(..., description='SASL username')
3463
3463
  sasl_password: str = Field(..., description='SASL password')
3464
- ssl_ca: str | None = Field(
3464
+ ca_certificate: str | None = Field(
3465
3465
  None,
3466
- description="PEM-encoded CA certificate used to verify the broker's TLS certificate. Required for private or self-signed CAs; leave empty to trust the system CA bundle.",
3466
+ description="PEM-encoded CA certificate used to verify the broker's TLS certificate — the provider's ca.pem. Required for private or self-signed CAs; leave empty to trust the system CA bundle.",
3467
3467
  )
3468
3468
 
3469
3469
 
3470
3470
  class ClientCertificateMTLS1(BaseModel):
3471
3471
  """
3472
- mTLS credentials: the client certificate and its private key, plus the CA certificate when the broker uses a private CA.
3472
+ mTLS credentials, named as the providers name them: Aiven's access key / access certificate / CA certificate, Confluent's client key / client certificate. Paste each PEM block whole, including its BEGIN and END lines.
3473
3473
  """
3474
3474
 
3475
3475
  model_config = ConfigDict(
3476
3476
  extra='forbid',
3477
3477
  )
3478
- ssl_certfile: str = Field(..., description='PEM-encoded client access certificate')
3479
- ssl_keyfile: str = Field(
3480
- ..., description='PEM-encoded client access key (private key)'
3478
+ access_key: str = Field(
3479
+ ...,
3480
+ description='PEM-encoded client private key — the block starting with -----BEGIN PRIVATE KEY----- (Aiven: service.key)',
3481
3481
  )
3482
- ssl_ca: str | None = Field(
3482
+ access_certificate: str = Field(
3483
+ ...,
3484
+ description='PEM-encoded client certificate — the block starting with -----BEGIN CERTIFICATE----- (Aiven: service.cert)',
3485
+ )
3486
+ ca_certificate: str | None = Field(
3483
3487
  None,
3484
- description="PEM-encoded CA certificate used to verify the broker's TLS certificate. Required for private or self-signed CAs; leave empty to trust the system CA bundle.",
3488
+ description="PEM-encoded CA certificate used to verify the broker's TLS certificate — the provider's ca.pem. Required for private or self-signed CAs; leave empty to trust the system CA bundle.",
3485
3489
  )
3486
3490
 
3487
3491
 
@@ -4221,39 +4225,43 @@ class GoogleWorkspaceInput(CoreInput):
4221
4225
  )
4222
4226
 
4223
4227
 
4224
- class DropboxRequiredAccessToken(BaseModel):
4228
+ class DropboxOAuthRefreshTokenRecommended(BaseModel):
4225
4229
  """
4226
- Access token issued from the Dropbox App Console. Simplest to set up; short-lived tokens expire after 4 hours.
4230
+ Recommended. An OAuth app key and secret plus a refresh token, which never expires — the client mints a fresh access token on every run. Obtain the refresh token once with `classifyre dropbox-auth --app-key <key> --app-secret <secret>`; it prints a URL you open in your own browser and exchanges the code it gives you.
4227
4231
  """
4228
4232
 
4229
4233
  model_config = ConfigDict(
4230
4234
  extra='forbid',
4231
4235
  )
4232
- auth_method: Literal['access_token']
4236
+ auth_method: Literal['oauth']
4237
+ app_key: str = Field(
4238
+ ..., description='Dropbox app key (App Console → Settings → App key)'
4239
+ )
4233
4240
 
4234
4241
 
4235
- class DropboxRequiredOAuth(BaseModel):
4242
+ class DropboxOAuthWithPKCENoAppSecret(BaseModel):
4236
4243
  """
4237
- OAuth 2.0 app credentials with a refresh token. Recommended for unattended scans the client mints a fresh access token on every run.
4244
+ Same as the OAuth refresh token flow but for apps that hold no secret: the authorization is proved with PKCE, so only the app key is needed to refresh. Obtain the refresh token with `classifyre dropbox-auth --app-key <key> --pkce`.
4238
4245
  """
4239
4246
 
4240
4247
  model_config = ConfigDict(
4241
4248
  extra='forbid',
4242
4249
  )
4243
- auth_method: Literal['oauth']
4250
+ auth_method: Literal['oauth_pkce']
4244
4251
  app_key: str = Field(
4245
- ...,
4246
- description='Dropbox app key (client ID) of the OAuth app that issued the refresh token',
4252
+ ..., description='Dropbox app key (App Console → Settings → App key)'
4247
4253
  )
4248
4254
 
4249
4255
 
4250
- class DropboxMaskedAccessToken(BaseModel):
4256
+ class DropboxAccessTokenShortLivedTestingOnly(BaseModel):
4257
+ """
4258
+ Testing only. The token generated by the App Console button is short-lived and stops working about four hours later with `invalid_access_token`, so scheduled scans will fail. Use one of the refresh-token methods for anything recurring.
4259
+ """
4260
+
4251
4261
  model_config = ConfigDict(
4252
4262
  extra='forbid',
4253
4263
  )
4254
- access_token: str = Field(
4255
- ..., description='Dropbox OAuth 2.0 access token (sl.… for short-lived tokens)'
4256
- )
4264
+ auth_method: Literal['access_token']
4257
4265
 
4258
4266
 
4259
4267
  class DropboxMaskedOAuth(BaseModel):
@@ -4261,12 +4269,31 @@ class DropboxMaskedOAuth(BaseModel):
4261
4269
  extra='forbid',
4262
4270
  )
4263
4271
  app_secret: str = Field(
4272
+ ..., description='Dropbox app secret (App Console → Settings → App secret)'
4273
+ )
4274
+ refresh_token: str = Field(
4264
4275
  ...,
4265
- description='Dropbox app secret of the OAuth app that issued the refresh token',
4276
+ description='Long-lived refresh token issued with token_access_type=offline',
4277
+ )
4278
+
4279
+
4280
+ class DropboxMaskedOAuthPkce(BaseModel):
4281
+ model_config = ConfigDict(
4282
+ extra='forbid',
4266
4283
  )
4267
4284
  refresh_token: str = Field(
4268
4285
  ...,
4269
- description='Dropbox OAuth 2.0 refresh token (obtained with token_access_type=offline)',
4286
+ description='Long-lived refresh token issued by the PKCE flow with token_access_type=offline',
4287
+ )
4288
+
4289
+
4290
+ class DropboxMaskedAccessToken(BaseModel):
4291
+ model_config = ConfigDict(
4292
+ extra='forbid',
4293
+ )
4294
+ access_token: str = Field(
4295
+ ...,
4296
+ description='Short-lived Dropbox access token (starts with sl.). Expires roughly four hours after it is generated.',
4270
4297
  )
4271
4298
 
4272
4299
 
@@ -4386,11 +4413,13 @@ class DropboxInput(CoreInput):
4386
4413
  type: Literal['DROPBOX'] | None = Field(
4387
4414
  None, description='Type of the asset or source'
4388
4415
  )
4389
- required: DropboxRequiredAccessToken | DropboxRequiredOAuth = Field(
4390
- ..., title='DropboxRequired'
4391
- )
4392
- masked: DropboxMaskedAccessToken | DropboxMaskedOAuth = Field(
4393
- ..., title='DropboxMasked'
4416
+ required: (
4417
+ DropboxOAuthRefreshTokenRecommended
4418
+ | DropboxOAuthWithPKCENoAppSecret
4419
+ | DropboxAccessTokenShortLivedTestingOnly
4420
+ ) = Field(..., title='DropboxRequired')
4421
+ masked: DropboxMaskedOAuth | DropboxMaskedOAuthPkce | DropboxMaskedAccessToken = (
4422
+ Field(..., title='DropboxMasked')
4394
4423
  )
4395
4424
  optional: DropboxOptional | None = None
4396
4425
  detectors: list[Detector] | None = Field(
@@ -0,0 +1,254 @@
1
+ """One-off Dropbox authorization helper.
2
+
3
+ Dropbox only hands out long-lived credentials through an interactive OAuth
4
+ authorization. A scan runner has no browser and no logged-in user, so the
5
+ exchange has to happen once, up front, on a machine where a human is present.
6
+
7
+ ``DropboxOAuth2FlowNoRedirect`` is built exactly for that: it needs no redirect
8
+ URI and no callback server. It prints a URL, the operator opens it in their own
9
+ browser, Dropbox shows them a short authorization code, and the code is pasted
10
+ back here and exchanged for a refresh token. Only that refresh token — which
11
+ does not expire — is ever stored on a source.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import sys
19
+ from typing import Any
20
+
21
+ from ..dependencies import require_module
22
+
23
+ # Scopes each mode needs. Dropbox binds scopes to the refresh token at
24
+ # authorization time, so anything missing here means a `missing_scope` error at
25
+ # scan time and a fresh authorization to fix it.
26
+ INDIVIDUAL_SCOPES = [
27
+ "account_info.read",
28
+ "files.metadata.read",
29
+ "files.content.read",
30
+ "sharing.read",
31
+ ]
32
+
33
+ TEAM_SCOPES = [
34
+ *INDIVIDUAL_SCOPES,
35
+ "team_info.read",
36
+ "members.read",
37
+ "team_data.member",
38
+ "team_data.team_space",
39
+ ]
40
+
41
+
42
+ def default_scopes(*, team: bool) -> list[str]:
43
+ return list(TEAM_SCOPES if team else INDIVIDUAL_SCOPES)
44
+
45
+
46
+ def build_flow(
47
+ *,
48
+ app_key: str,
49
+ app_secret: str | None,
50
+ scopes: list[str],
51
+ use_pkce: bool,
52
+ ) -> Any:
53
+ """Create the no-redirect authorization flow.
54
+
55
+ ``token_access_type="offline"`` is what makes Dropbox return a refresh
56
+ token; without it the exchange yields only a four-hour access token.
57
+ """
58
+ oauth = require_module(
59
+ module_name="dropbox.oauth",
60
+ source_name="Dropbox",
61
+ uv_groups=["dropbox"],
62
+ detail="Dropbox authorization requires the official Dropbox SDK.",
63
+ )
64
+ return oauth.DropboxOAuth2FlowNoRedirect(
65
+ app_key,
66
+ consumer_secret=None if use_pkce else app_secret,
67
+ token_access_type="offline",
68
+ scope=scopes,
69
+ use_pkce=use_pkce,
70
+ )
71
+
72
+
73
+ def verify_credentials(
74
+ *,
75
+ app_key: str,
76
+ app_secret: str | None,
77
+ refresh_token: str,
78
+ team: bool,
79
+ ) -> str:
80
+ """Prove the refresh token works before printing it as a recipe."""
81
+ dropbox_module = require_module(
82
+ module_name="dropbox",
83
+ source_name="Dropbox",
84
+ uv_groups=["dropbox"],
85
+ detail="Dropbox authorization requires the official Dropbox SDK.",
86
+ )
87
+ kwargs: dict[str, Any] = {
88
+ "oauth2_refresh_token": refresh_token,
89
+ "app_key": app_key,
90
+ "user_agent": "classifyre",
91
+ }
92
+ if app_secret:
93
+ kwargs["app_secret"] = app_secret
94
+
95
+ if team:
96
+ client = dropbox_module.DropboxTeam(**kwargs)
97
+ info = client.team_get_info()
98
+ return (
99
+ f"team '{getattr(info, 'name', '')}' ({getattr(info, 'num_licensed_users', '?')} seats)"
100
+ )
101
+
102
+ client = dropbox_module.Dropbox(**kwargs)
103
+ account = client.users_get_current_account()
104
+ display_name = getattr(getattr(account, "name", None), "display_name", "") or ""
105
+ return f"{display_name} <{getattr(account, 'email', '')}>"
106
+
107
+
108
+ def build_recipe_credentials(
109
+ *,
110
+ app_key: str,
111
+ app_secret: str | None,
112
+ refresh_token: str,
113
+ use_pkce: bool,
114
+ ) -> dict[str, Any]:
115
+ """The `required`/`masked` blocks to paste into a DROPBOX source."""
116
+ if use_pkce:
117
+ return {
118
+ "required": {"auth_method": "oauth_pkce", "app_key": app_key},
119
+ "masked": {"refresh_token": refresh_token},
120
+ }
121
+ return {
122
+ "required": {"auth_method": "oauth", "app_key": app_key},
123
+ "masked": {"app_secret": app_secret or "", "refresh_token": refresh_token},
124
+ }
125
+
126
+
127
+ def run_dropbox_auth_command(args: argparse.Namespace) -> int:
128
+ app_key = str(getattr(args, "app_key", "") or "").strip()
129
+ app_secret = str(getattr(args, "app_secret", "") or "").strip() or None
130
+ use_pkce = bool(getattr(args, "pkce", False))
131
+ team = bool(getattr(args, "team", False))
132
+
133
+ if not app_key:
134
+ print("--app-key is required (Dropbox App Console → Settings → App key)")
135
+ return 1
136
+ if not use_pkce and not app_secret:
137
+ print(
138
+ "--app-secret is required unless --pkce is used.\n"
139
+ "Use --pkce for an app that cannot hold a secret; it refreshes with the app key alone."
140
+ )
141
+ return 1
142
+
143
+ scopes = list(getattr(args, "scope", None) or []) or default_scopes(team=team)
144
+
145
+ try:
146
+ flow = build_flow(
147
+ app_key=app_key,
148
+ app_secret=app_secret,
149
+ scopes=scopes,
150
+ use_pkce=use_pkce,
151
+ )
152
+ authorize_url = flow.start()
153
+ except Exception as exc:
154
+ print(f"Failed to start the Dropbox authorization flow: {exc}")
155
+ return 1
156
+
157
+ print()
158
+ print("Dropbox authorization — this runs once; the runner never needs a browser.")
159
+ print(f" Mode: {'PKCE (no app secret)' if use_pkce else 'app key + secret'}")
160
+ print(f" Target: {'Dropbox Business team' if team else 'individual account'}")
161
+ print(f" Scopes: {' '.join(scopes)}")
162
+ print()
163
+ print("1. Open this URL in your browser:")
164
+ print(f" {authorize_url}")
165
+ print('2. Sign in if needed, then click "Allow".')
166
+ print("3. Copy the authorization code Dropbox shows you.")
167
+ print()
168
+
169
+ try:
170
+ auth_code = input("Authorization code: ").strip()
171
+ except (EOFError, KeyboardInterrupt):
172
+ print("\nAborted.")
173
+ return 1
174
+
175
+ if not auth_code:
176
+ print("No authorization code entered.")
177
+ return 1
178
+
179
+ try:
180
+ result = flow.finish(auth_code)
181
+ except Exception as exc:
182
+ print(f"Failed to exchange the authorization code: {exc}")
183
+ return 1
184
+
185
+ refresh_token = getattr(result, "refresh_token", None)
186
+ if not refresh_token:
187
+ print(
188
+ "Dropbox returned no refresh token. The app must be authorized with "
189
+ "token_access_type=offline; re-run this command."
190
+ )
191
+ return 1
192
+
193
+ try:
194
+ identity = verify_credentials(
195
+ app_key=app_key,
196
+ app_secret=app_secret,
197
+ refresh_token=refresh_token,
198
+ team=team,
199
+ )
200
+ except Exception as exc:
201
+ print(f"Refresh token issued, but the verification call failed: {exc}")
202
+ if team:
203
+ print(
204
+ "For a team scan the Dropbox app must use a team access type "
205
+ "('Team member file access')."
206
+ )
207
+ return 1
208
+
209
+ credentials = build_recipe_credentials(
210
+ app_key=app_key,
211
+ app_secret=app_secret,
212
+ refresh_token=refresh_token,
213
+ use_pkce=use_pkce,
214
+ )
215
+
216
+ print()
217
+ print(f"Authorized as {identity}.")
218
+ print("Paste these into the Dropbox source configuration:")
219
+ print()
220
+ print(json.dumps(credentials, indent=2))
221
+ print()
222
+ print("The refresh token does not expire. Treat it as a secret.")
223
+ return 0
224
+
225
+
226
+ def add_dropbox_auth_arguments(parser: argparse.ArgumentParser) -> None:
227
+ parser.add_argument("--app-key", default=None, help="Dropbox app key (dropbox-auth only)")
228
+ parser.add_argument(
229
+ "--app-secret",
230
+ default=None,
231
+ help="Dropbox app secret; omit when using --pkce (dropbox-auth only)",
232
+ )
233
+ parser.add_argument(
234
+ "--pkce",
235
+ action="store_true",
236
+ help="Authorize with PKCE so no app secret is needed (dropbox-auth only)",
237
+ )
238
+ parser.add_argument(
239
+ "--team",
240
+ action="store_true",
241
+ help="Authorize a Dropbox Business team app and request team scopes (dropbox-auth only)",
242
+ )
243
+ parser.add_argument(
244
+ "--scope",
245
+ action="append",
246
+ default=None,
247
+ help="Override a requested OAuth scope; repeatable (dropbox-auth only)",
248
+ )
249
+
250
+
251
+ def main() -> None: # pragma: no cover - thin argparse wrapper
252
+ parser = argparse.ArgumentParser(description="Authorize Classifyre against Dropbox")
253
+ add_dropbox_auth_arguments(parser)
254
+ sys.exit(run_dropbox_auth_command(parser.parse_args()))
@@ -7,7 +7,21 @@ from datetime import UTC, datetime
7
7
  from typing import Any
8
8
  from urllib.parse import quote
9
9
 
10
- from ...models.generated_input import DropboxInput, DropboxRequiredOAuth
10
+ # The generated model names come from each auth branch's schema `title`, which
11
+ # is also what the source form shows in its auth dropdown. Alias them back to
12
+ # stable names so renaming a label never churns this module.
13
+ from ...models.generated_input import (
14
+ DropboxAccessTokenShortLivedTestingOnly as DropboxRequiredAccessToken,
15
+ )
16
+ from ...models.generated_input import (
17
+ DropboxInput,
18
+ )
19
+ from ...models.generated_input import (
20
+ DropboxOAuthRefreshTokenRecommended as DropboxRequiredOAuth,
21
+ )
22
+ from ...models.generated_input import (
23
+ DropboxOAuthWithPKCENoAppSecret as DropboxRequiredOAuthPkce,
24
+ )
11
25
  from ...models.generated_single_asset_scan_results import SingleAssetScanResults
12
26
  from ..dependencies import require_module
13
27
  from ..object_storage.base import ObjectRef, ObjectStorageSourceBase
@@ -178,6 +192,11 @@ class DropboxSource(ObjectStorageSourceBase):
178
192
  Both client classes take the same credentials; what differs between an
179
193
  individual and a Business scan is the access type granted to the app,
180
194
  not the shape of the secret.
195
+
196
+ Refresh-token modes are the only ones that survive a scheduled run: the
197
+ SDK exchanges the refresh token for a new access token on every call
198
+ that needs one. PKCE apps refresh with the app key alone, so
199
+ ``app_secret`` is omitted rather than empty.
181
200
  """
182
201
  common: dict[str, Any] = {
183
202
  "timeout": self._request_timeout_seconds(),
@@ -187,27 +206,64 @@ class DropboxSource(ObjectStorageSourceBase):
187
206
  }
188
207
 
189
208
  required = self.config.required
190
- if isinstance(required, DropboxRequiredOAuth):
209
+
210
+ if isinstance(required, DropboxRequiredOAuth | DropboxRequiredOAuthPkce):
191
211
  app_key = str(required.app_key or "").strip()
192
- app_secret = self._masked_value("app_secret")
193
212
  refresh_token = self._masked_value("refresh_token")
194
- if not app_key or not app_secret or not refresh_token:
213
+ if not app_key or not refresh_token:
195
214
  raise ValueError(
196
- "Dropbox OAuth authentication requires required.app_key, "
197
- "masked.app_secret and masked.refresh_token"
215
+ "Dropbox refresh-token authentication requires required.app_key "
216
+ "and masked.refresh_token. Run "
217
+ "`classifyre dropbox-auth --app-key <key>` to obtain one."
198
218
  )
199
- return {
219
+ credentials: dict[str, Any] = {
200
220
  "oauth2_refresh_token": refresh_token,
201
221
  "app_key": app_key,
202
- "app_secret": app_secret,
203
222
  **common,
204
223
  }
224
+ if isinstance(required, DropboxRequiredOAuth):
225
+ app_secret = self._masked_value("app_secret")
226
+ if not app_secret:
227
+ raise ValueError(
228
+ "Dropbox OAuth authentication requires masked.app_secret. "
229
+ "Use auth_method 'oauth_pkce' for an app that holds no secret."
230
+ )
231
+ credentials["app_secret"] = app_secret
232
+ return credentials
205
233
 
206
234
  access_token = self._masked_value("access_token")
207
235
  if not access_token:
208
236
  raise ValueError("Dropbox access token authentication requires masked.access_token")
209
237
  return {"oauth2_access_token": access_token, **common}
210
238
 
239
+ def _uses_short_lived_token(self) -> bool:
240
+ return isinstance(self.config.required, DropboxRequiredAccessToken)
241
+
242
+ def _explain_auth_failure(self, exc: Exception) -> str:
243
+ """Turn Dropbox's opaque auth errors into something actionable."""
244
+ text = str(exc)
245
+ if "invalid_access_token" in text and self._uses_short_lived_token():
246
+ return (
247
+ "the access token is no longer valid. App Console tokens are "
248
+ "short-lived and stop working a few hours after they are generated. "
249
+ "Switch this source to auth_method 'oauth' (or 'oauth_pkce') and run "
250
+ "`classifyre dropbox-auth --app-key <key> --app-secret <secret>` to "
251
+ "get a refresh token that does not expire."
252
+ )
253
+ if "invalid_access_token" in text:
254
+ return (
255
+ "the refresh token was rejected. It may have been revoked, or it may "
256
+ "belong to a different Dropbox app than the configured app key. "
257
+ "Re-run `classifyre dropbox-auth` to issue a new one."
258
+ )
259
+ if "missing_scope" in text:
260
+ return (
261
+ f"{text} — the refresh token was issued without a scope this scan needs. "
262
+ "Re-run `classifyre dropbox-auth` with the required --scope values so the "
263
+ "authorization covers them."
264
+ )
265
+ return text
266
+
211
267
  def _build_client(self) -> Any:
212
268
  return self._sdk().Dropbox(**self._credential_kwargs())
213
269
 
@@ -655,7 +711,7 @@ class DropboxSource(ObjectStorageSourceBase):
655
711
  )
656
712
  except Exception as exc:
657
713
  result["status"] = "FAILURE"
658
- result["message"] = f"Failed to connect to Dropbox: {exc}"
714
+ result["message"] = f"Failed to connect to Dropbox: {self._explain_auth_failure(exc)}"
659
715
  return result
660
716
 
661
717
  async def extract_raw(self) -> AsyncGenerator[list[SingleAssetScanResults], None]: