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.
- google_drive_model-0.1.0/PKG-INFO +277 -0
- google_drive_model-0.1.0/README.md +243 -0
- google_drive_model-0.1.0/pyproject.toml +164 -0
- google_drive_model-0.1.0/pyproject.toml.orig +141 -0
- google_drive_model-0.1.0/src/gdm/__init__.py +77 -0
- google_drive_model-0.1.0/src/gdm/auth.py +514 -0
- google_drive_model-0.1.0/src/gdm/backends/__init__.py +15 -0
- google_drive_model-0.1.0/src/gdm/backends/base.py +115 -0
- google_drive_model-0.1.0/src/gdm/backends/drive.py +716 -0
- google_drive_model-0.1.0/src/gdm/backends/memory.py +448 -0
- google_drive_model-0.1.0/src/gdm/backends/registry.py +1133 -0
- google_drive_model-0.1.0/src/gdm/backends/sheets.py +644 -0
- google_drive_model-0.1.0/src/gdm/cli.py +1108 -0
- google_drive_model-0.1.0/src/gdm/client.py +383 -0
- google_drive_model-0.1.0/src/gdm/codecs.py +246 -0
- google_drive_model-0.1.0/src/gdm/collection/__init__.py +23 -0
- google_drive_model-0.1.0/src/gdm/collection/backend.py +515 -0
- google_drive_model-0.1.0/src/gdm/collection/collection.py +350 -0
- google_drive_model-0.1.0/src/gdm/collection/constants.py +59 -0
- google_drive_model-0.1.0/src/gdm/collection/content.py +496 -0
- google_drive_model-0.1.0/src/gdm/collection/inventory.py +285 -0
- google_drive_model-0.1.0/src/gdm/collection/metadata.py +780 -0
- google_drive_model-0.1.0/src/gdm/errors.py +136 -0
- google_drive_model-0.1.0/src/gdm/explore.py +489 -0
- google_drive_model-0.1.0/src/gdm/explore_types.py +64 -0
- google_drive_model-0.1.0/src/gdm/fields.py +166 -0
- google_drive_model-0.1.0/src/gdm/google_api.py +631 -0
- google_drive_model-0.1.0/src/gdm/indexes.py +735 -0
- google_drive_model-0.1.0/src/gdm/infer.py +644 -0
- google_drive_model-0.1.0/src/gdm/migration_target.py +842 -0
- google_drive_model-0.1.0/src/gdm/migrations.py +1774 -0
- google_drive_model-0.1.0/src/gdm/model.py +468 -0
- google_drive_model-0.1.0/src/gdm/permissions.py +278 -0
- google_drive_model-0.1.0/src/gdm/project.py +738 -0
- google_drive_model-0.1.0/src/gdm/py.typed +1 -0
- google_drive_model-0.1.0/src/gdm/query.py +366 -0
- google_drive_model-0.1.0/src/gdm/retry.py +145 -0
- google_drive_model-0.1.0/src/gdm/schema.py +850 -0
- google_drive_model-0.1.0/src/gdm/session.py +623 -0
- google_drive_model-0.1.0/src/gdm/sync.py +236 -0
- google_drive_model-0.1.0/src/gdm/testing/__init__.py +45 -0
- google_drive_model-0.1.0/src/gdm/tooling/__init__.py +3 -0
- google_drive_model-0.1.0/src/gdm/tooling/adopt.py +100 -0
- google_drive_model-0.1.0/src/gdm/tooling/collection_probe.py +170 -0
- google_drive_model-0.1.0/src/gdm/tooling/doctor.py +267 -0
- 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"]
|