tabalyst 0.6.1__tar.gz → 0.6.2__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 (205) hide show
  1. {tabalyst-0.6.1 → tabalyst-0.6.2}/MANIFEST.in +1 -0
  2. {tabalyst-0.6.1/src/tabalyst.egg-info → tabalyst-0.6.2}/PKG-INFO +31 -2
  3. {tabalyst-0.6.1 → tabalyst-0.6.2}/README.md +29 -1
  4. tabalyst-0.6.2/examples/README.md +144 -0
  5. {tabalyst-0.6.1 → tabalyst-0.6.2}/pyproject.toml +3 -2
  6. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/__init__.py +8 -0
  7. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cli/app.py +2 -0
  8. tabalyst-0.6.2/src/tabalyst/cli/generate.py +67 -0
  9. tabalyst-0.6.2/src/tabalyst/generate_anomalies.py +126 -0
  10. tabalyst-0.6.2/src/tabalyst/generate_csv.py +34 -0
  11. tabalyst-0.6.2/src/tabalyst/generate_definition.py +596 -0
  12. tabalyst-0.6.2/src/tabalyst/generate_examples/crm.yaml +70 -0
  13. tabalyst-0.6.2/src/tabalyst/generate_examples/insurance.yaml +71 -0
  14. tabalyst-0.6.2/src/tabalyst/generate_insurance.py +208 -0
  15. tabalyst-0.6.2/src/tabalyst/generate_references.py +221 -0
  16. tabalyst-0.6.2/src/tabalyst/generate_resources/SOURCES.md +27 -0
  17. tabalyst-0.6.2/src/tabalyst/generate_resources/addresses/street_directions_ca.csv +9 -0
  18. tabalyst-0.6.2/src/tabalyst/generate_resources/addresses/street_names_ca.csv +109 -0
  19. tabalyst-0.6.2/src/tabalyst/generate_resources/addresses/street_types_ca.csv +32 -0
  20. tabalyst-0.6.2/src/tabalyst/generate_resources/addresses/unit_types_ca.csv +12 -0
  21. tabalyst-0.6.2/src/tabalyst/generate_resources/contact/email_patterns.csv +12 -0
  22. tabalyst-0.6.2/src/tabalyst/generate_resources/contact/email_providers_ca.csv +10 -0
  23. tabalyst-0.6.2/src/tabalyst/generate_resources/contact/phone_area_codes_ca.csv +14 -0
  24. tabalyst-0.6.2/src/tabalyst/generate_resources/geography/cities_ca.csv +72 -0
  25. tabalyst-0.6.2/src/tabalyst/generate_resources/geography/postal_prefixes_ca.csv +72 -0
  26. tabalyst-0.6.2/src/tabalyst/generate_resources/geography/provinces_ca.csv +14 -0
  27. tabalyst-0.6.2/src/tabalyst/generate_resources/insurance/SOURCES.md +23 -0
  28. tabalyst-0.6.2/src/tabalyst/generate_resources/insurance/agencies.csv +21 -0
  29. tabalyst-0.6.2/src/tabalyst/generate_resources/insurance/branch_contract_weights.csv +13 -0
  30. tabalyst-0.6.2/src/tabalyst/generate_resources/insurance/claim_statuses.csv +5 -0
  31. tabalyst-0.6.2/src/tabalyst/generate_resources/insurance/contract_statuses.csv +5 -0
  32. tabalyst-0.6.2/src/tabalyst/generate_resources/insurance/contract_types.csv +5 -0
  33. tabalyst-0.6.2/src/tabalyst/generate_resources/insurance/payment_frequencies.csv +5 -0
  34. tabalyst-0.6.2/src/tabalyst/generate_resources/insurance/payment_methods.csv +5 -0
  35. tabalyst-0.6.2/src/tabalyst/generate_resources/people/family_names_ca.csv +136 -0
  36. tabalyst-0.6.2/src/tabalyst/generate_resources/people/first_names_en_ca.csv +81 -0
  37. tabalyst-0.6.2/src/tabalyst/generate_resources/people/first_names_fr_qc.csv +101 -0
  38. tabalyst-0.6.2/src/tabalyst/generate_service.py +367 -0
  39. tabalyst-0.6.2/src/tabalyst/generate_values.py +199 -0
  40. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/models.py +2 -2
  41. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/models.py +2 -2
  42. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/models.py +3 -3
  43. {tabalyst-0.6.1 → tabalyst-0.6.2/src/tabalyst.egg-info}/PKG-INFO +31 -2
  44. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst.egg-info/SOURCES.txt +37 -0
  45. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst.egg-info/requires.txt +1 -0
  46. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_cli.py +2 -2
  47. tabalyst-0.6.2/tests/test_generate.py +368 -0
  48. tabalyst-0.6.2/tests/test_generate_crm.py +51 -0
  49. tabalyst-0.6.2/tests/test_generate_insurance.py +73 -0
  50. tabalyst-0.6.2/tests/test_generate_lot4.py +122 -0
  51. tabalyst-0.6.2/tests/test_generate_references.py +116 -0
  52. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_index.py +5 -3
  53. tabalyst-0.6.1/examples/README.md +0 -75
  54. {tabalyst-0.6.1 → tabalyst-0.6.2}/LICENSE +0 -0
  55. {tabalyst-0.6.1 → tabalyst-0.6.2}/examples/config.json +0 -0
  56. {tabalyst-0.6.1 → tabalyst-0.6.2}/examples/input/basic.csv +0 -0
  57. {tabalyst-0.6.1 → tabalyst-0.6.2}/examples/input/insurance-customers.csv +0 -0
  58. {tabalyst-0.6.1 → tabalyst-0.6.2}/setup.cfg +0 -0
  59. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/__main__.py +0 -0
  60. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/_version.py +0 -0
  61. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/batch.py +0 -0
  62. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cache_service.py +0 -0
  63. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cli/__init__.py +0 -0
  64. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cli/cache.py +0 -0
  65. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cli/inspect.py +0 -0
  66. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cli/report.py +0 -0
  67. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cli/sample.py +0 -0
  68. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cli/scan.py +0 -0
  69. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cli/terminal.py +0 -0
  70. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/config.py +0 -0
  71. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/errors.py +0 -0
  72. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/execution_log.py +0 -0
  73. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspect_service.py +0 -0
  74. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/__init__.py +0 -0
  75. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/choices.py +0 -0
  76. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/excel_inspect/__init__.py +0 -0
  77. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/excel_inspect/build.py +0 -0
  78. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/excel_inspect/detect.py +0 -0
  79. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/excel_inspect/parameters.py +0 -0
  80. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/json_inspect/__init__.py +0 -0
  81. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/json_inspect/build.py +0 -0
  82. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/json_inspect/detect.py +0 -0
  83. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/json_inspect/parameters.py +0 -0
  84. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/persistence.py +0 -0
  85. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/resolution.py +0 -0
  86. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/option_notices.py +0 -0
  87. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/progress.py +0 -0
  88. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/project_open_service.py +0 -0
  89. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/project_scan_service.py +0 -0
  90. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/__init__.py +0 -0
  91. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_bounded_values.py +0 -0
  92. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_cache.py +0 -0
  93. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_codec.py +0 -0
  94. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_generation.py +0 -0
  95. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_locks.py +0 -0
  96. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_publication.py +0 -0
  97. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_queries.py +0 -0
  98. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_query_budget.py +0 -0
  99. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_recovery.py +0 -0
  100. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_retention.py +0 -0
  101. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_schema.py +0 -0
  102. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_session.py +0 -0
  103. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_staging.py +0 -0
  104. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_sync.py +0 -0
  105. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_validation.py +0 -0
  106. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/catalog.py +0 -0
  107. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/freshness.py +0 -0
  108. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/identity.py +0 -0
  109. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/index.py +0 -0
  110. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/location.py +0 -0
  111. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/models.py +0 -0
  112. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/store.py +0 -0
  113. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/report_config.py +0 -0
  114. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/report_profile.py +0 -0
  115. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/report_service.py +0 -0
  116. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/reporting.py +0 -0
  117. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/sampling.py +0 -0
  118. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/sampling_service.py +0 -0
  119. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scan_reuse.py +0 -0
  120. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scan_service.py +0 -0
  121. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/__init__.py +0 -0
  122. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/api.py +0 -0
  123. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/config.py +0 -0
  124. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/__init__.py +0 -0
  125. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/amount.py +0 -0
  126. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/base.py +0 -0
  127. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/boolean.py +0 -0
  128. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/currency.py +0 -0
  129. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/email.py +0 -0
  130. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/enumeration.py +0 -0
  131. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/ip.py +0 -0
  132. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/names.py +0 -0
  133. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/number.py +0 -0
  134. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/pattern.py +0 -0
  135. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/percentage.py +0 -0
  136. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/phone.py +0 -0
  137. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/postal.py +0 -0
  138. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/quantity.py +0 -0
  139. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/registry.py +0 -0
  140. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/shape.py +0 -0
  141. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/temporal.py +0 -0
  142. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/url.py +0 -0
  143. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/uuid.py +0 -0
  144. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/diagnostics.py +0 -0
  145. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/engine.py +0 -0
  146. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/exposure.py +0 -0
  147. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/field.py +0 -0
  148. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/identity.py +0 -0
  149. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/measures.py +0 -0
  150. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/normalization.py +0 -0
  151. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/observations.py +0 -0
  152. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/paths.py +0 -0
  153. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/__init__.py +0 -0
  154. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/base.py +0 -0
  155. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/csv_reader.py +0 -0
  156. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/excel_common.py +0 -0
  157. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/excel_reader.py +0 -0
  158. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/excel_table.py +0 -0
  159. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/json_common.py +0 -0
  160. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/json_reader.py +0 -0
  161. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/jsonl_reader.py +0 -0
  162. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/records.py +0 -0
  163. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/structure.py +0 -0
  164. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/technical.py +0 -0
  165. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/values.py +0 -0
  166. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/workers.py +0 -0
  167. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/service.py +0 -0
  168. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/shared_scan_service.py +0 -0
  169. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/static/report.js +0 -0
  170. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/static/theme.css +0 -0
  171. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/templates/column.html +0 -0
  172. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/templates/report.html +0 -0
  173. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst.egg-info/dependency_links.txt +0 -0
  174. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst.egg-info/entry_points.txt +0 -0
  175. {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst.egg-info/top_level.txt +0 -0
  176. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_all_collections.py +0 -0
  177. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_license_headers.py +0 -0
  178. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_option_notices.py +0 -0
  179. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_cache.py +0 -0
  180. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_database.py +0 -0
  181. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_generations.py +0 -0
  182. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_identity.py +0 -0
  183. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_location.py +0 -0
  184. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_open_service.py +0 -0
  185. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_queries.py +0 -0
  186. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_rescan.py +0 -0
  187. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_scan.py +0 -0
  188. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_sessions.py +0 -0
  189. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_store.py +0 -0
  190. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_public_api.py +0 -0
  191. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_reporting.py +0 -0
  192. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_sampling.py +0 -0
  193. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scan_command.py +0 -0
  194. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_amounts.py +0 -0
  195. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_batches.py +0 -0
  196. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_config.py +0 -0
  197. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_csv.py +0 -0
  198. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_email_url.py +0 -0
  199. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_exposure.py +0 -0
  200. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_paths.py +0 -0
  201. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_phone.py +0 -0
  202. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_postal.py +0 -0
  203. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_uuid_ip.py +0 -0
  204. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_shared_scan_cli.py +0 -0
  205. {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_source_freshness.py +0 -0
@@ -3,3 +3,4 @@ include README.md
3
3
  include examples/README.md
4
4
  include examples/config.json
5
5
  recursive-include examples/input *.csv
6
+ recursive-include src/tabalyst/generate_resources *.csv *.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tabalyst
3
- Version: 0.6.1
3
+ Version: 0.6.2
4
4
  Summary: Open-source toolkit for understanding and working with unfamiliar data.
5
5
  Author: Gregory Borelli
6
6
  License-Expression: MPL-2.0
@@ -27,6 +27,7 @@ Requires-Dist: jinja2>=3.1
27
27
  Requires-Dist: platformdirs>=4
28
28
  Requires-Dist: pydantic>=2.11
29
29
  Requires-Dist: python-calamine>=0.8
30
+ Requires-Dist: PyYAML>=6.0
30
31
  Requires-Dist: typer>=0.12
31
32
  Provides-Extra: dev
32
33
  Requires-Dist: build>=1.2; extra == "dev"
@@ -48,7 +49,8 @@ Its first tool is **Tabalyst Report**. The current CSV implementation,
48
49
  profile and a self-contained interactive HTML report.
49
50
  **Tabalyst Scan** describes CSV, JSON, JSONL and Excel files in a scan document,
50
51
  **Tabalyst Inspect** finds how to read a JSON, JSONL or Excel file, and
51
- **Tabalyst Sample** creates smaller CSV files.
52
+ **Tabalyst Sample** creates smaller CSV files by sampling, and **Tabalyst
53
+ Generate** creates synthetic CSV datasets.
52
54
 
53
55
  Tabalyst is in beta. Its interfaces may still change while
54
56
  the shared toolkit architecture is being established.
@@ -273,6 +275,33 @@ Sampling reads CSV records as a stream. Random and stratified sampling keep
273
275
  only the requested sample, plus stratum counts, in memory. Existing outputs
274
276
  require `--force`, and an input file is never overwritten.
275
277
 
278
+ ## Tabalyst Generate
279
+
280
+ Generate synthetic CSV data from a versioned YAML definition. The packaged
281
+ `crm` example writes one contacts file; `insurance`
282
+ writes three branch files and a combined file:
283
+
284
+ ```console
285
+ tabalyst generate --example crm -o crm-contacts.csv
286
+ tabalyst generate --example insurance --rows 3000 -d insurance-output/
287
+ tabalyst generate --example insurance --rows 3000 --anomaly-profile dirty_realistic -d insurance-dirty/
288
+ ```
289
+
290
+ You can pass `examples/generate/input/crm.yaml` or
291
+ `examples/generate/input/insurance.yaml` instead of `--example`. A selected anomaly
292
+ profile writes an altered CSV, a cell change log and a summary beside the
293
+ untouched clean CSV. Insurance's profile changes only the combined file.
294
+ `--rows 3000` apportions 2,182, 273 and 545 rows to the three branches.
295
+ Existing outputs require `--force`.
296
+
297
+ Run `python examples/generate/generate_examples.py` to generate every YAML
298
+ from `examples/generate/input/` into ignored local output folders.
299
+
300
+ Insurance preserves the 33 source columns and the combined `filiale` column of
301
+ the existing public demo, but its generated rows are different. The branch date
302
+ and identifier formats are preserved; `filiale` is the last column in the new
303
+ combined CSV. The current public demo and report remain unchanged.
304
+
276
305
  ## Tabalyst Inspect
277
306
 
278
307
  A JSON file can hold several arrays, and an Excel workbook several sheets.
@@ -10,7 +10,8 @@ Its first tool is **Tabalyst Report**. The current CSV implementation,
10
10
  profile and a self-contained interactive HTML report.
11
11
  **Tabalyst Scan** describes CSV, JSON, JSONL and Excel files in a scan document,
12
12
  **Tabalyst Inspect** finds how to read a JSON, JSONL or Excel file, and
13
- **Tabalyst Sample** creates smaller CSV files.
13
+ **Tabalyst Sample** creates smaller CSV files by sampling, and **Tabalyst
14
+ Generate** creates synthetic CSV datasets.
14
15
 
15
16
  Tabalyst is in beta. Its interfaces may still change while
16
17
  the shared toolkit architecture is being established.
@@ -235,6 +236,33 @@ Sampling reads CSV records as a stream. Random and stratified sampling keep
235
236
  only the requested sample, plus stratum counts, in memory. Existing outputs
236
237
  require `--force`, and an input file is never overwritten.
237
238
 
239
+ ## Tabalyst Generate
240
+
241
+ Generate synthetic CSV data from a versioned YAML definition. The packaged
242
+ `crm` example writes one contacts file; `insurance`
243
+ writes three branch files and a combined file:
244
+
245
+ ```console
246
+ tabalyst generate --example crm -o crm-contacts.csv
247
+ tabalyst generate --example insurance --rows 3000 -d insurance-output/
248
+ tabalyst generate --example insurance --rows 3000 --anomaly-profile dirty_realistic -d insurance-dirty/
249
+ ```
250
+
251
+ You can pass `examples/generate/input/crm.yaml` or
252
+ `examples/generate/input/insurance.yaml` instead of `--example`. A selected anomaly
253
+ profile writes an altered CSV, a cell change log and a summary beside the
254
+ untouched clean CSV. Insurance's profile changes only the combined file.
255
+ `--rows 3000` apportions 2,182, 273 and 545 rows to the three branches.
256
+ Existing outputs require `--force`.
257
+
258
+ Run `python examples/generate/generate_examples.py` to generate every YAML
259
+ from `examples/generate/input/` into ignored local output folders.
260
+
261
+ Insurance preserves the 33 source columns and the combined `filiale` column of
262
+ the existing public demo, but its generated rows are different. The branch date
263
+ and identifier formats are preserved; `filiale` is the last column in the new
264
+ combined CSV. The current public demo and report remain unchanged.
265
+
238
266
  ## Tabalyst Inspect
239
267
 
240
268
  A JSON file can hold several arrays, and an Excel workbook several sheets.
@@ -0,0 +1,144 @@
1
+ # Tabalyst examples
2
+
3
+ The examples are organized by input name so additional public datasets can be
4
+ added without mixing source data and generated artifacts.
5
+
6
+ ```text
7
+ examples/
8
+ |-- config.json
9
+ |-- generate/
10
+ | |-- generate_examples.py
11
+ | |-- input/ (four YAML definitions)
12
+ | `-- output/ (generated locally; ignored by Git)
13
+ |-- input/
14
+ | |-- basic.csv
15
+ | |-- insurance-customers.csv
16
+ | |-- orders.json
17
+ | |-- sales.xlsx
18
+ | `-- web-events.jsonl
19
+ `-- output/
20
+ |-- basic/
21
+ | |-- report.html
22
+ | |-- report.json
23
+ | `-- executions.json
24
+ |-- insurance-customers/
25
+ | |-- report.html
26
+ | |-- report.json
27
+ | |-- report/ (34 standalone column pages with --details)
28
+ | `-- executions.json
29
+ |-- orders/
30
+ | |-- report.html
31
+ | |-- report.json
32
+ | `-- executions.json
33
+ |-- sales/
34
+ | |-- report.html
35
+ | |-- report.json
36
+ | `-- executions.json
37
+ `-- web-events/
38
+ |-- report.html
39
+ |-- report.json
40
+ `-- executions.json
41
+ ```
42
+
43
+ `basic.csv` is a five-row smoke example. `insurance-customers.csv` contains 3,000
44
+ synthetic customer and contract records across 34 columns. Its people, contact
45
+ details, and contracts are fictional; email addresses use reserved `.example`
46
+ domains. `orders.json` is a synthetic shop export: 60 customers with nested
47
+ addresses, tags and orders. Its report has one dataset, `$.customers[]`, the only
48
+ collection of arrays of objects that Tabalyst Inspect finds in it.
49
+ `web-events.jsonl` is a synthetic web analytics log in JSON Lines: 300 events with
50
+ nested `user` and `device` objects, tags arrays, null values and an optional
51
+ field that appears late, plus three bad lines (a truncated record, a line that
52
+ is not JSON and a JSON array instead of an object). Its report is partial on
53
+ purpose: the three lines are excluded and counted, the 300 events are analyzed.
54
+ Its people and identifiers are fictional. `sales.xlsx` is a synthetic Excel
55
+ workbook: a sheet `Orders` of 120 fictional orders under two title lines, a
56
+ five-row `Regions` lookup sheet and a `Notes` sheet. Inspect selects `$.Orders`,
57
+ which has more than ten times the rows of `$.Regions`, so the report needs no
58
+ choice. The workbook is written by `make_sales_workbook.py`, with the same
59
+ bytes on every run; it needs `pip install xlsxwriter` and is only to run when the
60
+ input itself must be regenerated.
61
+
62
+ From the repository root, regenerate the outputs with:
63
+
64
+ ```console
65
+ tabalyst report examples/input/basic.csv -o examples/output/basic/report.html --config examples/config.json --force
66
+ tabalyst report examples/input/insurance-customers.csv -o examples/output/insurance-customers/report.html --config examples/config.json --details --force
67
+ tabalyst report examples/input/orders.json -o examples/output/orders/report.html --config examples/config.json --force
68
+ tabalyst report examples/input/web-events.jsonl -o examples/output/web-events/report.html --config examples/config.json --force
69
+ tabalyst report examples/input/sales.xlsx -o examples/output/sales/report.html --config examples/config.json --force
70
+ ```
71
+
72
+ Every output folder is independent and contains its own HTML report, canonical
73
+ JSON profile and cumulative `executions.json` history. The insurance example
74
+ also has standalone HTML pages under `report/`. Delete an output folder
75
+ before regenerating when a fresh one-entry execution history is required.
76
+
77
+ The input datasets and generated output are public repository examples. The wheel
78
+ contains the runtime package, report resources, and packaged copies of the
79
+ `examples/generate/input/` YAML definitions. CRM and Insurance are local development
80
+ demos. Generate is under development
81
+ and is not advertised as a released feature. The CRM data is synthetic, with
82
+ reserved `.example` domains and fictional phone numbers. The configured 20,000
83
+ rows, generic account names and CRM audit story were approved for the example
84
+ on 2026-10-05. Generate remains local development work until a release.
85
+
86
+ To review the CRM export outside the checkout:
87
+
88
+ ```console
89
+ tabalyst generate --example crm --rows 500 --anomaly-profile dirty_realistic -d OUTPUT_DIR
90
+ tabalyst scan OUTPUT_DIR/crm-contacts.dirty_realistic.csv -o OUTPUT_DIR/scan.json
91
+ tabalyst report OUTPUT_DIR/scan.json --scan -o OUTPUT_DIR/report.html
92
+ ```
93
+
94
+ Scan and Report expose missing values, fragmented categories and normalized
95
+ variants. The mutation summary counts requested, applied and skipped changes.
96
+ Neither tool currently diagnoses cross-column CRM rules such as stage versus
97
+ probability.
98
+
99
+ `examples/generate/input/common-references.yaml` is a lot 3R development fixture.
100
+ It generates coherent synthetic Canadian person, address and contact fields
101
+ from packaged references. Its prefixes and area codes are synthetic, email
102
+ domains end in `.example`, and phone line numbers use reserved `555-01xx`.
103
+ The Insurance example is now complete in `examples/generate/input/insurance.yaml`
104
+ and the packaged `--example insurance` copy. Its seven domain tables are in
105
+ `src/tabalyst/generate_resources/insurance/`; their provenance is in
106
+ `insurance/SOURCES.md`. To review 3,000 rows outside the checkout:
107
+
108
+ ```console
109
+ tabalyst generate --example insurance --rows 3000 --anomaly-profile dirty_realistic -d OUTPUT_DIR
110
+ ```
111
+
112
+ This creates clean files for Principal, Ontario and Quebec, their clean union,
113
+ and three anomaly artifacts for that union. The 2,182/273/545 branch allocation
114
+ comes from the declared 200,000/25,000/50,000 weights. The new clean CSV has
115
+ the same 34 column names as the existing public demo, but places `filiale` last
116
+ and has different rows and identifiers. Do not replace
117
+ `examples/input/insurance-customers.csv` or its report without a separate
118
+ decision to change the published demo.
119
+
120
+ `examples/generate/input/cohorts-anomalies.yaml` is a lot 4 development fixture. It
121
+ shows two normal status conventions in the clean CSV and a separate `dirty`
122
+ profile that writes an altered CSV, cell-level change log and summary. Run it
123
+ with `tabalyst generate examples/generate/input/cohorts-anomalies.yaml --anomaly-profile dirty -d OUTPUT_DIR`.
124
+ Keep `OUTPUT_DIR` outside the repository for one-off local trials. The packaged
125
+ Insurance YAML is the same complete example as the visible file.
126
+
127
+ ## Regenerate all Generate examples
128
+
129
+ Run the script from the repository root with an editable installation of
130
+ Tabalyst. It reads every YAML in `examples/generate/input/` and writes into
131
+ `examples/generate/output/<definition>/`. The output folder is ignored by Git.
132
+ By default it uses each YAML's declared row counts and generates every declared
133
+ anomaly profile along with its clean CSV. Insurance therefore writes 275,000
134
+ combined rows; allow enough disk space for the clean and altered copies.
135
+
136
+ ```console
137
+ python examples/generate/generate_examples.py
138
+ ```
139
+
140
+ Existing outputs are protected; pass `--force` to replace generated files.
141
+ For a quick review run, `--rows 30` overrides each definition's row count,
142
+ and `--output-dir PATH` writes outside the checkout. `--clean-only` skips the
143
+ profile passes. A definition with several profiles gets one subfolder per
144
+ profile so its clean files cannot collide.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tabalyst"
7
- version = "0.6.1"
7
+ version = "0.6.2"
8
8
  description = "Open-source toolkit for understanding and working with unfamiliar data."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -40,6 +40,7 @@ dependencies = [
40
40
  "platformdirs>=4",
41
41
  "pydantic>=2.11",
42
42
  "python-calamine>=0.8",
43
+ "PyYAML>=6.0",
43
44
  "typer>=0.12",
44
45
  ]
45
46
 
@@ -59,7 +60,7 @@ tabalyst = "tabalyst.cli:main"
59
60
  where = ["src"]
60
61
 
61
62
  [tool.setuptools.package-data]
62
- tabalyst = ["templates/*.html", "static/*.css", "static/*.js"]
63
+ tabalyst = ["templates/*.html", "static/*.css", "static/*.js", "generate_examples/*.yaml", "generate_resources/**/*.csv", "generate_resources/*.md"]
63
64
 
64
65
  [project.urls]
65
66
  Homepage = "https://github.com/loribel-labs/tabalyst"
@@ -6,6 +6,10 @@
6
6
 
7
7
  from tabalyst._version import __version__
8
8
  from tabalyst.errors import ConfigurationError, InputError, ReportError, TabalystError
9
+ from tabalyst.generate_service import GeneratePlan as GeneratePlan
10
+ from tabalyst.generate_service import GenerateResult as GenerateResult
11
+ from tabalyst.generate_service import build_generate_plan as build_generate_plan
12
+ from tabalyst.generate_service import generate_datasets as generate_datasets
9
13
  from tabalyst.inspect_service import InspectResult as InspectResult
10
14
  from tabalyst.inspect_service import generate_inspections as generate_inspections
11
15
  from tabalyst.inspect_service import inspect as inspect
@@ -26,6 +30,8 @@ from tabalyst.service import analyze_scan as analyze_scan
26
30
 
27
31
  __all__ = [
28
32
  "ConfigurationError",
33
+ "GeneratePlan",
34
+ "GenerateResult",
29
35
  "InputError",
30
36
  "InspectResult",
31
37
  "ReportError",
@@ -34,6 +40,8 @@ __all__ = [
34
40
  "TabalystError",
35
41
  "__version__",
36
42
  "analyze",
43
+ "build_generate_plan",
44
+ "generate_datasets",
37
45
  "generate_inspections",
38
46
  "generate_reports",
39
47
  "generate_samples",
@@ -10,6 +10,7 @@ import typer
10
10
 
11
11
  from tabalyst import __version__
12
12
  from tabalyst.cli.cache import cache_app
13
+ from tabalyst.cli.generate import generate_command
13
14
  from tabalyst.cli.inspect import inspect_command
14
15
  from tabalyst.cli.report import report_command
15
16
  from tabalyst.cli.sample import sample_command
@@ -45,6 +46,7 @@ def root(
45
46
 
46
47
 
47
48
  app.command("inspect")(inspect_command)
49
+ app.command("generate")(generate_command)
48
50
  app.command("report")(report_command)
49
51
  app.command("sample")(sample_command)
50
52
  app.command("scan")(scan_command)
@@ -0,0 +1,67 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+
5
+ """CLI adapter for Tabalyst Generate."""
6
+
7
+ from pathlib import Path
8
+ from typing import Annotated
9
+
10
+ import typer
11
+
12
+ from tabalyst.cli.terminal import error_exit_code
13
+ from tabalyst.errors import TabalystError
14
+ from tabalyst.generate_service import build_generate_plan, generate_datasets
15
+
16
+
17
+ def generate_command(
18
+ definition: Annotated[
19
+ Path | None,
20
+ typer.Argument(help="YAML Generate definition."),
21
+ ] = None,
22
+ example: Annotated[
23
+ str | None,
24
+ typer.Option("--example", help="Packaged example: crm or insurance."),
25
+ ] = None,
26
+ output: Annotated[
27
+ Path | None,
28
+ typer.Option("--output", "-o", help="One clean CSV file."),
29
+ ] = None,
30
+ output_dir: Annotated[
31
+ Path | None,
32
+ typer.Option("--output-dir", "-d", help="Directory for all artifacts."),
33
+ ] = None,
34
+ rows: Annotated[
35
+ int | None,
36
+ typer.Option("--rows", help="Number of generated rows."),
37
+ ] = None,
38
+ seed: Annotated[
39
+ int | None,
40
+ typer.Option("--seed", help="Root random seed."),
41
+ ] = None,
42
+ as_of_date: Annotated[
43
+ str | None,
44
+ typer.Option("--as-of-date", help="Reference date (YYYY-MM-DD)."),
45
+ ] = None,
46
+ anomaly_profile: Annotated[
47
+ str | None,
48
+ typer.Option("--anomaly-profile", help="Selected anomaly profile."),
49
+ ] = None,
50
+ force: Annotated[
51
+ bool,
52
+ typer.Option("--force", "-f", help="Replace existing artifacts."),
53
+ ] = False,
54
+ ) -> None:
55
+ """Validate and generate datasets from a YAML definition."""
56
+ try:
57
+ plan = build_generate_plan(
58
+ definition, example=example, output=output, output_dir=output_dir,
59
+ rows=rows, seed=seed, as_of_date=as_of_date,
60
+ anomaly_profile=anomaly_profile, force=force,
61
+ )
62
+ result = generate_datasets(plan)
63
+ except TabalystError as exc:
64
+ typer.echo(f"Error: {exc}", err=True)
65
+ raise typer.Exit(error_exit_code(exc)) from exc
66
+ for path in result.paths:
67
+ typer.echo(f"Created: {path}", err=True)
@@ -0,0 +1,126 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+
5
+ """Measured, streaming mutation pass over a staged clean Generate CSV."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import csv
10
+ import random
11
+ from collections import Counter
12
+ from pathlib import Path
13
+
14
+ from tabalyst.generate_values import _stream
15
+
16
+ CHANGE_FIELDS = ["event_id", "dataset", "profile", "row_number", "operation_index",
17
+ "kind", "column", "before", "after", "cause"]
18
+
19
+
20
+ def _mutate(row: dict[str, str], operation: dict, history: dict[str, list[str]],
21
+ rng: random.Random) -> dict[str, str]:
22
+ column = operation["column"]
23
+ before = row[column]
24
+ kind = operation["kind"]
25
+ if kind == "set_missing":
26
+ return {column: ""} if before else {}
27
+ if kind == "replace":
28
+ return {column: operation["value"]} if operation["value"] != before else {}
29
+ if kind == "duplicate":
30
+ choices = [value for value in history[column] if value and value != before]
31
+ return {column: rng.choice(choices)} if choices else {}
32
+ if kind == "swap":
33
+ other = operation["value"]
34
+ return {column: row[other], other: before} if before != row[other] else {}
35
+ transforms = {
36
+ "upper": str.upper,
37
+ "lower": str.lower,
38
+ "strip": str.strip,
39
+ "date_slash": lambda value: value.replace("-", "/"),
40
+ "phone_spaces": lambda value: value.replace("-", " "),
41
+ }
42
+ after = transforms[operation["value"]](before)
43
+ return {column: after} if before and after != before else {}
44
+
45
+
46
+ def inject_anomalies(clean: Path, altered: Path, changes: Path, *,
47
+ dataset: str, profile: dict, seed: int, count: int,
48
+ delimiter: str) -> dict:
49
+ """Write altered CSV and cell log; return requested and actual counts."""
50
+ operations = profile["operations"]
51
+ requested = [round(count * operation["rate"]) for operation in operations]
52
+ remaining = requested.copy()
53
+ selection_rngs = [_stream(seed, dataset, f"{profile['id']}:{index}",
54
+ "anomaly_selection") for index in range(len(operations))]
55
+ mutation_rngs = [_stream(seed, dataset, f"{profile['id']}:{index}", "anomaly_value")
56
+ for index in range(len(operations))]
57
+ applied: Counter[int] = Counter()
58
+ skipped: Counter[int] = Counter()
59
+ history: dict[str, list[str]] = {}
60
+ affected_rows = 0
61
+ changed_cells = 0
62
+ events = 0
63
+ with (clean.open("r", encoding="utf-8", newline="") as source,
64
+ altered.open("w", encoding="utf-8", newline="") as dirty,
65
+ changes.open("w", encoding="utf-8", newline="") as journal):
66
+ reader = csv.DictReader(source, delimiter=delimiter)
67
+ fields = reader.fieldnames or []
68
+ history = {field: [] for field in fields}
69
+ writer = csv.DictWriter(dirty, fieldnames=fields, delimiter=delimiter,
70
+ lineterminator="\n", extrasaction="raise")
71
+ log = csv.DictWriter(journal, fieldnames=CHANGE_FIELDS, lineterminator="\n")
72
+ writer.writeheader()
73
+ log.writeheader()
74
+ observed = 0
75
+ for row_index, clean_row in enumerate(reader):
76
+ observed += 1
77
+ row = clean_row.copy()
78
+ changed = False
79
+ for index, operation in enumerate(operations):
80
+ rows_left = count - row_index
81
+ selected = (remaining[index] == rows_left or
82
+ (remaining[index] > 0 and
83
+ selection_rngs[index].random() < remaining[index] / rows_left))
84
+ if not selected:
85
+ continue
86
+ remaining[index] -= 1
87
+ when = operation.get("when", {})
88
+ if any(row.get(field) != value for field, value in when.items()):
89
+ skipped[index] += 1
90
+ continue
91
+ updates = _mutate(row, operation, history, mutation_rngs[index])
92
+ if not updates:
93
+ skipped[index] += 1
94
+ continue
95
+ events += 1
96
+ for column, after in updates.items():
97
+ before = row[column]
98
+ row[column] = after
99
+ changed_cells += 1
100
+ log.writerow({"event_id": events, "dataset": dataset,
101
+ "profile": profile["id"], "row_number": row_index + 2,
102
+ "operation_index": index + 1, "kind": operation["kind"],
103
+ "column": column, "before": before, "after": after,
104
+ "cause": f"{profile['id']}.operations[{index}]"})
105
+ applied[index] += 1
106
+ changed = True
107
+ writer.writerow(row)
108
+ affected_rows += int(changed)
109
+ for field, value in clean_row.items():
110
+ if value and len(history[field]) < 1000:
111
+ history[field].append(value)
112
+ if observed != count:
113
+ raise ValueError(f"Clean row count changed for {dataset}: {observed} != {count}")
114
+ return {
115
+ "dataset": dataset, "profile": profile["id"], "seed": seed,
116
+ "total_rows": count, "affected_rows": affected_rows,
117
+ "affected_row_rate": affected_rows / count if count else 0.0,
118
+ "events": events, "changed_cells": changed_cells,
119
+ "operations": [
120
+ {"index": index + 1, "kind": operation["kind"], "column": operation["column"],
121
+ "rate": operation["rate"], "requested_rows": requested[index],
122
+ "applied_rows": applied[index], "skipped_rows": skipped[index],
123
+ "applied_rate": applied[index] / count if count else 0.0}
124
+ for index, operation in enumerate(operations)
125
+ ],
126
+ }
@@ -0,0 +1,34 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+
5
+ """Streaming CSV output adapted from dataset_factory.exporters.csv."""
6
+
7
+ import csv
8
+ from collections.abc import Iterable
9
+ from pathlib import Path
10
+
11
+
12
+ def write_csv(
13
+ path: Path,
14
+ fieldnames: list[str],
15
+ rows: Iterable[dict[str, str]],
16
+ *,
17
+ delimiter: str = ",",
18
+ ) -> int:
19
+ """Write a staged CSV using stable UTF-8/LF and strict field names."""
20
+ count = 0
21
+ with path.open("w", encoding="utf-8", newline="") as stream:
22
+ writer = csv.DictWriter(
23
+ stream,
24
+ fieldnames=fieldnames,
25
+ extrasaction="raise",
26
+ lineterminator="\n",
27
+ delimiter=delimiter,
28
+ )
29
+ writer.writeheader()
30
+ for row in rows:
31
+ writer.writerow(row)
32
+ count += 1
33
+ stream.flush()
34
+ return count