model2data 1.11.0__tar.gz → 1.12.0__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.
- {model2data-1.11.0/model2data.egg-info → model2data-1.12.0}/PKG-INFO +1 -1
- {model2data-1.11.0 → model2data-1.12.0}/README.md +12 -9
- {model2data-1.11.0 → model2data-1.12.0}/model2data/generate/core.py +1 -1
- {model2data-1.11.0 → model2data-1.12.0}/model2data/generate/timeline.py +73 -13
- {model2data-1.11.0 → model2data-1.12.0}/model2data/generate/when.py +17 -13
- {model2data-1.11.0 → model2data-1.12.0/model2data.egg-info}/PKG-INFO +1 -1
- {model2data-1.11.0 → model2data-1.12.0}/pyproject.toml +1 -1
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_determinism.py +10 -9
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_timeline.py +65 -1
- {model2data-1.11.0 → model2data-1.12.0}/LICENSE +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/README_PYPI.md +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/LICENSE +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/VENDORED.md +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/_classes/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/_classes/base.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/_classes/column.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/_classes/enum.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/_classes/expression.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/_classes/index.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/_classes/note.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/_classes/project.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/_classes/reference.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/_classes/sticky_note.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/_classes/table.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/_classes/table_group.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/classes/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/constants.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/database.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/definitions/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/definitions/column.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/definitions/common.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/definitions/enum.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/definitions/generic.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/definitions/index.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/definitions/project.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/definitions/records.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/definitions/reference.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/definitions/sticky_note.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/definitions/table.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/definitions/table_group.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/definitions/table_partial.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/exceptions.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/parser/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/parser/blueprints.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/parser/parser.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/base.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/column.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/enum.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/expression.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/index.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/note.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/project.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/reference.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/renderer.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/sticky_note.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/table.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/table_group.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/utils.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/column.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/enum.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/expression.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/index.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/note.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/reference.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/renderer.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/table.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/utils.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/tools.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/cli.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/dbt/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/dbt/hint_tests.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/dbt/naming.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/dbt/project.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/dbt/templates/dbt_project.yml.jinja +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/dbt/templates/hint_macros/model2data_hint_tests.sql +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/dbt/templates/hint_macros/model2data_when_tests.sql +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/dbt/templates/history_macros/model2data_history_tests.sql +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/dbt/templates/macros/generate_schema_name.sql +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/dbt/templates/profiles.yml.jinja +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/dbt/tests.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/defects/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/defects/apply.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/defects/checks.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/defects/presets.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/defects/report.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/generate/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/generate/days.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/generate/faker.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/generate/hints.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/generate/history.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/generate/kinds.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/generate/options.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/generate/relationships.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/guide/setup.md +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/guide/triage.md +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/guide/tune.md +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/model/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/model/_yaml.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/model/dbml.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/model/document.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/model/dump.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/model/engine.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/model/errors.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/model/reader.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/model/types.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/model/validate.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/output.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/parse/__init__.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/parse/dbml.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/spec/PUBLISHING.md +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/spec/README.md +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/spec/examples/coffee_webshop.model2data.yml +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/spec/model.schema.json +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data/utils.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data.egg-info/SOURCES.txt +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data.egg-info/dependency_links.txt +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data.egg-info/entry_points.txt +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data.egg-info/requires.txt +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/model2data.egg-info/top_level.txt +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/setup.cfg +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_as_of_anchor.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_cli.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_cli_model.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_column_time_hints.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_coverage_gaps.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_days.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_dbml_parser.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_dbml_parser_fuzz.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_dbt_integration.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_dbt_naming.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_dbt_project.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_dbt_tests.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_defects.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_defects_dbt.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_defects_replay.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_defects_report.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_defects_review.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_distributions.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_examples_model.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_faker_name_inference.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_fast_paths.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_generation.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_guide.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_hint_tests.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_history.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_issue_lines.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_lone_country.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_model.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_model_dbml.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_model_edges.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_model_messages.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_model_vocabulary.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_options.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_release_stress.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_reproducibility.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_row_identity.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_shaping.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_spec_alignment.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_spec_schema.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_table_seeds.py +0 -0
- {model2data-1.11.0 → model2data-1.12.0}/tests/test_when.py +0 -0
|
@@ -57,8 +57,9 @@ access required.
|
|
|
57
57
|
- **Relationship-preserving.** Foreign keys resolve to real parent rows; tables are generated in
|
|
58
58
|
dependency order.
|
|
59
59
|
- **Deterministic, byte for byte.** The same model, `--seed` and `--as-of` (and the same options)
|
|
60
|
-
give the same files, byte for byte, on any machine and
|
|
61
|
-
fixtures, safe to diff across CI runs
|
|
60
|
+
give the same files, byte for byte, on any machine and any day with the same model2data
|
|
61
|
+
version — safe to commit fixtures, safe to diff across CI runs. See
|
|
62
|
+
[The determinism promise](#the-determinism-promise).
|
|
62
63
|
- **Re-rollable one table at a time.** `--table-seed orders=7` regenerates a single table and
|
|
63
64
|
leaves every other table byte-identical, so you can keep the four tables that look right.
|
|
64
65
|
- **A real dbt project, not just CSVs.** Seeds, staging models that `ref()` them, schema tests,
|
|
@@ -229,14 +230,14 @@ jobs:
|
|
|
229
230
|
```
|
|
230
231
|
|
|
231
232
|
Inputs: `files` (glob, default `**/*.model2data.yml`), `version` (default: the release the tag
|
|
232
|
-
points at, e.g. `1.
|
|
233
|
+
points at, e.g. `1.12.0`; a specifier such as `>=1.10,<2` also works), `python-version` (3.12).
|
|
233
234
|
|
|
234
235
|
**pre-commit**
|
|
235
236
|
|
|
236
237
|
```yaml
|
|
237
238
|
repos:
|
|
238
239
|
- repo: https://github.com/JB-Analytica/model2data
|
|
239
|
-
rev: v1.
|
|
240
|
+
rev: v1.12.0
|
|
240
241
|
hooks:
|
|
241
242
|
- id: model2data-validate
|
|
242
243
|
```
|
|
@@ -250,7 +251,7 @@ model2data:
|
|
|
250
251
|
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
251
252
|
changes: ["**/*.model2data.yml"]
|
|
252
253
|
script:
|
|
253
|
-
- pip install model2data==1.
|
|
254
|
+
- pip install model2data==1.12.0
|
|
254
255
|
- model2data validate --glob "**/*.model2data.yml"
|
|
255
256
|
```
|
|
256
257
|
|
|
@@ -323,13 +324,15 @@ The same model, seed and `--as-of` — with the same options (`--rows`, `--rows-
|
|
|
323
324
|
|
|
324
325
|
- on any machine and operating system, and on every supported Python version;
|
|
325
326
|
- with any Faker or pandas version model2data accepts;
|
|
326
|
-
-
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
- with the same model2data version, whenever you run it.
|
|
328
|
+
|
|
329
|
+
A new release may change what a seed produces, to fix a bug or make the data more realistic.
|
|
330
|
+
When it does, the changelog says which models are affected and why. Pin the version
|
|
331
|
+
(`model2data==1.11.0`) when fixtures must stay the same across upgrades.
|
|
329
332
|
|
|
330
333
|
`tests/test_determinism.py` holds the file hashes of a spread of models, presets, multi-day runs
|
|
331
334
|
and locales, and runs on every pull request — against the newest Faker and pandas too — so a
|
|
332
|
-
change to a single output byte fails the build
|
|
335
|
+
change to a single output byte fails the build unless it is deliberate and in the changelog.
|
|
333
336
|
|
|
334
337
|
If one table comes out wrong and the rest looks right, `--table-seed` re-rolls just that table.
|
|
335
338
|
Every other table's seed CSV stays byte-identical, and children of the re-rolled table still
|
|
@@ -248,7 +248,7 @@ def generate_data_from_dbml(
|
|
|
248
248
|
# composite-key retry regenerates a temporal column's value into a
|
|
249
249
|
# frame that already respects created/updated/closed ordering, rather
|
|
250
250
|
# than one where only the untouched columns do.
|
|
251
|
-
df = order_row_times(df, table_def, as_of=as_of)
|
|
251
|
+
df = order_row_times(df, table_def, as_of=as_of, time_profile=profile)
|
|
252
252
|
df = _resolve_self_referencing_fks(
|
|
253
253
|
df,
|
|
254
254
|
table_def,
|
|
@@ -197,6 +197,60 @@ def weighted_timestamps(row_count: int, profile: TimeProfile, anchor: date) -> l
|
|
|
197
197
|
_TIMESTAMP_GAP_SCALE_SECONDS = 3 * 86400
|
|
198
198
|
_DATE_GAP_SCALE_DAYS = 3
|
|
199
199
|
|
|
200
|
+
# How many gaps `place_after` draws for a moment inside the window, in the
|
|
201
|
+
# column's shape, before it settles for one drawn evenly between the bounds.
|
|
202
|
+
_PLACE_TRIES = 20
|
|
203
|
+
|
|
204
|
+
# A weekend day's weight against a weekday's under `business_hours`, as
|
|
205
|
+
# `_day_weight` weighs it.
|
|
206
|
+
_WEEKEND_WEIGHT = 0.3
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def gap(kind: str) -> timedelta:
|
|
210
|
+
"""How far after what it follows a dependent column lands: 3 days on average."""
|
|
211
|
+
if kind == "timestamp":
|
|
212
|
+
return timedelta(seconds=round(random.expovariate(1 / _TIMESTAMP_GAP_SCALE_SECONDS)))
|
|
213
|
+
return timedelta(days=max(0, round(random.expovariate(1 / _DATE_GAP_SCALE_DAYS))))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def place_after(lower: datetime, kind: str, upper: datetime, business_hours: bool) -> datetime:
|
|
217
|
+
"""A moment a random gap after `lower`, in the column's shape, before `upper`.
|
|
218
|
+
|
|
219
|
+
The gap decides roughly when; `business_hours` then decides the weekday
|
|
220
|
+
and the hour, as it does for a column drawn on its own: a weekend day is
|
|
221
|
+
kept 3 times in 10, and a timestamp's time of day is redrawn from
|
|
222
|
+
`HOUR_WEIGHTS` on the gap's day (a time before `lower` is drawn again).
|
|
223
|
+
A timestamp lands before `upper`, a date on or before it. A draw past
|
|
224
|
+
`upper` is drawn again, so a short window does not pile rows up on its
|
|
225
|
+
last moment; after `_PLACE_TRIES` the moment is drawn evenly between the
|
|
226
|
+
bounds, and a window with no room at all returns `lower`: the column
|
|
227
|
+
never lands before what it follows.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def inside(moment: datetime) -> bool:
|
|
231
|
+
return moment < upper if kind == "timestamp" else moment.date() <= upper.date()
|
|
232
|
+
|
|
233
|
+
for _ in range(_PLACE_TRIES):
|
|
234
|
+
moment = lower + gap(kind)
|
|
235
|
+
if business_hours:
|
|
236
|
+
if moment.weekday() >= 5 and random.random() >= _WEEKEND_WEIGHT:
|
|
237
|
+
continue
|
|
238
|
+
if kind == "timestamp":
|
|
239
|
+
hour = random.choices(range(24), weights=HOUR_WEIGHTS)[0]
|
|
240
|
+
moment = datetime(moment.year, moment.month, moment.day, hour) + timedelta(
|
|
241
|
+
seconds=random.randrange(3600)
|
|
242
|
+
)
|
|
243
|
+
if moment < lower:
|
|
244
|
+
continue
|
|
245
|
+
if inside(moment):
|
|
246
|
+
return moment
|
|
247
|
+
if kind == "timestamp":
|
|
248
|
+
span = int((upper - lower).total_seconds())
|
|
249
|
+
return lower + timedelta(seconds=random.randrange(span)) if span > 0 else lower
|
|
250
|
+
span = (upper.date() - lower.date()).days
|
|
251
|
+
return lower + timedelta(days=random.randint(0, span)) if span > 0 else lower
|
|
252
|
+
|
|
253
|
+
|
|
200
254
|
# Column-name stems that place a column at a stage in the created -> updated ->
|
|
201
255
|
# closed chain. A column matches a stage by tokenizing its name on non-
|
|
202
256
|
# alphanumeric characters and looking for the stem as a contiguous run of
|
|
@@ -330,6 +384,14 @@ def _format_value(moment: datetime, kind: str) -> Union[str, date]:
|
|
|
330
384
|
return moment.date()
|
|
331
385
|
|
|
332
386
|
|
|
387
|
+
def column_business_hours(column: ColumnDef, time_profile: Optional[TimeProfile]) -> bool:
|
|
388
|
+
"""Whether `column` is drawn in business hours: its own hint, else the run's."""
|
|
389
|
+
note = column.note or {}
|
|
390
|
+
if "business_hours" in note:
|
|
391
|
+
return bool(note["business_hours"])
|
|
392
|
+
return bool(time_profile and time_profile.business_hours)
|
|
393
|
+
|
|
394
|
+
|
|
333
395
|
def _build_dependencies(
|
|
334
396
|
table_def: TableDef, columns_by_name: dict, temporal_columns: list[ColumnDef]
|
|
335
397
|
) -> dict[str, set[str]]:
|
|
@@ -413,15 +475,20 @@ def _topological_order(table_def: TableDef, deps: dict[str, set[str]]) -> list[s
|
|
|
413
475
|
return order
|
|
414
476
|
|
|
415
477
|
|
|
416
|
-
def order_row_times(
|
|
478
|
+
def order_row_times(
|
|
479
|
+
df, table_def: TableDef, as_of: AsOf = None, time_profile: Optional[TimeProfile] = None
|
|
480
|
+
):
|
|
417
481
|
"""Fix up temporal columns so an earlier-stage column never lands later.
|
|
418
482
|
|
|
419
483
|
Runs unconditionally (every profile, not just non-uniform ones): a row
|
|
420
484
|
updated before it was created is wrong regardless of how the timestamps
|
|
421
485
|
were drawn. A column with dependencies is set, per row, to the latest of
|
|
422
|
-
those dependencies plus a random gap
|
|
423
|
-
|
|
424
|
-
|
|
486
|
+
those dependencies plus a random gap, inside the window and in the
|
|
487
|
+
column's business-hours shape (see `place_after`; `time_profile` is the
|
|
488
|
+
run's, a column's own `business_hours` hint replacing it), unless either
|
|
489
|
+
side is null for that row -- a nullable column that happened to come back
|
|
490
|
+
null keeps its null, rather than this pass forcing a value in. The run's
|
|
491
|
+
growth and seasonality reach the column through the one it follows.
|
|
425
492
|
|
|
426
493
|
Leaves `df` untouched when the table holds no `after` hint and no
|
|
427
494
|
recognisable created/updated/closed-style name pair, and never modifies a
|
|
@@ -456,7 +523,7 @@ def order_row_times(df, table_def: TableDef, as_of: AsOf = None):
|
|
|
456
523
|
other_kind = _column_kind(columns_by_name[name].data_type)
|
|
457
524
|
assert other_kind is not None
|
|
458
525
|
required_kinds[name] = other_kind
|
|
459
|
-
|
|
526
|
+
business_hours = column_business_hours(columns_by_name[column_name], time_profile)
|
|
460
527
|
|
|
461
528
|
for idx in df.index:
|
|
462
529
|
if _parse_value(df.at[idx, column_name], kind) is None:
|
|
@@ -475,14 +542,7 @@ def order_row_times(df, table_def: TableDef, as_of: AsOf = None):
|
|
|
475
542
|
continue
|
|
476
543
|
latest_required = max(earlier)
|
|
477
544
|
|
|
478
|
-
|
|
479
|
-
if kind == "timestamp":
|
|
480
|
-
moment = latest_required + timedelta(seconds=round(gap))
|
|
481
|
-
else:
|
|
482
|
-
moment = latest_required + timedelta(days=max(0, round(gap)))
|
|
483
|
-
if moment > cap:
|
|
484
|
-
moment = cap
|
|
485
|
-
|
|
545
|
+
moment = place_after(latest_required, kind, cap, business_hours)
|
|
486
546
|
df.at[idx, column_name] = _format_value(moment, kind)
|
|
487
547
|
|
|
488
548
|
return df
|
|
@@ -45,12 +45,12 @@ from model2data.generate import kinds
|
|
|
45
45
|
from model2data.generate.faker import AsOf, generate_column_values
|
|
46
46
|
from model2data.generate.options import TimeProfile
|
|
47
47
|
from model2data.generate.timeline import (
|
|
48
|
-
_DATE_GAP_SCALE_DAYS,
|
|
49
|
-
_TIMESTAMP_GAP_SCALE_SECONDS,
|
|
50
48
|
_build_dependencies,
|
|
51
49
|
_format_value,
|
|
52
50
|
_parse_value,
|
|
53
51
|
_topological_order,
|
|
52
|
+
column_business_hours,
|
|
53
|
+
place_after,
|
|
54
54
|
)
|
|
55
55
|
from model2data.parse.dbml import ColumnDef, TableDef
|
|
56
56
|
|
|
@@ -123,7 +123,13 @@ def _temporal_kind(column: ColumnDef) -> Optional[str]:
|
|
|
123
123
|
class _Order:
|
|
124
124
|
"""What a filled date or timestamp of one table must sit between, row by row."""
|
|
125
125
|
|
|
126
|
-
def __init__(
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
table_def: TableDef,
|
|
129
|
+
frame: pd.DataFrame,
|
|
130
|
+
time_profile: Optional[TimeProfile] = None,
|
|
131
|
+
):
|
|
132
|
+
self.time_profile = time_profile
|
|
127
133
|
self.by_name = {column.name: column for column in table_def.columns}
|
|
128
134
|
temporal = [
|
|
129
135
|
column
|
|
@@ -154,6 +160,11 @@ class _Order:
|
|
|
154
160
|
found = self._moments(followers, row)
|
|
155
161
|
return min(found) if found else None
|
|
156
162
|
|
|
163
|
+
def place(self, name: str, lower: datetime, kind: str, upper: datetime) -> datetime:
|
|
164
|
+
"""A moment for `name` a random gap after `lower`, in the column's shape."""
|
|
165
|
+
business_hours = column_business_hours(self.by_name[name], self.time_profile)
|
|
166
|
+
return place_after(lower, kind, upper, business_hours)
|
|
167
|
+
|
|
157
168
|
def settle(self, name: str, row: int, cap: datetime) -> None:
|
|
158
169
|
"""Move what follows `name` in this row to no earlier than what it follows, in order.
|
|
159
170
|
|
|
@@ -172,17 +183,10 @@ class _Order:
|
|
|
172
183
|
lower = self.after(other, row)
|
|
173
184
|
if value is None or lower is None or value >= lower:
|
|
174
185
|
continue
|
|
175
|
-
self.frame.iat[row, column] = _format_value(
|
|
186
|
+
self.frame.iat[row, column] = _format_value(self.place(other, lower, kind, cap), kind)
|
|
176
187
|
moved.add(other)
|
|
177
188
|
|
|
178
189
|
|
|
179
|
-
def _gap(kind: str) -> timedelta:
|
|
180
|
-
"""How far after what it follows a filled value lands: the ordering pass's own gap."""
|
|
181
|
-
if kind == "timestamp":
|
|
182
|
-
return timedelta(seconds=round(random.expovariate(1 / _TIMESTAMP_GAP_SCALE_SECONDS)))
|
|
183
|
-
return timedelta(days=max(0, round(random.expovariate(1 / _DATE_GAP_SCALE_DAYS))))
|
|
184
|
-
|
|
185
|
-
|
|
186
190
|
def _draw_like(
|
|
187
191
|
column: ColumnDef,
|
|
188
192
|
held: list,
|
|
@@ -223,7 +227,7 @@ def apply_when(
|
|
|
223
227
|
columns = when_columns(table_def)
|
|
224
228
|
if not columns:
|
|
225
229
|
return
|
|
226
|
-
order = _Order(table_def, frame)
|
|
230
|
+
order = _Order(table_def, frame, time_profile)
|
|
227
231
|
for column in columns:
|
|
228
232
|
name = column.name
|
|
229
233
|
match = matching(frame, order.by_name, conditions(column) or {})
|
|
@@ -274,7 +278,7 @@ def _moment(
|
|
|
274
278
|
span = max(0, int((upper - start).total_seconds()))
|
|
275
279
|
moment = start + timedelta(seconds=random.randint(0, span))
|
|
276
280
|
elif lower is not None:
|
|
277
|
-
moment = lower
|
|
281
|
+
moment = order.place(name, lower, kind, upper)
|
|
278
282
|
else:
|
|
279
283
|
held = [
|
|
280
284
|
parsed
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
"""The same model, seed and `--as-of` write byte-identical files
|
|
1
|
+
"""The same model, seed and `--as-of` write byte-identical files on the same version.
|
|
2
2
|
|
|
3
3
|
That is the product's promise: a committed dataset, a diffed CI run and a teaching
|
|
4
4
|
exercise all lean on it. This module enforces it. Each case runs the real CLI
|
|
5
5
|
in-process, hashes every file it writes, and compares against the hashes in
|
|
6
|
-
`fixtures/determinism.json
|
|
6
|
+
`fixtures/determinism.json`.
|
|
7
7
|
|
|
8
|
-
If a case fails, output changed for the same seed. That is a
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
If a case fails, output changed for the same seed. That is not a snapshot to
|
|
9
|
+
refresh: either the change is unintended (find it), or it is deliberate and the
|
|
10
|
+
changelog says which models it affects and why. Faker or pandas releases that
|
|
11
|
+
change the data show up here too (CI runs this file against the newest of both).
|
|
11
12
|
|
|
12
|
-
Re-recording is for adding or
|
|
13
|
+
Re-recording is for adding cases or deliberate, changelogged changes only:
|
|
13
14
|
|
|
14
15
|
MODEL2DATA_RECORD_DETERMINISM=1 uv run pytest tests/test_determinism.py
|
|
15
16
|
|
|
@@ -123,9 +124,9 @@ def test_same_seed_same_bytes(case_id, tmp_path, recorded):
|
|
|
123
124
|
f"DETERMINISM BROKEN in case {case_id!r}: the same model, seed and --as-of no "
|
|
124
125
|
"longer write the same files.\n"
|
|
125
126
|
f" changed files: {changed}\n new files: {added}\n missing files: {missing}\n"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
127
|
+
"Do not just re-record the hashes: re-record only for a deliberate change the "
|
|
128
|
+
"CHANGELOG describes (which models, and why). If the change comes from a Faker or "
|
|
129
|
+
"pandas release, pin or work around it."
|
|
129
130
|
)
|
|
130
131
|
|
|
131
132
|
|
|
@@ -10,13 +10,15 @@ change is deliberate and is called out in CHANGELOG.md, but it still needs a
|
|
|
10
10
|
fixed expectation so a future refactor can't silently drift it further.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
import random
|
|
13
14
|
from datetime import date, datetime, timedelta
|
|
14
15
|
|
|
16
|
+
import pandas as pd
|
|
15
17
|
import pytest
|
|
16
18
|
|
|
17
19
|
from model2data.generate.core import generate_data_from_dbml
|
|
18
20
|
from model2data.generate.options import TimeProfile
|
|
19
|
-
from model2data.generate.timeline import order_row_times
|
|
21
|
+
from model2data.generate.timeline import order_row_times, place_after
|
|
20
22
|
from model2data.parse.dbml import ColumnDef, TableDef
|
|
21
23
|
|
|
22
24
|
ANCHOR = date(2026, 3, 15)
|
|
@@ -315,6 +317,68 @@ def test_after_hint_is_honoured():
|
|
|
315
317
|
assert datetime.fromisoformat(ordered) <= datetime.fromisoformat(shipped)
|
|
316
318
|
|
|
317
319
|
|
|
320
|
+
def _bookings(scheduled_note: dict, rows: int = 2000, profile=None) -> pd.DataFrame:
|
|
321
|
+
tables = {
|
|
322
|
+
"bookings": TableDef(
|
|
323
|
+
name="bookings",
|
|
324
|
+
columns=[
|
|
325
|
+
ColumnDef("id", "bigint", {"pk"}),
|
|
326
|
+
ColumnDef("booked_at", "timestamp", {"not null"}),
|
|
327
|
+
ColumnDef("scheduled_for", "timestamp", {"not null"}, note=scheduled_note),
|
|
328
|
+
],
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
return generate_data_from_dbml(
|
|
332
|
+
tables, [], base_rows=rows, seed=1, as_of=ANCHOR, time_profile=profile
|
|
333
|
+
)["bookings"]
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _working_share(values) -> float:
|
|
337
|
+
moments = [datetime.fromisoformat(v) for v in values]
|
|
338
|
+
return sum(1 for t in moments if t.weekday() < 5 and 8 <= t.hour < 18) / len(moments)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def test_a_column_with_after_keeps_its_business_hours():
|
|
342
|
+
"""The gap decides roughly when; the column's `business_hours` decides the hour."""
|
|
343
|
+
df = _bookings({"after": "booked_at", "business_hours": True})
|
|
344
|
+
|
|
345
|
+
assert _working_share(df["scheduled_for"]) >= 0.65
|
|
346
|
+
assert _working_share(df["booked_at"]) < 0.4
|
|
347
|
+
for booked, scheduled in zip(df["booked_at"], df["scheduled_for"], strict=True):
|
|
348
|
+
assert datetime.fromisoformat(booked) <= datetime.fromisoformat(scheduled)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_the_runs_business_hours_reach_a_column_with_after():
|
|
352
|
+
df = _bookings({"after": "booked_at"}, profile=TimeProfile(business_hours=True))
|
|
353
|
+
|
|
354
|
+
assert _working_share(df["scheduled_for"]) >= 0.65
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def test_a_column_with_after_does_not_pile_up_on_as_of_midnight():
|
|
358
|
+
"""A gap past the window is drawn again, not set to the window's end."""
|
|
359
|
+
df = _bookings({"after": "booked_at"})
|
|
360
|
+
cap = datetime(ANCHOR.year, ANCHOR.month, ANCHOR.day)
|
|
361
|
+
|
|
362
|
+
scheduled = [datetime.fromisoformat(v) for v in df["scheduled_for"]]
|
|
363
|
+
assert max(scheduled) < cap
|
|
364
|
+
# Two rows may share a second by chance; the old clamp put dozens on one.
|
|
365
|
+
assert max(scheduled.count(moment) for moment in scheduled) <= 2
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def test_place_after_stays_between_its_bounds():
|
|
369
|
+
random.seed(0)
|
|
370
|
+
lower = datetime(2026, 3, 14, 23, 0)
|
|
371
|
+
upper = datetime(2026, 3, 15)
|
|
372
|
+
for business_hours in (False, True):
|
|
373
|
+
moments = [place_after(lower, "timestamp", upper, business_hours) for _ in range(200)]
|
|
374
|
+
assert all(lower <= moment < upper for moment in moments)
|
|
375
|
+
assert len(set(moments)) > 150
|
|
376
|
+
|
|
377
|
+
dates = [place_after(lower, "date", upper, True) for _ in range(50)]
|
|
378
|
+
assert {moment.date() for moment in dates} <= {date(2026, 3, 14), date(2026, 3, 15)}
|
|
379
|
+
assert place_after(upper, "timestamp", upper, False) == upper
|
|
380
|
+
|
|
381
|
+
|
|
318
382
|
def test_start_end_suffix_pair_is_honoured():
|
|
319
383
|
tables = {
|
|
320
384
|
"subscriptions": TableDef(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/definitions/sticky_note.py
RENAMED
|
File without changes
|
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/definitions/table_group.py
RENAMED
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/definitions/table_partial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/__init__.py
RENAMED
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/column.py
RENAMED
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/enum.py
RENAMED
|
File without changes
|
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/index.py
RENAMED
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/note.py
RENAMED
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/project.py
RENAMED
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/reference.py
RENAMED
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/renderer.py
RENAMED
|
File without changes
|
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/table.py
RENAMED
|
File without changes
|
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/dbml/default/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/__init__.py
RENAMED
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/column.py
RENAMED
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/enum.py
RENAMED
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/expression.py
RENAMED
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/index.py
RENAMED
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/note.py
RENAMED
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/reference.py
RENAMED
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/renderer.py
RENAMED
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/table.py
RENAMED
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/_vendor/pydbml/renderer/sql/default/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/dbt/templates/macros/generate_schema_name.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model2data-1.11.0 → model2data-1.12.0}/model2data/spec/examples/coffee_webshop.model2data.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|