google-drive-model 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.
Files changed (46) hide show
  1. google_drive_model-0.1.0/PKG-INFO +277 -0
  2. google_drive_model-0.1.0/README.md +243 -0
  3. google_drive_model-0.1.0/pyproject.toml +164 -0
  4. google_drive_model-0.1.0/pyproject.toml.orig +141 -0
  5. google_drive_model-0.1.0/src/gdm/__init__.py +77 -0
  6. google_drive_model-0.1.0/src/gdm/auth.py +514 -0
  7. google_drive_model-0.1.0/src/gdm/backends/__init__.py +15 -0
  8. google_drive_model-0.1.0/src/gdm/backends/base.py +115 -0
  9. google_drive_model-0.1.0/src/gdm/backends/drive.py +716 -0
  10. google_drive_model-0.1.0/src/gdm/backends/memory.py +448 -0
  11. google_drive_model-0.1.0/src/gdm/backends/registry.py +1133 -0
  12. google_drive_model-0.1.0/src/gdm/backends/sheets.py +644 -0
  13. google_drive_model-0.1.0/src/gdm/cli.py +1108 -0
  14. google_drive_model-0.1.0/src/gdm/client.py +383 -0
  15. google_drive_model-0.1.0/src/gdm/codecs.py +246 -0
  16. google_drive_model-0.1.0/src/gdm/collection/__init__.py +23 -0
  17. google_drive_model-0.1.0/src/gdm/collection/backend.py +515 -0
  18. google_drive_model-0.1.0/src/gdm/collection/collection.py +350 -0
  19. google_drive_model-0.1.0/src/gdm/collection/constants.py +59 -0
  20. google_drive_model-0.1.0/src/gdm/collection/content.py +496 -0
  21. google_drive_model-0.1.0/src/gdm/collection/inventory.py +285 -0
  22. google_drive_model-0.1.0/src/gdm/collection/metadata.py +780 -0
  23. google_drive_model-0.1.0/src/gdm/errors.py +136 -0
  24. google_drive_model-0.1.0/src/gdm/explore.py +489 -0
  25. google_drive_model-0.1.0/src/gdm/explore_types.py +64 -0
  26. google_drive_model-0.1.0/src/gdm/fields.py +166 -0
  27. google_drive_model-0.1.0/src/gdm/google_api.py +631 -0
  28. google_drive_model-0.1.0/src/gdm/indexes.py +735 -0
  29. google_drive_model-0.1.0/src/gdm/infer.py +644 -0
  30. google_drive_model-0.1.0/src/gdm/migration_target.py +842 -0
  31. google_drive_model-0.1.0/src/gdm/migrations.py +1774 -0
  32. google_drive_model-0.1.0/src/gdm/model.py +468 -0
  33. google_drive_model-0.1.0/src/gdm/permissions.py +278 -0
  34. google_drive_model-0.1.0/src/gdm/project.py +738 -0
  35. google_drive_model-0.1.0/src/gdm/py.typed +1 -0
  36. google_drive_model-0.1.0/src/gdm/query.py +366 -0
  37. google_drive_model-0.1.0/src/gdm/retry.py +145 -0
  38. google_drive_model-0.1.0/src/gdm/schema.py +850 -0
  39. google_drive_model-0.1.0/src/gdm/session.py +623 -0
  40. google_drive_model-0.1.0/src/gdm/sync.py +236 -0
  41. google_drive_model-0.1.0/src/gdm/testing/__init__.py +45 -0
  42. google_drive_model-0.1.0/src/gdm/tooling/__init__.py +3 -0
  43. google_drive_model-0.1.0/src/gdm/tooling/adopt.py +100 -0
  44. google_drive_model-0.1.0/src/gdm/tooling/collection_probe.py +170 -0
  45. google_drive_model-0.1.0/src/gdm/tooling/doctor.py +267 -0
  46. google_drive_model-0.1.0/src/gdm/tooling/mime.py +62 -0
