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 +21 -0
- seedloom-0.1.0/PKG-INFO +186 -0
- seedloom-0.1.0/README.md +156 -0
- seedloom-0.1.0/pyproject.toml +36 -0
- seedloom-0.1.0/seedloom/__init__.py +1 -0
- seedloom-0.1.0/seedloom/cli.py +195 -0
- seedloom-0.1.0/seedloom/config.py +85 -0
- seedloom-0.1.0/seedloom/generator.py +235 -0
- seedloom-0.1.0/seedloom/graph.py +50 -0
- seedloom-0.1.0/seedloom/inserter.py +93 -0
- seedloom-0.1.0/seedloom/introspect.py +167 -0
- seedloom-0.1.0/seedloom/models.py +96 -0
- seedloom-0.1.0/seedloom/providers/__init__.py +93 -0
- seedloom-0.1.0/seedloom/providers/anthropic_provider.py +52 -0
- seedloom-0.1.0/seedloom/providers/base.py +89 -0
- seedloom-0.1.0/seedloom/providers/gemini_provider.py +69 -0
- seedloom-0.1.0/seedloom/providers/ollama_provider.py +61 -0
- seedloom-0.1.0/seedloom/providers/openai_provider.py +70 -0
- seedloom-0.1.0/seedloom.egg-info/PKG-INFO +186 -0
- seedloom-0.1.0/seedloom.egg-info/SOURCES.txt +26 -0
- seedloom-0.1.0/seedloom.egg-info/dependency_links.txt +1 -0
- seedloom-0.1.0/seedloom.egg-info/entry_points.txt +2 -0
- seedloom-0.1.0/seedloom.egg-info/requires.txt +22 -0
- seedloom-0.1.0/seedloom.egg-info/top_level.txt +1 -0
- seedloom-0.1.0/setup.cfg +4 -0
- seedloom-0.1.0/tests/test_generator.py +74 -0
- seedloom-0.1.0/tests/test_graph.py +87 -0
- seedloom-0.1.0/tests/test_integration.py +113 -0
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.
|
seedloom-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
seedloom-0.1.0/README.md
ADDED
|
@@ -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
|
+
)
|