model2data 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 JB Analytica
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: model2data
3
+ Version: 0.1.0
4
+ Summary: Generate analytics-ready datasets from DBML models
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: dbt-core>=1.5.0
9
+ Requires-Dist: dbt-duckdb>=1.5.0
10
+ Requires-Dist: faker>=37.12.0
11
+ Requires-Dist: pandas>=2.3.3
12
+ Requires-Dist: pyyaml>=6.0.3
13
+ Requires-Dist: typer>=0.20.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest; extra == "dev"
16
+ Dynamic: license-file
17
+
18
+ # model2data
19
+
20
+ [![PyPI](https://img.shields.io/pypi/v/model2data)](https://pypi.org/project/model2data/)
21
+ [![CI](https://github.com/JB-Analytica/model2data/actions/workflows/ci.yml/badge.svg)](https://github.com/JB-Analytica/model2data/actions/workflows/ci.yml)
22
+ [![codecov](https://codecov.io/gh/JB-Analytica/model2data/branch/main/graph/badge.svg)](https://codecov.io/gh/JB-Analytica/model2data)
23
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
24
+
25
+ `model2data` turns **data models into analytics-ready datasets** in seconds.
26
+
27
+ Given a **DBML file**, it generates synthetic but realistic data, a complete dbt project scaffold, and everything you need to start analyzing or testing data pipelines.
28
+
29
+ ---
30
+
31
+ ## What problem does it solve?
32
+
33
+ Building analytics or testing dbt pipelines often requires realistic data, but using real data raises privacy concerns, and creating mock data manually is time-consuming. `model2data` automates this by generating synthetic datasets from your data model definitions, ensuring privacy-safe, deterministic, and relationship-preserving data for development and testing.
34
+
35
+ ---
36
+
37
+ ## How it works (high level)
38
+
39
+ 1. **Parse DBML**: Reads your database schema from a DBML file, extracting tables, columns, types, and relationships.
40
+ 2. **Generate Data**: Uses Faker and custom logic to create realistic synthetic data, respecting foreign keys and constraints.
41
+ 3. **Scaffold dbt Project**: Creates a dbt project with seeds (CSV files), staging models, profiles, and tests, ready to run with DuckDB.
42
+
43
+ ---
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install model2data
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Quick start
54
+
55
+ We provide an example Hacker News dataset in `examples/hackernews.dbml`.
56
+
57
+ Generate a project with synthetic data:
58
+
59
+ ```bash
60
+ model2data generate --file examples/hackernews.dbml --rows 200 --seed 42
61
+ ```
62
+
63
+ This creates a `dbt_hackernews/` folder with your data and dbt setup.
64
+
65
+ Run dbt to load and transform the data:
66
+
67
+ ```bash
68
+ cd dbt_hackernews
69
+ dbt deps
70
+ dbt seed
71
+ dbt run
72
+ ```
73
+
74
+ Your analytics-ready dataset is now in DuckDB!
75
+
76
+ ---
77
+
78
+ ## Generated dbt project structure
79
+
80
+ The generated dbt project includes:
81
+
82
+ ```
83
+ dbt_{project_name}/
84
+ โ”œโ”€โ”€ seeds/
85
+ โ”‚ โ””โ”€โ”€ {project_name}/
86
+ โ”‚ โ”œโ”€โ”€ table1.csv
87
+ โ”‚ โ””โ”€โ”€ table2.csv
88
+ โ”œโ”€โ”€ models/
89
+ โ”‚ โ””โ”€โ”€ {project_name}/
90
+ โ”‚ โ””โ”€โ”€ staging/
91
+ โ”‚ โ”œโ”€โ”€ __sources.yml
92
+ โ”‚ โ”œโ”€โ”€ stg_table1.sql
93
+ โ”‚ โ”œโ”€โ”€ stg_table1.yml
94
+ โ”‚ โ””โ”€โ”€ ...
95
+ โ”œโ”€โ”€ macros/
96
+ โ”‚ โ””โ”€โ”€ generate_schema_name.sql
97
+ โ”œโ”€โ”€ dbt_project.yml
98
+ โ”œโ”€โ”€ profiles.yml # DuckDB config
99
+ โ””โ”€โ”€ {project_name}.duckdb
100
+ ```
101
+
102
+ - **Seeds**: CSV files with generated synthetic data.
103
+ - **Staging Models**: Basic dbt models that load from seeds.
104
+ - **Sources & Tests**: YAML configs defining sources and basic tests (not_null, unique).
105
+ - **Profiles**: Pre-configured for DuckDB with schema handling.
106
+
107
+ ---
108
+
109
+ ## Design decisions / non-goals
110
+
111
+ - **DuckDB Default**: Chosen for its zero-config, file-based nature, making it easy to get started without database setup. Other adapters can be configured manually.
112
+ - **dbt Integration**: Leverages dbt's transformation capabilities for a familiar workflow in analytics engineering.
113
+ - **Synthetic Data**: Uses deterministic generation for reproducibility; not intended for production use or as a replacement for real data.
114
+ - **Non-goals**: This is not a data migration tool, ETL pipeline, or real-time data generator. It focuses on static, synthetic datasets for testing and prototyping.
115
+
116
+ ---
117
+
118
+ ## Limitations
119
+
120
+ - Supports basic DBML features; complex constraints or advanced SQL types may not be fully handled.
121
+ - Synthetic data generation is heuristic-based and may not perfectly mimic real-world distributions or edge cases.
122
+ - Currently optimized for DuckDB; other databases require manual profile adjustments.
123
+ - No support for incremental models or advanced dbt features in generated projects.
124
+
125
+ ---
126
+
127
+ ## Roadmap
128
+
129
+ - Support for additional database adapters (e.g., Snowflake, BigQuery).
130
+ - Enhanced data type handling and custom generators.
131
+ - Integration with more dbt features like incremental models.
132
+ - Web-based DBML editor and data preview.
133
+
134
+ ---
135
+
136
+ ## Contributing
137
+
138
+ We welcome contributions!
139
+
140
+ - Open issues for bugs or feature requests.
141
+ - Submit PRs to add new DBML examples, custom data generators, or improvements.
142
+ - Ensure all new features include tests if possible.
143
+
144
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
145
+
146
+ ## Code of Conduct
147
+
148
+ Please read our [Code of Conduct](CODE_OF_CONDUCT.md) to understand our community standards.
149
+
150
+ ---
151
+
152
+ ## License
153
+
154
+ MIT License. See LICENSE for details.
@@ -0,0 +1,137 @@
1
+ # model2data
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/model2data)](https://pypi.org/project/model2data/)
4
+ [![CI](https://github.com/JB-Analytica/model2data/actions/workflows/ci.yml/badge.svg)](https://github.com/JB-Analytica/model2data/actions/workflows/ci.yml)
5
+ [![codecov](https://codecov.io/gh/JB-Analytica/model2data/branch/main/graph/badge.svg)](https://codecov.io/gh/JB-Analytica/model2data)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ `model2data` turns **data models into analytics-ready datasets** in seconds.
9
+
10
+ Given a **DBML file**, it generates synthetic but realistic data, a complete dbt project scaffold, and everything you need to start analyzing or testing data pipelines.
11
+
12
+ ---
13
+
14
+ ## What problem does it solve?
15
+
16
+ Building analytics or testing dbt pipelines often requires realistic data, but using real data raises privacy concerns, and creating mock data manually is time-consuming. `model2data` automates this by generating synthetic datasets from your data model definitions, ensuring privacy-safe, deterministic, and relationship-preserving data for development and testing.
17
+
18
+ ---
19
+
20
+ ## How it works (high level)
21
+
22
+ 1. **Parse DBML**: Reads your database schema from a DBML file, extracting tables, columns, types, and relationships.
23
+ 2. **Generate Data**: Uses Faker and custom logic to create realistic synthetic data, respecting foreign keys and constraints.
24
+ 3. **Scaffold dbt Project**: Creates a dbt project with seeds (CSV files), staging models, profiles, and tests, ready to run with DuckDB.
25
+
26
+ ---
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install model2data
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Quick start
37
+
38
+ We provide an example Hacker News dataset in `examples/hackernews.dbml`.
39
+
40
+ Generate a project with synthetic data:
41
+
42
+ ```bash
43
+ model2data generate --file examples/hackernews.dbml --rows 200 --seed 42
44
+ ```
45
+
46
+ This creates a `dbt_hackernews/` folder with your data and dbt setup.
47
+
48
+ Run dbt to load and transform the data:
49
+
50
+ ```bash
51
+ cd dbt_hackernews
52
+ dbt deps
53
+ dbt seed
54
+ dbt run
55
+ ```
56
+
57
+ Your analytics-ready dataset is now in DuckDB!
58
+
59
+ ---
60
+
61
+ ## Generated dbt project structure
62
+
63
+ The generated dbt project includes:
64
+
65
+ ```
66
+ dbt_{project_name}/
67
+ โ”œโ”€โ”€ seeds/
68
+ โ”‚ โ””โ”€โ”€ {project_name}/
69
+ โ”‚ โ”œโ”€โ”€ table1.csv
70
+ โ”‚ โ””โ”€โ”€ table2.csv
71
+ โ”œโ”€โ”€ models/
72
+ โ”‚ โ””โ”€โ”€ {project_name}/
73
+ โ”‚ โ””โ”€โ”€ staging/
74
+ โ”‚ โ”œโ”€โ”€ __sources.yml
75
+ โ”‚ โ”œโ”€โ”€ stg_table1.sql
76
+ โ”‚ โ”œโ”€โ”€ stg_table1.yml
77
+ โ”‚ โ””โ”€โ”€ ...
78
+ โ”œโ”€โ”€ macros/
79
+ โ”‚ โ””โ”€โ”€ generate_schema_name.sql
80
+ โ”œโ”€โ”€ dbt_project.yml
81
+ โ”œโ”€โ”€ profiles.yml # DuckDB config
82
+ โ””โ”€โ”€ {project_name}.duckdb
83
+ ```
84
+
85
+ - **Seeds**: CSV files with generated synthetic data.
86
+ - **Staging Models**: Basic dbt models that load from seeds.
87
+ - **Sources & Tests**: YAML configs defining sources and basic tests (not_null, unique).
88
+ - **Profiles**: Pre-configured for DuckDB with schema handling.
89
+
90
+ ---
91
+
92
+ ## Design decisions / non-goals
93
+
94
+ - **DuckDB Default**: Chosen for its zero-config, file-based nature, making it easy to get started without database setup. Other adapters can be configured manually.
95
+ - **dbt Integration**: Leverages dbt's transformation capabilities for a familiar workflow in analytics engineering.
96
+ - **Synthetic Data**: Uses deterministic generation for reproducibility; not intended for production use or as a replacement for real data.
97
+ - **Non-goals**: This is not a data migration tool, ETL pipeline, or real-time data generator. It focuses on static, synthetic datasets for testing and prototyping.
98
+
99
+ ---
100
+
101
+ ## Limitations
102
+
103
+ - Supports basic DBML features; complex constraints or advanced SQL types may not be fully handled.
104
+ - Synthetic data generation is heuristic-based and may not perfectly mimic real-world distributions or edge cases.
105
+ - Currently optimized for DuckDB; other databases require manual profile adjustments.
106
+ - No support for incremental models or advanced dbt features in generated projects.
107
+
108
+ ---
109
+
110
+ ## Roadmap
111
+
112
+ - Support for additional database adapters (e.g., Snowflake, BigQuery).
113
+ - Enhanced data type handling and custom generators.
114
+ - Integration with more dbt features like incremental models.
115
+ - Web-based DBML editor and data preview.
116
+
117
+ ---
118
+
119
+ ## Contributing
120
+
121
+ We welcome contributions!
122
+
123
+ - Open issues for bugs or feature requests.
124
+ - Submit PRs to add new DBML examples, custom data generators, or improvements.
125
+ - Ensure all new features include tests if possible.
126
+
127
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
128
+
129
+ ## Code of Conduct
130
+
131
+ Please read our [Code of Conduct](CODE_OF_CONDUCT.md) to understand our community standards.
132
+
133
+ ---
134
+
135
+ ## License
136
+
137
+ MIT License. See LICENSE for details.
@@ -0,0 +1,160 @@
1
+ from pathlib import Path
2
+ from typing import Optional
3
+ import random
4
+ import shutil
5
+
6
+ import typer
7
+ from faker import Faker
8
+
9
+ from model2data.parse.dbml import parse_dbml
10
+ from model2data.generate.core import generate_data_from_dbml
11
+ from model2data.dbt.project import (
12
+ create_project_scaffold,
13
+ create_profiles_yml,
14
+ create_staging_models,
15
+ )
16
+ from model2data.dbt.tests import generate_dbt_yml
17
+ from model2data.utils import normalize_identifier
18
+
19
+ app = typer.Typer(
20
+ help=(
21
+ "model2data: Generate analytics-ready datasets from DBML models.\n\n"
22
+ "Given a DBML file, this tool produces:\n"
23
+ "โ€ข Synthetic but realistic data\n"
24
+ "โ€ข A runnable dbt project scaffold\n"
25
+ "โ€ข dbt seeds, staging models, and profiles\n"
26
+ ),
27
+ add_completion=False,
28
+ )
29
+
30
+
31
+ @app.command(help="Generate synthetic data and a dbt project from a DBML model.")
32
+ def main(
33
+ file: Path = typer.Option(
34
+ ...,
35
+ "--file",
36
+ "-f",
37
+ exists=True,
38
+ file_okay=True,
39
+ dir_okay=False,
40
+ readable=True,
41
+ resolve_path=True,
42
+ help="Path to the DBML file to generate data from.",
43
+ ),
44
+ rows: int = typer.Option(
45
+ 100,
46
+ "--rows",
47
+ "-r",
48
+ min=10,
49
+ help="Number of rows to generate per table.",
50
+ ),
51
+ seed: Optional[int] = typer.Option(
52
+ None,
53
+ "--seed",
54
+ help=(
55
+ "Optional random seed for deterministic generation.\n"
56
+ "Using the same seed will always produce identical datasets."
57
+ ),
58
+ ),
59
+ name: Optional[str] = typer.Option(
60
+ None,
61
+ "--name",
62
+ "-n",
63
+ help="Optional override for the generated dbt project's name.",
64
+ ),
65
+ force: bool = typer.Option(
66
+ False,
67
+ "--force",
68
+ help="Overwrite the destination directory if it already exists.",
69
+ ),
70
+ ):
71
+ """
72
+ Generate synthetic data and a dbt project from a DBML model.
73
+ """
74
+
75
+ # -------------------------
76
+ # Deterministic seed
77
+ # -------------------------
78
+ if seed is not None:
79
+ random.seed(seed)
80
+ Faker.seed(seed)
81
+ typer.echo(f"๐Ÿ” Using deterministic seed: {seed}")
82
+
83
+ # -------------------------
84
+ # Parse DBML (names untouched)
85
+ # -------------------------
86
+ tables, refs = parse_dbml(file)
87
+ if not tables:
88
+ typer.echo("โŒ No tables found in the provided DBML file.")
89
+ raise typer.Exit(1)
90
+
91
+ # -------------------------
92
+ # DBML โ†’ dbt name mapping
93
+ # -------------------------
94
+ dbt_name_map = {
95
+ table_name: normalize_identifier(table_name)
96
+ for table_name in tables.keys()
97
+ }
98
+
99
+ project_name = normalize_identifier(name or file.stem)
100
+ dest = Path.cwd() / f"dbt_{project_name}"
101
+ profile_name = f"{project_name}_profile"
102
+
103
+ if dest.exists():
104
+ if not force:
105
+ typer.echo(
106
+ f"โŒ Destination {dest} already exists.\n"
107
+ "Use --force to overwrite."
108
+ )
109
+ raise typer.Exit(1)
110
+ shutil.rmtree(dest)
111
+
112
+ # -------------------------
113
+ # dbt project scaffold
114
+ # -------------------------
115
+ typer.echo(f"๐Ÿ“ฆ Creating dbt project scaffold at {dest}")
116
+ create_project_scaffold(dest, project_name, profile_name)
117
+
118
+ # -------------------------
119
+ # Generate synthetic data
120
+ # -------------------------
121
+ typer.echo("๐Ÿงฎ Generating synthetic datasets from DBML definitions...")
122
+ generated_tables = generate_data_from_dbml(
123
+ tables=tables,
124
+ refs=refs,
125
+ base_rows=rows,
126
+ seed=seed,
127
+ )
128
+
129
+ # -------------------------
130
+ # Write dbt seeds (normalized names)
131
+ # -------------------------
132
+ seeds_path = dest / "seeds/raw"
133
+ for table_key, df in generated_tables.items():
134
+ csv_path = seeds_path / f"{table_key}.csv"
135
+ df.to_csv(csv_path, index=False)
136
+
137
+ # -------------------------
138
+ # Build dbt assets
139
+ # -------------------------
140
+ typer.echo("๐Ÿ—‚๏ธ Building staging models for generated seeds...")
141
+ create_staging_models(dest, project_name)
142
+
143
+ typer.echo("๐Ÿงช Generating dbt yml with tests...")
144
+ generate_dbt_yml(dest, tables, refs, project_name)
145
+
146
+ typer.echo("๐Ÿชช Ensuring dbt profile exists...")
147
+ create_profiles_yml(dest, profile_name)
148
+
149
+ # Keep original DBML for reference
150
+ shutil.copy(file, dest / file.name)
151
+
152
+ # -------------------------
153
+ # Done
154
+ # -------------------------
155
+ typer.echo("\n๐ŸŽ‰ model2data generation complete!\n")
156
+ typer.echo("Next steps:")
157
+ typer.echo(f" cd {dest}")
158
+ typer.echo(" dbt deps")
159
+ typer.echo(" dbt seed")
160
+ typer.echo(" dbt run")
@@ -0,0 +1,74 @@
1
+ from pathlib import Path
2
+ import shutil
3
+ import jinja2
4
+
5
+ TEMPLATES_DIR = Path(__file__).parent / "templates"
6
+
7
+
8
+ def create_project_scaffold(dest: Path, project_name: str, profile_name: str) -> None:
9
+ dest.mkdir(parents=True, exist_ok=True)
10
+
11
+ # dbt folders
12
+ (dest / "models" / "staging").mkdir(parents=True, exist_ok=True)
13
+ (dest / "seeds" / "raw").mkdir(parents=True, exist_ok=True)
14
+ (dest / "analysis").mkdir(exist_ok=True)
15
+ (dest / "macros").mkdir(exist_ok=True)
16
+ (dest / "tests").mkdir(exist_ok=True)
17
+ (dest / "snapshots").mkdir(exist_ok=True)
18
+
19
+ # dbt_project.yml
20
+ _render_template(
21
+ template_name="dbt_project.yml.jinja",
22
+ output_path=dest / "dbt_project.yml",
23
+ context={"project_name": project_name, "profile_name": profile_name},
24
+ )
25
+
26
+ # Copy over any macros from templates
27
+ template_macros_dir = Path("model2data/dbt/templates/macros")
28
+ if template_macros_dir.exists():
29
+ for macro_file in template_macros_dir.glob("*.sql"):
30
+ target_file = dest / "macros" / macro_file.name
31
+ if not target_file.exists():
32
+ target_file.write_text(macro_file.read_text())
33
+
34
+
35
+ def create_staging_models(dest: Path, project_name: str) -> None:
36
+ """
37
+ Creates staging models in models/staging/ folder that reference raw seed tables as sources.
38
+ """
39
+ seeds_path = dest / "seeds" / "raw"
40
+ models_path = dest / "models" / "staging"
41
+ models_path.mkdir(parents=True, exist_ok=True)
42
+
43
+ for csv_file in seeds_path.glob("*.csv"):
44
+ table_name = csv_file.stem # keep full seed name, e.g., raw_stories
45
+ model_file = models_path / f"stg_{table_name}.sql"
46
+
47
+ if not model_file.exists():
48
+ sql_content = f"""\
49
+ -- Auto-generated staging model for {table_name}
50
+ select *
51
+ from {{{{ source('raw', '{table_name}') }}}}
52
+ """
53
+ model_file.write_text(sql_content)
54
+
55
+
56
+ def create_profiles_yml(dest: Path, profile_name: str) -> None:
57
+ profiles_file = dest / "profiles.yml"
58
+ if profiles_file.exists():
59
+ content = profiles_file.read_text()
60
+ if profile_name in content:
61
+ return
62
+ _render_template(
63
+ template_name="profiles.yml.jinja",
64
+ output_path=profiles_file,
65
+ context={"profile_name": profile_name},
66
+ )
67
+
68
+
69
+ def _render_template(template_name: str, output_path: Path, context: dict) -> None:
70
+ template_path = TEMPLATES_DIR / template_name
71
+ if not template_path.exists():
72
+ raise FileNotFoundError(f"Template not found: {template_path}")
73
+ template = jinja2.Template(template_path.read_text())
74
+ output_path.write_text(template.render(**context))