@@ -0,0 +1,277 @@
1
+ Metadata-Version: 2.4
2
+ Name: google-drive-model
3
+ Version: 0.1.0
4
+ Summary: Pydantic models and a SQLModel-inspired API for Google Drive and Sheets
5
+ Keywords: google-drive,google-sheets,orm,odm,pydantic
6
+ Author: Yotam Manor
7
+ License-Expression: MIT
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Framework :: Pydantic :: 2
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Typing :: Typed
18
+ Requires-Dist: google-api-python-client>=2.160.0
19
+ Requires-Dist: google-auth>=2.37.0
20
+ Requires-Dist: google-auth-httplib2>=0.2.0
21
+ Requires-Dist: pydantic>=2.10.0
22
+ Requires-Dist: rich>=13.9.0
23
+ Requires-Dist: typer>=0.15.0
24
+ Requires-Dist: google-auth-oauthlib>=1.2.1 ; extra == 'oauth'
25
+ Requires-Dist: pyyaml>=6.0.2 ; extra == 'yaml'
26
+ Requires-Python: >=3.11
27
+ Project-URL: Changelog, https://github.com/yotammanor/gdm/blob/main/CHANGELOG.md
28
+ Project-URL: Documentation, https://yotammanor.github.io/gdm/
29
+ Project-URL: Issues, https://github.com/yotammanor/gdm/issues
30
+ Project-URL: Repository, https://github.com/yotammanor/gdm
31
+ Provides-Extra: oauth
32
+ Provides-Extra: yaml
33
+ Description-Content-Type: text/markdown
34
+
35
+ # Google Drive Model
36
+
37
+ <p align="center">
38
+ <em>Pydantic models and a SQLModel-inspired session for Google Drive folders and Sheets.</em>
39
+ </p>
40
+
41
+ <p align="center">
42
+ <a href="https://github.com/yotammanor/gdm/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/yotammanor/gdm/ci.yml?branch=main&label=CI" alt="CI status"></a>
43
+ <a href="https://github.com/yotammanor/gdm/blob/main/LICENSE"><img src="https://img.shields.io/github/license/yotammanor/gdm.svg" alt="License: MIT"></a>
44
+ <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.11–3.14-blue.svg" alt="Python 3.11–3.14"></a>
45
+ <a href="https://pydantic.dev"><img src="https://img.shields.io/badge/Pydantic-v2-3776AB.svg" alt="Pydantic v2"></a>
46
+ <img src="https://img.shields.io/badge/status-alpha-orange.svg" alt="Development status: alpha">
47
+ </p>
48
+
49
+ ---
50
+
51
+ **GDM** maps Pydantic models onto **existing Drive folders** and **Google Sheets rows** with a familiar `Session`, `select()`, and `Field()` API—without pretending Drive is a relational database.
52
+
53
+ Center your app on a **mixed folder** already full of Google Docs, Slides, images, PDFs, shortcuts, and nested subfolders. GDM maps Drive metadata with `Field(drive=...)`, application metadata with `Field(metadata=...)`, and keeps **native bodies lazy** until you call `object.content`.
54
+
55
+ > **Distribution vs import name:** install **`google-drive-model`**. The PyPI
56
+ > name `gdm` is taken by an unrelated dependency manager. Import and CLI use
57
+ > **`gdm`**:
58
+ >
59
+ > ```python
60
+ > from gdm import GDM, GDMModel, DriveCollection, Session, select
61
+ > ```
62
+
63
+ **Documentation:** [yotammanor.github.io/gdm](https://yotammanor.github.io/gdm/)
64
+ · [docs source](https://github.com/yotammanor/gdm/tree/main/docs)
65
+ · **Design:** [`docs/design.md`](https://github.com/yotammanor/gdm/blob/main/docs/design.md)
66
+
67
+ ---
68
+
69
+ ## Why GDM?
70
+
71
+ | You have… | GDM gives you… |
72
+ | --- | --- |
73
+ | A launch folder with Docs, decks, and PNGs | `DriveCollection[T]` over direct children; MIME filters; lazy native content |
74
+ | A team spreadsheet with extra columns | Progressive sheet mapping—bind declared fields, preserve the rest |
75
+ | Scripts that need typed reads/writes | Pydantic validation on every boundary crossing |
76
+ | Multiple apps on one Google account | Root-scoped projects with isolated `.gdm/` metadata |
77
+
78
+ GDM is **honest about platform limits**: no cross-file transactions, no server-side joins, no row-level security. Drive permissions remain the security boundary.
79
+
80
+ ---
81
+
82
+ ## Installation
83
+
84
+ Install from PyPI:
85
+
86
+ ```console
87
+ pip install "google-drive-model[oauth]"
88
+ ```
89
+
90
+ To work from source:
91
+
92
+ ```console
93
+ git clone https://github.com/yotammanor/gdm.git
94
+ cd gdm
95
+ uv sync --all-groups --frozen
96
+ ```
97
+
98
+ | Extra | Purpose |
99
+ | --- | --- |
100
+ | `oauth` | Interactive installed-app login (`google-auth-oauthlib`) |
101
+ | `yaml` | YAML codec for opt-in structured file collections |
102
+
103
+ ---
104
+
105
+ ## Quickstart: adopt a Drive folder
106
+
107
+ ### 1. Authenticate and open a project
108
+
109
+ ```console
110
+ gdm auth login
111
+ gdm --project-dir . --apply init --root-id YOUR_ROOT_FOLDER_ID --name my-app --remote
112
+ ```
113
+
114
+ This creates the shared project manifest, registry, and collection metadata area
115
+ under the Drive root. Tokens and local schema/index state stay local.
116
+
117
+ ### 2. Define a collection model
118
+
119
+ ```python
120
+ from gdm import DriveObjectMixin, Field, GDMModel
121
+
122
+
123
+ class CampaignAsset(
124
+ GDMModel,
125
+ DriveObjectMixin,
126
+ storage="collection",
127
+ collection="Campaigns/Launch",
128
+ mapping="partial",
129
+ ):
130
+ campaign: str | None = Field(default=None, metadata="campaign")
131
+ approved: bool = Field(default=False, metadata="approved")
132
+ ```
133
+
134
+ `DriveObjectMixin` adds standard read-only Drive fields (`drive_id`, `name`, `mime_type`, …) mapped with `Field(drive=...)`.
135
+
136
+ ### 3. Connect, bind, and use the collection
137
+
138
+ ```python
139
+ from gdm import GDM, select
140
+
141
+
142
+ client = GDM.open(root_folder_id="YOUR_ROOT_FOLDER_ID")
143
+ client.bind_collection(
144
+ CampaignAsset,
145
+ path="Campaigns/Launch", # resolved once; immutable folder ID is persisted
146
+ )
147
+
148
+ assets = client.collection(CampaignAsset)
149
+ images = assets.where_mime("image/*")
150
+ logo = images.get("IMAGE_FILE_ID")
151
+ brief = assets.get("GOOGLE_DOC_FILE_ID")
152
+
153
+ data = logo.content.download() # explicit — metadata queries skip bodies
154
+ doc = brief.content.google_doc() # native Docs API handle
155
+ doc.batch_update([...])
156
+
157
+ with client.session() as session:
158
+ approved = session.exec(
159
+ select(CampaignAsset).where(CampaignAsset.approved == True) # noqa: E712
160
+ ).all()
161
+ ```
162
+
163
+ Collection operations are **explicit about safety**: `adopt` vs `move` vs `shortcut`, `detach` vs `trash`, and `delete_permanently` for irreversible removal. `session.delete()` trashes collection members; it never silently purges Drive files.
164
+
165
+ See the [Drive collections tutorial](https://yotammanor.github.io/gdm/tutorial/collection/) for duplicate names, shortcuts, recursion, and metadata reconciliation.
166
+
167
+ ---
168
+
169
+ ## Sheet tables (second story)
170
+
171
+ Bind a worksheet without rewriting your team's sheet:
172
+
173
+ ```python
174
+ from gdm import Field, GDMModel
175
+
176
+
177
+ class Task(GDMModel, storage="sheet", mapping="partial", source="Tasks/Q1"):
178
+ id: str | None = Field(default=None, primary_key=True)
179
+ title: str
180
+ status: str = "todo"
181
+ ```
182
+
183
+ ```python
184
+ from gdm import GDM, ResourceKind, select
185
+
186
+
187
+ client = GDM.open(root_folder_id="YOUR_ROOT_FOLDER_ID")
188
+ client.bind(
189
+ Task,
190
+ resource_id="YOUR_SPREADSHEET_ID",
191
+ kind=ResourceKind.SPREADSHEET,
192
+ worksheet="Tasks",
193
+ )
194
+ with client.session() as session:
195
+ open_tasks = session.exec(select(Task).where(Task.status == "open").limit(50)).all()
196
+ session.add(Task(title="Ship docs", status="open"))
197
+ session.commit()
198
+ ```
199
+
200
+ Foreign keys can reference **Drive file IDs** from collection models. Relationships resolve client-side with `session.prefetch()`—see [Relationships](https://yotammanor.github.io/gdm/storage/relationships/).
201
+
202
+ ---
203
+
204
+ ## CLI
205
+
206
+ | Command | Purpose |
207
+ | --- | --- |
208
+ | `gdm init` | Local `.gdm/` scaffolding; optional remote project with `--apply --remote` |
209
+ | `gdm explore` | Inventory a folder (metadata only—zero content reads) |
210
+ | `gdm infer collection` | Suggest a collection model from Drive metadata |
211
+ | `gdm infer file` | Infer from a local JSON/text/CSV sample |
212
+ | `gdm adopt` | Bind a folder (dry-run by default) |
213
+ | `gdm doctor` | Metadata reconciliation, drift, scale warnings |
214
+ | `gdm migrate plan/apply/rollback` | Forward, resumable migrations with declared rollback steps |
215
+ | `gdm index rebuild` | Rebuild optional sidecar indexes |
216
+ | `gdm auth login` | Interactive OAuth |
217
+
218
+ Terminal demo (**actual local CLI recording**, no live OAuth or Drive mutations):
219
+
220
+ <p align="center">
221
+ <a href="https://github.com/yotammanor/gdm/blob/main/docs/assets/demo.cast"><img src="https://raw.githubusercontent.com/yotammanor/gdm/main/docs/assets/demo.gif" alt="Recorded GDM CLI demo — open demo.cast to play" width="740"></a>
222
+ </p>
223
+
224
+ Regenerate the canonical cast (and the GIF when `agg` is installed):
225
+ `bash docs/assets/regenerate-demo.sh`
226
+
227
+ ---
228
+
229
+ ## Architecture at a glance
230
+
231
+ ```text
232
+ Your Pydantic models (GDMModel + Field)
233
+
234
+
235
+ GDM client ──► DriveCollection[T] (folder-native CRUD, lazy content)
236
+ │ │
237
+ │ └──► metadata Sheet under .gdm/collections/
238
+
239
+ Session / select() ──► expression tree (metadata queries; no body fetch)
240
+
241
+ ├──► Sheets backend (rows, batchUpdate)
242
+ └──► Drive backend (files, appProperties routing keys)
243
+
244
+
245
+ Drive .gdm/ metadata (manifest, registry, collection metadata)
246
+ Local .gdm/ tooling state (schema snapshots, migration checkpoints, indexes)
247
+ ```
248
+
249
+ **Key limitations** (by design):
250
+
251
+ - `Session.commit()` coordinates writes but is **not** an ACID transaction across files.
252
+ - Metadata queries never download native bodies; content access is explicit via `.content`.
253
+ - Relationships and joins are **client-side** with batched prefetch.
254
+ - Export produces portable views; native Docs/Slides mutation uses their respective APIs.
255
+ - Docs and Slides use atomic revision preconditions. Drive binary and Sheets
256
+ updates use best-effort conflict checks because those APIs expose no general
257
+ conditional write precondition.
258
+
259
+ Full rationale: [`docs/design.md`](https://github.com/yotammanor/gdm/blob/main/docs/design.md).
260
+
261
+ ---
262
+
263
+ ## Contributing
264
+
265
+ See [CONTRIBUTING.md](https://github.com/yotammanor/gdm/blob/main/CONTRIBUTING.md), [SECURITY.md](https://github.com/yotammanor/gdm/blob/main/SECURITY.md), and [CODE_OF_CONDUCT.md](https://github.com/yotammanor/gdm/blob/main/CODE_OF_CONDUCT.md).
266
+
267
+ ```console
268
+ uv run pytest
269
+ uv run ruff check .
270
+ uv run mkdocs build --strict
271
+ ```
272
+
273
+ ---
274
+
275
+ ## License
276
+
277
+ MIT © [Yotam Manor](https://github.com/yotammanor)
@@ -0,0 +1,243 @@
1
+ # Google Drive Model
2
+
3
+ <p align="center">
4
+ <em>Pydantic models and a SQLModel-inspired session for Google Drive folders and Sheets.</em>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://github.com/yotammanor/gdm/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/yotammanor/gdm/ci.yml?branch=main&label=CI" alt="CI status"></a>
9
+ <a href="https://github.com/yotammanor/gdm/blob/main/LICENSE"><img src="https://img.shields.io/github/license/yotammanor/gdm.svg" alt="License: MIT"></a>
10
+ <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.11–3.14-blue.svg" alt="Python 3.11–3.14"></a>
11
+ <a href="https://pydantic.dev"><img src="https://img.shields.io/badge/Pydantic-v2-3776AB.svg" alt="Pydantic v2"></a>
12
+ <img src="https://img.shields.io/badge/status-alpha-orange.svg" alt="Development status: alpha">
13
+ </p>
14
+
15
+ ---
16
+
17
+ **GDM** maps Pydantic models onto **existing Drive folders** and **Google Sheets rows** with a familiar `Session`, `select()`, and `Field()` API—without pretending Drive is a relational database.
18
+
19
+ Center your app on a **mixed folder** already full of Google Docs, Slides, images, PDFs, shortcuts, and nested subfolders. GDM maps Drive metadata with `Field(drive=...)`, application metadata with `Field(metadata=...)`, and keeps **native bodies lazy** until you call `object.content`.
20
+
21
+ > **Distribution vs import name:** install **`google-drive-model`**. The PyPI
22
+ > name `gdm` is taken by an unrelated dependency manager. Import and CLI use
23
+ > **`gdm`**:
24
+ >
25
+ > ```python
26
+ > from gdm import GDM, GDMModel, DriveCollection, Session, select
27
+ > ```
28
+
29
+ **Documentation:** [yotammanor.github.io/gdm](https://yotammanor.github.io/gdm/)
30
+ · [docs source](https://github.com/yotammanor/gdm/tree/main/docs)
31
+ · **Design:** [`docs/design.md`](https://github.com/yotammanor/gdm/blob/main/docs/design.md)
32
+
33
+ ---
34
+
35
+ ## Why GDM?
36
+
37
+ | You have… | GDM gives you… |
38
+ | --- | --- |
39
+ | A launch folder with Docs, decks, and PNGs | `DriveCollection[T]` over direct children; MIME filters; lazy native content |
40
+ | A team spreadsheet with extra columns | Progressive sheet mapping—bind declared fields, preserve the rest |
41
+ | Scripts that need typed reads/writes | Pydantic validation on every boundary crossing |
42
+ | Multiple apps on one Google account | Root-scoped projects with isolated `.gdm/` metadata |
43
+
44
+ GDM is **honest about platform limits**: no cross-file transactions, no server-side joins, no row-level security. Drive permissions remain the security boundary.
45
+
46
+ ---
47
+
48
+ ## Installation
49
+
50
+ Install from PyPI:
51
+
52
+ ```console
53
+ pip install "google-drive-model[oauth]"
54
+ ```
55
+
56
+ To work from source:
57
+
58
+ ```console
59
+ git clone https://github.com/yotammanor/gdm.git
60
+ cd gdm
61
+ uv sync --all-groups --frozen
62
+ ```
63
+
64
+ | Extra | Purpose |
65
+ | --- | --- |
66
+ | `oauth` | Interactive installed-app login (`google-auth-oauthlib`) |
67
+ | `yaml` | YAML codec for opt-in structured file collections |
68
+
69
+ ---
70
+
71
+ ## Quickstart: adopt a Drive folder
72
+
73
+ ### 1. Authenticate and open a project
74
+
75
+ ```console
76
+ gdm auth login
77
+ gdm --project-dir . --apply init --root-id YOUR_ROOT_FOLDER_ID --name my-app --remote
78
+ ```
79
+
80
+ This creates the shared project manifest, registry, and collection metadata area
81
+ under the Drive root. Tokens and local schema/index state stay local.
82
+
83
+ ### 2. Define a collection model
84
+
85
+ ```python
86
+ from gdm import DriveObjectMixin, Field, GDMModel
87
+
88
+
89
+ class CampaignAsset(
90
+ GDMModel,
91
+ DriveObjectMixin,
92
+ storage="collection",
93
+ collection="Campaigns/Launch",
94
+ mapping="partial",
95
+ ):
96
+ campaign: str | None = Field(default=None, metadata="campaign")
97
+ approved: bool = Field(default=False, metadata="approved")
98
+ ```
99
+
100
+ `DriveObjectMixin` adds standard read-only Drive fields (`drive_id`, `name`, `mime_type`, …) mapped with `Field(drive=...)`.
101
+
102
+ ### 3. Connect, bind, and use the collection
103
+
104
+ ```python
105
+ from gdm import GDM, select
106
+
107
+
108
+ client = GDM.open(root_folder_id="YOUR_ROOT_FOLDER_ID")
109
+ client.bind_collection(
110
+ CampaignAsset,
111
+ path="Campaigns/Launch", # resolved once; immutable folder ID is persisted
112
+ )
113
+
114
+ assets = client.collection(CampaignAsset)
115
+ images = assets.where_mime("image/*")
116
+ logo = images.get("IMAGE_FILE_ID")
117
+ brief = assets.get("GOOGLE_DOC_FILE_ID")
118
+
119
+ data = logo.content.download() # explicit — metadata queries skip bodies
120
+ doc = brief.content.google_doc() # native Docs API handle
121
+ doc.batch_update([...])
122
+
123
+ with client.session() as session:
124
+ approved = session.exec(
125
+ select(CampaignAsset).where(CampaignAsset.approved == True) # noqa: E712
126
+ ).all()
127
+ ```
128
+
129
+ Collection operations are **explicit about safety**: `adopt` vs `move` vs `shortcut`, `detach` vs `trash`, and `delete_permanently` for irreversible removal. `session.delete()` trashes collection members; it never silently purges Drive files.
130
+
131
+ See the [Drive collections tutorial](https://yotammanor.github.io/gdm/tutorial/collection/) for duplicate names, shortcuts, recursion, and metadata reconciliation.
132
+
133
+ ---
134
+
135
+ ## Sheet tables (second story)
136
+
137
+ Bind a worksheet without rewriting your team's sheet:
138
+
139
+ ```python
140
+ from gdm import Field, GDMModel
141
+
142
+
143
+ class Task(GDMModel, storage="sheet", mapping="partial", source="Tasks/Q1"):
144
+ id: str | None = Field(default=None, primary_key=True)
145
+ title: str
146
+ status: str = "todo"
147
+ ```
148
+
149
+ ```python
150
+ from gdm import GDM, ResourceKind, select
151
+
152
+
153
+ client = GDM.open(root_folder_id="YOUR_ROOT_FOLDER_ID")
154
+ client.bind(
155
+ Task,
156
+ resource_id="YOUR_SPREADSHEET_ID",
157
+ kind=ResourceKind.SPREADSHEET,
158
+ worksheet="Tasks",
159
+ )
160
+ with client.session() as session:
161
+ open_tasks = session.exec(select(Task).where(Task.status == "open").limit(50)).all()
162
+ session.add(Task(title="Ship docs", status="open"))
163
+ session.commit()
164
+ ```
165
+
166
+ Foreign keys can reference **Drive file IDs** from collection models. Relationships resolve client-side with `session.prefetch()`—see [Relationships](https://yotammanor.github.io/gdm/storage/relationships/).
167
+
168
+ ---
169
+
170
+ ## CLI
171
+
172
+ | Command | Purpose |
173
+ | --- | --- |
174
+ | `gdm init` | Local `.gdm/` scaffolding; optional remote project with `--apply --remote` |
175
+ | `gdm explore` | Inventory a folder (metadata only—zero content reads) |
176
+ | `gdm infer collection` | Suggest a collection model from Drive metadata |
177
+ | `gdm infer file` | Infer from a local JSON/text/CSV sample |
178
+ | `gdm adopt` | Bind a folder (dry-run by default) |
179
+ | `gdm doctor` | Metadata reconciliation, drift, scale warnings |
180
+ | `gdm migrate plan/apply/rollback` | Forward, resumable migrations with declared rollback steps |
181
+ | `gdm index rebuild` | Rebuild optional sidecar indexes |
182
+ | `gdm auth login` | Interactive OAuth |
183
+
184
+ Terminal demo (**actual local CLI recording**, no live OAuth or Drive mutations):
185
+
186
+ <p align="center">
187
+ <a href="https://github.com/yotammanor/gdm/blob/main/docs/assets/demo.cast"><img src="https://raw.githubusercontent.com/yotammanor/gdm/main/docs/assets/demo.gif" alt="Recorded GDM CLI demo — open demo.cast to play" width="740"></a>
188
+ </p>
189
+
190
+ Regenerate the canonical cast (and the GIF when `agg` is installed):
191
+ `bash docs/assets/regenerate-demo.sh`
192
+
193
+ ---
194
+
195
+ ## Architecture at a glance
196
+
197
+ ```text
198
+ Your Pydantic models (GDMModel + Field)
199
+
200
+
201
+ GDM client ──► DriveCollection[T] (folder-native CRUD, lazy content)
202
+ │ │
203
+ │ └──► metadata Sheet under .gdm/collections/
204
+
205
+ Session / select() ──► expression tree (metadata queries; no body fetch)
206
+
207
+ ├──► Sheets backend (rows, batchUpdate)
208
+ └──► Drive backend (files, appProperties routing keys)
209
+
210
+
211
+ Drive .gdm/ metadata (manifest, registry, collection metadata)
212
+ Local .gdm/ tooling state (schema snapshots, migration checkpoints, indexes)
213
+ ```
214
+
215
+ **Key limitations** (by design):
216
+
217
+ - `Session.commit()` coordinates writes but is **not** an ACID transaction across files.
218
+ - Metadata queries never download native bodies; content access is explicit via `.content`.
219
+ - Relationships and joins are **client-side** with batched prefetch.
220
+ - Export produces portable views; native Docs/Slides mutation uses their respective APIs.
221
+ - Docs and Slides use atomic revision preconditions. Drive binary and Sheets
222
+ updates use best-effort conflict checks because those APIs expose no general
223
+ conditional write precondition.
224
+
225
+ Full rationale: [`docs/design.md`](https://github.com/yotammanor/gdm/blob/main/docs/design.md).
226
+
227
+ ---
228
+
229
+ ## Contributing
230
+
231
+ See [CONTRIBUTING.md](https://github.com/yotammanor/gdm/blob/main/CONTRIBUTING.md), [SECURITY.md](https://github.com/yotammanor/gdm/blob/main/SECURITY.md), and [CODE_OF_CONDUCT.md](https://github.com/yotammanor/gdm/blob/main/CODE_OF_CONDUCT.md).
232
+
233
+ ```console
234
+ uv run pytest
235
+ uv run ruff check .
236
+ uv run mkdocs build --strict
237
+ ```
238
+
239
+ ---
240
+
241
+ ## License
242
+
243
+ MIT © [Yotam Manor](https://github.com/yotammanor)
@@ -0,0 +1,164 @@
1
+ [project]
2
+ name = "google-drive-model"
3
+ version = "0.1.0"
4
+ description = "Pydantic models and a SQLModel-inspired API for Google Drive and Sheets"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "MIT"
8
+ keywords = [
9
+ "google-drive",
10
+ "google-sheets",
11
+ "orm",
12
+ "odm",
13
+ "pydantic",
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Framework :: Pydantic :: 2",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = [
28
+ "google-api-python-client>=2.160.0",
29
+ "google-auth>=2.37.0",
30
+ "google-auth-httplib2>=0.2.0",
31
+ "pydantic>=2.10.0",
32
+ "rich>=13.9.0",
33
+ "typer>=0.15.0",
34
+ ]
35
+
36
+ [[project.authors]]
37
+ name = "Yotam Manor"
38
+
39
+ [project.optional-dependencies]
40
+ oauth = ["google-auth-oauthlib>=1.2.1"]
41
+ yaml = ["pyyaml>=6.0.2"]
42
+
43
+ [project.scripts]
44
+ gdm = "gdm.cli:app"
45
+
46
+ [project.urls]
47
+ Changelog = "https://github.com/yotammanor/gdm/blob/main/CHANGELOG.md"
48
+ Documentation = "https://yotammanor.github.io/gdm/"
49
+ Issues = "https://github.com/yotammanor/gdm/issues"
50
+ Repository = "https://github.com/yotammanor/gdm"
51
+
52
+ [build-system]
53
+ requires = ["uv_build>=0.10.6,<0.11.0"]
54
+ build-backend = "uv_build"
55
+
56
+ [dependency-groups]
57
+ dev = [
58
+ "google-auth-oauthlib>=1.2.1",
59
+ "mkdocs-material>=9.6.0",
60
+ "mkdocstrings[python]>=0.28.0",
61
+ "pyyaml>=6.0.2",
62
+ "pytest>=8.3.0",
63
+ "pytest-cov>=6.0.0",
64
+ "pytest-mock>=3.14.0",
65
+ "ruff>=0.11.0",
66
+ "twine>=6.1.0",
67
+ "ty>=0.0.1a18",
68
+ ]
69
+
70
+ [tool.uv.build-backend]
71
+ module-name = "gdm"
72
+
73
+ [tool.pytest.ini_options]
74
+ addopts = [
75
+ "--strict-config",
76
+ "--strict-markers",
77
+ "-ra",
78
+ ]
79
+ testpaths = ["tests"]
80
+ markers = ["live: uses a real Google Drive account"]
81
+
82
+ [tool.coverage.run]
83
+ branch = true
84
+ source = ["gdm"]
85
+
86
+ [tool.coverage.report]
87
+ exclude_also = [
88
+ "if TYPE_CHECKING:",
89
+ "raise NotImplementedError",
90
+ ]
91
+ fail_under = 90
92
+ show_missing = true
93
+
94
+ [tool.ruff]
95
+ line-length = 88
96
+ target-version = "py311"
97
+
98
+ [tool.ruff.lint]
99
+ select = [
100
+ "A",
101
+ "ANN",
102
+ "B",
103
+ "C4",
104
+ "E",
105
+ "F",
106
+ "I",
107
+ "N",
108
+ "PIE",
109
+ "PT",
110
+ "RUF",
111
+ "SIM",
112
+ "UP",
113
+ ]
114
+ ignore = ["ANN401"]
115
+
116
+ [tool.ruff.lint.per-file-ignores]
117
+ "tests/**/*.py" = [
118
+ "ANN",
119
+ "SIM117",
120
+ ]
121
+ "tests/test_structured_codecs.py" = [
122
+ "ANN",
123
+ "SIM117",
124
+ ]
125
+ "tests/test_google_api_extra.py" = ["N802"]
126
+ "src/gdm/google_api.py" = [
127
+ "N802",
128
+ "N803",
129
+ "A002",
130
+ ]
131
+ "src/gdm/auth.py" = ["N806"]
132
+ "src/gdm/errors.py" = ["N818"]
133
+ "src/gdm/backends/drive.py" = ["N803"]
134
+ "src/gdm/backends/sheets.py" = [
135
+ "N803",
136
+ "A002",
137
+ "N802",
138
+ ]
139
+ "src/gdm/permissions.py" = ["N803"]
140
+ "src/gdm/sync.py" = [
141
+ "N803",
142
+ "N802",
143
+ ]
144
+ "src/gdm/project.py" = ["N803"]
145
+ "src/gdm/fields.py" = ["A002"]
146
+ "src/gdm/collection/metadata.py" = ["N803"]
147
+ "tests/collection_fakes.py" = [
148
+ "N802",
149
+ "N803",
150
+ "SIM102",
151
+ ]
152
+ "tests/fakes.py" = [
153
+ "N802",
154
+ "N803",
155
+ "A002",
156
+ "SIM102",
157
+ "E501",
158
+ ]
159
+
160
+ [tool.ruff.format]
161
+ docstring-code-format = true
162
+
163
+ [tool.ty.src]
164
+ include = ["src"]