seedloom 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.
seedloom-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 seedloom
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,186 @@
1
+ Metadata-Version: 2.4
2
+ Name: seedloom
3
+ Version: 0.1.0
4
+ Summary: AI-powered database seeding: introspects your schema and generates realistic, referentially-valid seed data with Claude, OpenAI, Gemini, or local models.
5
+ Author-email: Naeem Ahmed <freelancer.nak@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/therealonenak/seedloom
8
+ Project-URL: Repository, https://github.com/therealonenak/seedloom
9
+ Project-URL: Issues, https://github.com/therealonenak/seedloom/issues
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: psycopg2-binary>=2.9
14
+ Requires-Dist: click>=8.1
15
+ Requires-Dist: rich>=13.0
16
+ Provides-Extra: anthropic
17
+ Requires-Dist: anthropic>=0.40.0; extra == "anthropic"
18
+ Provides-Extra: openai
19
+ Requires-Dist: openai>=1.50.0; extra == "openai"
20
+ Provides-Extra: gemini
21
+ Requires-Dist: google-genai>=0.3.0; extra == "gemini"
22
+ Provides-Extra: ollama
23
+ Requires-Dist: requests>=2.31; extra == "ollama"
24
+ Provides-Extra: all
25
+ Requires-Dist: seedloom[anthropic,gemini,ollama,openai]; extra == "all"
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Requires-Dist: seedloom[all]; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # seedloom
32
+
33
+ AI-powered database seeding. `seedloom` connects to your PostgreSQL database,
34
+ introspects the real schema (tables, columns, types, foreign keys, enums,
35
+ constraints), and uses an LLM to generate realistic, **referentially-valid**
36
+ seed data - then inserts it in the correct dependency order.
37
+
38
+ Works with Claude, OpenAI, Gemini, local Ollama models, and any
39
+ OpenAI-compatible endpoint (Groq, Together, Fireworks, OpenRouter, DeepSeek,
40
+ Mistral, LM Studio, vLLM, text-generation-webui) - see [Providers](#providers).
41
+
42
+ No more hand-writing 50 fake users and hoping your `orders` table's `user_id`
43
+ values actually exist.
44
+
45
+ ## Why not just use Faker?
46
+
47
+ Faker generates *plausible-looking* values per column, with no awareness of
48
+ your schema's relationships. seedloom:
49
+
50
+ - Reads your **actual** schema (works with Prisma, Django, raw SQL migrations - anything)
51
+ - Resolves foreign key dependency order automatically (seeds `users` before `orders` before `order_items`)
52
+ - Constrains foreign-key columns to an `enum` of **real, already-inserted** parent IDs - the model
53
+ structurally cannot invent a dangling reference
54
+ - Respects `NOT NULL`, `UNIQUE`, enum types, and column types when generating values
55
+ - Skips columns the DB fills in itself (`SERIAL`, `gen_random_uuid()`, `now()` defaults)
56
+ - Auto-fills image/avatar/logo/banner/video columns with real, working CDN URLs
57
+ (picsum.photos, i.pravatar.cc, sample .mp4s) instead of letting the model
58
+ invent broken links
59
+ - Automatically excludes ORM/migration bookkeeping tables (`SequelizeMeta`,
60
+ `knex_migrations`, `alembic_version`, `django_migrations`,
61
+ `__EFMigrationsHistory`, etc.) from introspection - they're never seeded
62
+ - Swap the LLM provider with one flag or env var - cloud or fully local/free
63
+
64
+ ## Install
65
+
66
+ ```bash
67
+ pip install seedloom[anthropic] # or [openai], [gemini], [ollama], [all]
68
+ ```
69
+
70
+ Or from source:
71
+
72
+ ```bash
73
+ git clone https://github.com/therealonenak/seedloom
74
+ cd seedloom
75
+ pip install -e ".[all]"
76
+ ```
77
+
78
+ Each provider is an optional extra so you only install the SDK you need.
79
+ `ollama` has no cloud SDK - it just needs `requests`, already covered by that extra.
80
+
81
+ ## Setup
82
+
83
+ Set `DATABASE_URL` plus whichever provider you're using (or put them in a `.env`
84
+ file in your working directory):
85
+
86
+ ```bash
87
+ export DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"
88
+ export SEEDLOOM_PROVIDER="anthropic" # default; see Providers below
89
+ export ANTHROPIC_API_KEY="sk-ant-..."
90
+ ```
91
+
92
+ ## Usage
93
+
94
+ ```bash
95
+ # 1. Introspect your schema and cache it locally
96
+ seedloom init
97
+
98
+ # 2. Generate + insert seed data, in FK-safe order
99
+ seedloom run --rows 20
100
+
101
+ # Preview generated data without inserting anything
102
+ seedloom run --rows 5 --dry-run
103
+
104
+ # Only seed specific tables
105
+ seedloom run --rows 50 --tables users,products
106
+
107
+ # Override the provider/model for a single run
108
+ seedloom run --provider gemini --model gemini-2.5-flash --rows 20
109
+ seedloom run --provider ollama --model llama3.1 --rows 20
110
+ ```
111
+
112
+ Try it against the example schema in `examples/schema.sql` on a scratch database.
113
+
114
+ ## Providers
115
+
116
+ Set `SEEDLOOM_PROVIDER` (or pass `--provider`) plus the matching API key env var.
117
+ `SEEDLOOM_MODEL` overrides the default model for any provider.
118
+
119
+ | Provider | `SEEDLOOM_PROVIDER` | API key env var | Notes |
120
+ |---|---|---|---|
121
+ | Anthropic (Claude) | `anthropic` | `ANTHROPIC_API_KEY` | default |
122
+ | OpenAI | `openai` | `OPENAI_API_KEY` | |
123
+ | Google Gemini | `gemini` | `GEMINI_API_KEY` | official Gemini API; free tier is capped at 20 requests/day per model - schemas with many tables will likely exhaust it in one run |
124
+ | Ollama (local) | `ollama` | - | run any open-source model locally; needs `ollama serve` |
125
+ | Groq | `groq` | `GROQ_API_KEY` | free tier, serves open-source models fast |
126
+ | Together AI | `together` | `TOGETHER_API_KEY` | open-source models, free tier |
127
+ | Fireworks AI | `fireworks` | `FIREWORKS_API_KEY` | open-source models |
128
+ | OpenRouter | `openrouter` | `OPENROUTER_API_KEY` | many free `:free`-suffixed open-source models |
129
+ | DeepSeek | `deepseek` | `DEEPSEEK_API_KEY` | |
130
+ | Mistral | `mistral` | `MISTRAL_API_KEY` | |
131
+ | LM Studio (local) | `lmstudio` | - | local server, OpenAI-compatible |
132
+ | vLLM (local) | `vllm` | - | local server, OpenAI-compatible |
133
+ | text-generation-webui (local) | `text_generation_webui` | - | local server, OpenAI-compatible |
134
+ | Any OpenAI-compatible endpoint | `openai_compatible` | `OPENAI_COMPATIBLE_API_KEY` | set `SEEDLOOM_BASE_URL` |
135
+
136
+ Ollama host defaults to `http://localhost:11434`; override with `SEEDLOOM_HOST` or `--host`.
137
+ Custom/self-hosted OpenAI-compatible endpoints use `SEEDLOOM_BASE_URL` or `--base-url`.
138
+
139
+ A note on **Google Antigravity**: it's a desktop agentic IDE, not something with
140
+ a public API for scripts like this to call. If you want Google's models
141
+ programmatically, `gemini` (the official Gemini API) is the supported path -
142
+ same models, real API key, free tier included.
143
+
144
+ ## How it works
145
+
146
+ 1. **Introspect** - queries `information_schema` / `pg_catalog` to build a full
147
+ picture of your schema: columns, types, nullability, uniqueness, enums, and
148
+ foreign keys. ORM/migration bookkeeping tables are filtered out automatically.
149
+ 2. **Order** - topologically sorts tables so parents are always seeded before
150
+ their dependents (`users` → `orders` → `order_items`).
151
+ 3. **Generate** - for each table, builds a JSON Schema describing exactly what
152
+ a valid row looks like (including an `enum` of real parent-key values for
153
+ any FK column) and asks the configured provider to fill it in via
154
+ tool-use / structured output. Image/video columns are then populated with
155
+ real, working CDN URLs rather than model-invented links.
156
+ 4. **Insert** - batch-inserts the generated rows and tracks the primary keys
157
+ the database assigns, so the *next* table's foreign keys always point at
158
+ something real.
159
+
160
+ ## Rate limits
161
+
162
+ Cloud providers get automatic retry with exponential backoff on transient
163
+ 429/rate-limit errors, honoring the provider's own suggested retry delay
164
+ when it's present in the error. If a provider reports a **daily** quota is
165
+ exhausted (e.g. Gemini's free-tier 20-requests/day cap), seedloom fails
166
+ fast with a clear message instead of retrying for several minutes against
167
+ a quota that won't reset - switch `--provider`/`--model` or re-run once it
168
+ resets. Already-seeded tables are skipped automatically on the next run.
169
+
170
+ ## Limitations (v1)
171
+
172
+ - PostgreSQL only (MySQL/SQLite support welcome as a PR)
173
+ - Single-column primary keys only for FK-pool tracking (composite PKs insert fine, just aren't reused as FK sources yet)
174
+ - No deferred-constraint support for genuinely cyclic FK relationships between two tables
175
+ - No per-provider quota/cost estimation before a run - a large schema can burn through a free-tier daily quota in a single `seedloom run`
176
+
177
+ ## Contributing
178
+
179
+ PRs welcome - this was built as a learning project and is deliberately kept
180
+ readable over clever. Good first contributions: MySQL/SQLite introspection,
181
+ composite PK support, a `--seed-from-existing` mode that samples real rows
182
+ for context instead of generating from scratch.
183
+
184
+ ## License
185
+
186
+ MIT
@@ -0,0 +1,156 @@
1
+ # seedloom
2
+
3
+ AI-powered database seeding. `seedloom` connects to your PostgreSQL database,
4
+ introspects the real schema (tables, columns, types, foreign keys, enums,
5
+ constraints), and uses an LLM to generate realistic, **referentially-valid**
6
+ seed data - then inserts it in the correct dependency order.
7
+
8
+ Works with Claude, OpenAI, Gemini, local Ollama models, and any
9
+ OpenAI-compatible endpoint (Groq, Together, Fireworks, OpenRouter, DeepSeek,
10
+ Mistral, LM Studio, vLLM, text-generation-webui) - see [Providers](#providers).
11
+
12
+ No more hand-writing 50 fake users and hoping your `orders` table's `user_id`
13
+ values actually exist.
14
+
15
+ ## Why not just use Faker?
16
+
17
+ Faker generates *plausible-looking* values per column, with no awareness of
18
+ your schema's relationships. seedloom:
19
+
20
+ - Reads your **actual** schema (works with Prisma, Django, raw SQL migrations - anything)
21
+ - Resolves foreign key dependency order automatically (seeds `users` before `orders` before `order_items`)
22
+ - Constrains foreign-key columns to an `enum` of **real, already-inserted** parent IDs - the model
23
+ structurally cannot invent a dangling reference
24
+ - Respects `NOT NULL`, `UNIQUE`, enum types, and column types when generating values
25
+ - Skips columns the DB fills in itself (`SERIAL`, `gen_random_uuid()`, `now()` defaults)
26
+ - Auto-fills image/avatar/logo/banner/video columns with real, working CDN URLs
27
+ (picsum.photos, i.pravatar.cc, sample .mp4s) instead of letting the model
28
+ invent broken links
29
+ - Automatically excludes ORM/migration bookkeeping tables (`SequelizeMeta`,
30
+ `knex_migrations`, `alembic_version`, `django_migrations`,
31
+ `__EFMigrationsHistory`, etc.) from introspection - they're never seeded
32
+ - Swap the LLM provider with one flag or env var - cloud or fully local/free
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install seedloom[anthropic] # or [openai], [gemini], [ollama], [all]
38
+ ```
39
+
40
+ Or from source:
41
+
42
+ ```bash
43
+ git clone https://github.com/therealonenak/seedloom
44
+ cd seedloom
45
+ pip install -e ".[all]"
46
+ ```
47
+
48
+ Each provider is an optional extra so you only install the SDK you need.
49
+ `ollama` has no cloud SDK - it just needs `requests`, already covered by that extra.
50
+
51
+ ## Setup
52
+
53
+ Set `DATABASE_URL` plus whichever provider you're using (or put them in a `.env`
54
+ file in your working directory):
55
+
56
+ ```bash
57
+ export DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"
58
+ export SEEDLOOM_PROVIDER="anthropic" # default; see Providers below
59
+ export ANTHROPIC_API_KEY="sk-ant-..."
60
+ ```
61
+
62
+ ## Usage
63
+
64
+ ```bash
65
+ # 1. Introspect your schema and cache it locally
66
+ seedloom init
67
+
68
+ # 2. Generate + insert seed data, in FK-safe order
69
+ seedloom run --rows 20
70
+
71
+ # Preview generated data without inserting anything
72
+ seedloom run --rows 5 --dry-run
73
+
74
+ # Only seed specific tables
75
+ seedloom run --rows 50 --tables users,products
76
+
77
+ # Override the provider/model for a single run
78
+ seedloom run --provider gemini --model gemini-2.5-flash --rows 20
79
+ seedloom run --provider ollama --model llama3.1 --rows 20
80
+ ```
81
+
82
+ Try it against the example schema in `examples/schema.sql` on a scratch database.
83
+
84
+ ## Providers
85
+
86
+ Set `SEEDLOOM_PROVIDER` (or pass `--provider`) plus the matching API key env var.
87
+ `SEEDLOOM_MODEL` overrides the default model for any provider.
88
+
89
+ | Provider | `SEEDLOOM_PROVIDER` | API key env var | Notes |
90
+ |---|---|---|---|
91
+ | Anthropic (Claude) | `anthropic` | `ANTHROPIC_API_KEY` | default |
92
+ | OpenAI | `openai` | `OPENAI_API_KEY` | |
93
+ | Google Gemini | `gemini` | `GEMINI_API_KEY` | official Gemini API; free tier is capped at 20 requests/day per model - schemas with many tables will likely exhaust it in one run |
94
+ | Ollama (local) | `ollama` | - | run any open-source model locally; needs `ollama serve` |
95
+ | Groq | `groq` | `GROQ_API_KEY` | free tier, serves open-source models fast |
96
+ | Together AI | `together` | `TOGETHER_API_KEY` | open-source models, free tier |
97
+ | Fireworks AI | `fireworks` | `FIREWORKS_API_KEY` | open-source models |
98
+ | OpenRouter | `openrouter` | `OPENROUTER_API_KEY` | many free `:free`-suffixed open-source models |
99
+ | DeepSeek | `deepseek` | `DEEPSEEK_API_KEY` | |
100
+ | Mistral | `mistral` | `MISTRAL_API_KEY` | |
101
+ | LM Studio (local) | `lmstudio` | - | local server, OpenAI-compatible |
102
+ | vLLM (local) | `vllm` | - | local server, OpenAI-compatible |
103
+ | text-generation-webui (local) | `text_generation_webui` | - | local server, OpenAI-compatible |
104
+ | Any OpenAI-compatible endpoint | `openai_compatible` | `OPENAI_COMPATIBLE_API_KEY` | set `SEEDLOOM_BASE_URL` |
105
+
106
+ Ollama host defaults to `http://localhost:11434`; override with `SEEDLOOM_HOST` or `--host`.
107
+ Custom/self-hosted OpenAI-compatible endpoints use `SEEDLOOM_BASE_URL` or `--base-url`.
108
+
109
+ A note on **Google Antigravity**: it's a desktop agentic IDE, not something with
110
+ a public API for scripts like this to call. If you want Google's models
111
+ programmatically, `gemini` (the official Gemini API) is the supported path -
112
+ same models, real API key, free tier included.
113
+
114
+ ## How it works
115
+
116
+ 1. **Introspect** - queries `information_schema` / `pg_catalog` to build a full
117
+ picture of your schema: columns, types, nullability, uniqueness, enums, and
118
+ foreign keys. ORM/migration bookkeeping tables are filtered out automatically.
119
+ 2. **Order** - topologically sorts tables so parents are always seeded before
120
+ their dependents (`users` → `orders` → `order_items`).
121
+ 3. **Generate** - for each table, builds a JSON Schema describing exactly what
122
+ a valid row looks like (including an `enum` of real parent-key values for
123
+ any FK column) and asks the configured provider to fill it in via
124
+ tool-use / structured output. Image/video columns are then populated with
125
+ real, working CDN URLs rather than model-invented links.
126
+ 4. **Insert** - batch-inserts the generated rows and tracks the primary keys
127
+ the database assigns, so the *next* table's foreign keys always point at
128
+ something real.
129
+
130
+ ## Rate limits
131
+
132
+ Cloud providers get automatic retry with exponential backoff on transient
133
+ 429/rate-limit errors, honoring the provider's own suggested retry delay
134
+ when it's present in the error. If a provider reports a **daily** quota is
135
+ exhausted (e.g. Gemini's free-tier 20-requests/day cap), seedloom fails
136
+ fast with a clear message instead of retrying for several minutes against
137
+ a quota that won't reset - switch `--provider`/`--model` or re-run once it
138
+ resets. Already-seeded tables are skipped automatically on the next run.
139
+
140
+ ## Limitations (v1)
141
+
142
+ - PostgreSQL only (MySQL/SQLite support welcome as a PR)
143
+ - Single-column primary keys only for FK-pool tracking (composite PKs insert fine, just aren't reused as FK sources yet)
144
+ - No deferred-constraint support for genuinely cyclic FK relationships between two tables
145
+ - No per-provider quota/cost estimation before a run - a large schema can burn through a free-tier daily quota in a single `seedloom run`
146
+
147
+ ## Contributing
148
+
149
+ PRs welcome - this was built as a learning project and is deliberately kept
150
+ readable over clever. Good first contributions: MySQL/SQLite introspection,
151
+ composite PK support, a `--seed-from-existing` mode that samples real rows
152
+ for context instead of generating from scratch.
153
+
154
+ ## License
155
+
156
+ MIT
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "seedloom"
7
+ version = "0.1.0"
8
+ description = "AI-powered database seeding: introspects your schema and generates realistic, referentially-valid seed data with Claude, OpenAI, Gemini, or local models."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Naeem Ahmed", email = "freelancer.nak@gmail.com" }]
13
+ dependencies = [
14
+ "psycopg2-binary>=2.9",
15
+ "click>=8.1",
16
+ "rich>=13.0",
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/therealonenak/seedloom"
21
+ Repository = "https://github.com/therealonenak/seedloom"
22
+ Issues = "https://github.com/therealonenak/seedloom/issues"
23
+
24
+ [project.optional-dependencies]
25
+ anthropic = ["anthropic>=0.40.0"]
26
+ openai = ["openai>=1.50.0"]
27
+ gemini = ["google-genai>=0.3.0"]
28
+ ollama = ["requests>=2.31"]
29
+ all = ["seedloom[anthropic,openai,gemini,ollama]"]
30
+ dev = ["pytest>=8.0", "seedloom[all]"]
31
+
32
+ [project.scripts]
33
+ seedloom = "seedloom.cli:main"
34
+
35
+ [tool.setuptools.packages.find]
36
+ include = ["seedloom*"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,195 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import click
8
+ import psycopg2
9
+ from rich.console import Console
10
+ from rich.table import Table as RichTable
11
+
12
+ from .config import Config
13
+ from .generator import generate_rows
14
+ from .graph import CyclicDependencyError, resolve_seed_order
15
+ from .inserter import existing_column_values, insert_rows, table_row_count
16
+ from .introspect import introspect
17
+ from .models import Schema
18
+ from .providers import ProviderError, SUPPORTED_PROVIDERS, get_provider
19
+
20
+ console = Console()
21
+ SCHEMA_CACHE = Path(".seedloom_schema.json")
22
+
23
+
24
+ @click.group()
25
+ def main() -> None:
26
+ """seedloom — AI-powered database seeding.
27
+
28
+ Introspects your Postgres schema and uses Claude to generate realistic,
29
+ referentially-valid seed data.
30
+ """
31
+
32
+
33
+ @main.command()
34
+ def init() -> None:
35
+ """Connect to the database, introspect the schema, and cache it locally."""
36
+ try:
37
+ config = Config.load(require_provider=False)
38
+ except EnvironmentError as e:
39
+ console.print(f"[red]{e}[/red]")
40
+ sys.exit(1)
41
+
42
+ console.print("[cyan]Connecting and introspecting schema...[/cyan]")
43
+ try:
44
+ schema = introspect(config.database_url)
45
+ except psycopg2.OperationalError as e:
46
+ console.print(f"[red]Could not connect to database: {e}[/red]")
47
+ sys.exit(1)
48
+
49
+ if not schema.tables:
50
+ console.print("[yellow]No tables found in the 'public' schema.[/yellow]")
51
+ sys.exit(0)
52
+
53
+ SCHEMA_CACHE.write_text(json.dumps(schema.to_dict(), indent=2))
54
+
55
+ t = RichTable(title="Discovered schema")
56
+ t.add_column("Table")
57
+ t.add_column("Columns")
58
+ t.add_column("Foreign Keys")
59
+ for table in schema.tables.values():
60
+ fks = ", ".join(f"{fk.column}->{fk.ref_table}.{fk.ref_column}" for fk in table.foreign_keys)
61
+ t.add_row(table.name, str(len(table.columns)), fks or "-")
62
+ console.print(t)
63
+ console.print(f"[green]Schema cached to {SCHEMA_CACHE}[/green]. Run 'seedloom run' next.")
64
+
65
+
66
+ @main.command()
67
+ @click.option("--rows", default=10, show_default=True, help="Rows to generate per table.")
68
+ @click.option("--tables", default=None, help="Comma-separated subset of tables to seed (default: all).")
69
+ @click.option("--dry-run", is_flag=True, help="Generate data and print it without inserting.")
70
+ @click.option(
71
+ "--provider",
72
+ default=None,
73
+ help=f"Override provider from config. Supported: {', '.join(SUPPORTED_PROVIDERS)}.",
74
+ )
75
+ @click.option("--model", default=None, help="Override model from config.")
76
+ @click.option("--base-url", default=None, help="Override base URL (openai_compatible or self-hosted endpoints).")
77
+ @click.option("--host", default=None, help="Override Ollama host (default: http://localhost:11434).")
78
+ def run(
79
+ rows: int,
80
+ tables: str | None,
81
+ dry_run: bool,
82
+ provider: str | None,
83
+ model: str | None,
84
+ base_url: str | None,
85
+ host: str | None,
86
+ ) -> None:
87
+ """Generate and insert seed data, respecting foreign key order."""
88
+ try:
89
+ config = Config.load(provider_override=provider or "")
90
+ except EnvironmentError as e:
91
+ console.print(f"[red]{e}[/red]")
92
+ sys.exit(1)
93
+
94
+ if not SCHEMA_CACHE.exists():
95
+ console.print("[red]No cached schema found. Run 'seedloom init' first.[/red]")
96
+ sys.exit(1)
97
+
98
+ schema = Schema.from_dict(json.loads(SCHEMA_CACHE.read_text()))
99
+
100
+ try:
101
+ order = resolve_seed_order(schema)
102
+ except CyclicDependencyError as e:
103
+ console.print(f"[red]{e}[/red]")
104
+ sys.exit(1)
105
+
106
+ if tables:
107
+ wanted = set(t.strip() for t in tables.split(","))
108
+ order = [t for t in order if t in wanted]
109
+
110
+ try:
111
+ active_provider = get_provider(
112
+ config.provider,
113
+ api_key=config.api_key,
114
+ model=model or config.model,
115
+ base_url=base_url or config.base_url,
116
+ host=host or config.host,
117
+ )
118
+ except ProviderError as e:
119
+ console.print(f"[red]{e}[/red]")
120
+ sys.exit(1)
121
+
122
+ console.print(f"[cyan]Using provider: {config.provider}[/cyan]")
123
+ conn = None if dry_run else psycopg2.connect(config.database_url)
124
+
125
+ fk_pools: dict[str, dict[str, list]] = {} # table -> column -> values
126
+
127
+ referenced_columns: dict[str, set[str]] = {}
128
+ for t in schema.tables.values():
129
+ for fk in t.foreign_keys:
130
+ referenced_columns.setdefault(fk.ref_table, set()).add(fk.ref_column)
131
+
132
+ try:
133
+ for table_name in order:
134
+ table = schema.tables[table_name]
135
+ needed_columns = sorted(referenced_columns.get(table_name, set()))
136
+
137
+ to_generate = rows
138
+ if conn is not None:
139
+ existing_count = table_row_count(conn, table_name)
140
+ if existing_count > 0 and needed_columns:
141
+ existing_values = existing_column_values(conn, table_name, needed_columns)
142
+ for col, vals in existing_values.items():
143
+ if vals:
144
+ fk_pools.setdefault(table_name, {})[col] = vals
145
+
146
+ if existing_count >= rows:
147
+ console.print(
148
+ f"[yellow]Skipping '{table_name}' — already has {existing_count} row(s) "
149
+ f"(>= {rows} requested).[/yellow]"
150
+ )
151
+ continue
152
+
153
+ to_generate = rows - existing_count
154
+ if existing_count > 0:
155
+ console.print(
156
+ f"[cyan]'{table_name}' has {existing_count} row(s); generating "
157
+ f"{to_generate} more to reach {rows}...[/cyan]"
158
+ )
159
+ else:
160
+ console.print(f"[cyan]Generating {to_generate} rows for '{table_name}'...[/cyan]")
161
+ else:
162
+ console.print(f"[cyan]Generating {to_generate} rows for '{table_name}'...[/cyan]")
163
+
164
+ fk_value_pool: dict[str, list] = {}
165
+ for fk in table.foreign_keys:
166
+ parent_pool = fk_pools.get(fk.ref_table, {}).get(fk.ref_column, [])
167
+ if parent_pool:
168
+ fk_value_pool[fk.column] = parent_pool
169
+
170
+ try:
171
+ generated = generate_rows(active_provider, table, to_generate, fk_value_pool)
172
+ except ProviderError as e:
173
+ console.print(f"[red]{e}[/red]")
174
+ sys.exit(1)
175
+
176
+ if dry_run:
177
+ console.print(generated)
178
+ continue
179
+
180
+ inserted_values = insert_rows(conn, table, generated, needed_columns)
181
+ for col, vals in inserted_values.items():
182
+ if vals:
183
+ fk_pools.setdefault(table_name, {}).setdefault(col, [])
184
+ fk_pools[table_name][col].extend(vals)
185
+
186
+ console.print(f"[green]Inserted {len(generated)} rows into '{table_name}'.[/green]")
187
+ finally:
188
+ if conn:
189
+ conn.close()
190
+
191
+ console.print("[bold green]Done.[/bold green]")
192
+
193
+
194
+ if __name__ == "__main__":
195
+ main()
@@ -0,0 +1,85 @@
1
+ """Configuration loading: env vars + optional .env file, no external deps."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from .providers import NO_KEY_REQUIRED, SUPPORTED_PROVIDERS
9
+
10
+ _PROVIDER_KEY_ENV: dict[str, str] = {
11
+ "anthropic": "ANTHROPIC_API_KEY",
12
+ "openai": "OPENAI_API_KEY",
13
+ "gemini": "GEMINI_API_KEY",
14
+ "groq": "GROQ_API_KEY",
15
+ "together": "TOGETHER_API_KEY",
16
+ "fireworks": "FIREWORKS_API_KEY",
17
+ "openrouter": "OPENROUTER_API_KEY",
18
+ "deepseek": "DEEPSEEK_API_KEY",
19
+ "mistral": "MISTRAL_API_KEY",
20
+ "openai_compatible": "OPENAI_COMPATIBLE_API_KEY",
21
+ }
22
+
23
+
24
+ def _load_dotenv(path: Path = Path(".env")) -> None:
25
+ """Minimal .env loader — avoids pulling in python-dotenv as a dependency."""
26
+ if not path.exists():
27
+ return
28
+ for line in path.read_text().splitlines():
29
+ line = line.strip()
30
+ if not line or line.startswith("#") or "=" not in line:
31
+ continue
32
+ key, _, value = line.partition("=")
33
+ key = key.strip()
34
+ value = value.strip().strip('"').strip("'")
35
+ os.environ.setdefault(key, value)
36
+
37
+
38
+ @dataclass
39
+ class Config:
40
+ database_url: str
41
+ provider: str = "anthropic"
42
+ api_key: str = ""
43
+ model: str = ""
44
+ base_url: str = ""
45
+ host: str = ""
46
+
47
+ @classmethod
48
+ def load(cls, provider_override: str = "", require_provider: bool = True) -> "Config":
49
+ _load_dotenv()
50
+ db_url = os.environ.get("DATABASE_URL", "")
51
+ provider = (provider_override or os.environ.get("SEEDLOOM_PROVIDER", "anthropic")).lower()
52
+ model = os.environ.get("SEEDLOOM_MODEL", "")
53
+ base_url = os.environ.get("SEEDLOOM_BASE_URL", "")
54
+ host = os.environ.get("SEEDLOOM_HOST", "")
55
+
56
+ missing = []
57
+ if not db_url:
58
+ missing.append("DATABASE_URL")
59
+
60
+ if provider not in SUPPORTED_PROVIDERS:
61
+ raise EnvironmentError(
62
+ f"Unknown provider '{provider}'. Supported: {', '.join(SUPPORTED_PROVIDERS)}."
63
+ )
64
+
65
+ api_key = ""
66
+ if require_provider and provider not in NO_KEY_REQUIRED:
67
+ key_env = _PROVIDER_KEY_ENV.get(provider, f"{provider.upper()}_API_KEY")
68
+ api_key = os.environ.get(key_env, "")
69
+ if not api_key:
70
+ missing.append(key_env)
71
+
72
+ if missing:
73
+ raise EnvironmentError(
74
+ f"Missing required environment variable(s): {', '.join(missing)}. "
75
+ "Set them in your shell or in a .env file in the current directory."
76
+ )
77
+
78
+ return cls(
79
+ database_url=db_url,
80
+ provider=provider,
81
+ api_key=api_key,
82
+ model=model,
83
+ base_url=base_url,
84
+ host=host,
85
+ )