ppbase 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 (100) hide show
  1. ppbase-0.1.0/PKG-INFO +20 -0
  2. ppbase-0.1.0/README.md +270 -0
  3. ppbase-0.1.0/ppbase/__init__.py +17 -0
  4. ppbase-0.1.0/ppbase/__main__.py +744 -0
  5. ppbase-0.1.0/ppbase/admin/dist/assets/_id_-DZJDi57q.js +65 -0
  6. ppbase-0.1.0/ppbase/admin/dist/assets/arrow-right-CFjpsotf.js +6 -0
  7. ppbase-0.1.0/ppbase/admin/dist/assets/badge-_xdB2pxd.js +1 -0
  8. ppbase-0.1.0/ppbase/admin/dist/assets/breadcrumb-BaA-PKls.js +1 -0
  9. ppbase-0.1.0/ppbase/admin/dist/assets/button-cOPHmTk3.js +1 -0
  10. ppbase-0.1.0/ppbase/admin/dist/assets/check-Bg_MTf5r.js +6 -0
  11. ppbase-0.1.0/ppbase/admin/dist/assets/checkbox-2bYeilqt.js +1 -0
  12. ppbase-0.1.0/ppbase/admin/dist/assets/chevron-left-iXTKBRUV.js +6 -0
  13. ppbase-0.1.0/ppbase/admin/dist/assets/circle-alert-C7lepEss.js +6 -0
  14. ppbase-0.1.0/ppbase/admin/dist/assets/collection-editor-7ZuVyhTs.js +19 -0
  15. ppbase-0.1.0/ppbase/admin/dist/assets/dashboard-DqfGINoH.js +16 -0
  16. ppbase-0.1.0/ppbase/admin/dist/assets/empty-state-BzTkdxCW.js +1 -0
  17. ppbase-0.1.0/ppbase/admin/dist/assets/index-BMDse7c9.js +1 -0
  18. ppbase-0.1.0/ppbase/admin/dist/assets/index-DhFFCX27.js +1 -0
  19. ppbase-0.1.0/ppbase/admin/dist/assets/index-hkEk4VxW.js +177 -0
  20. ppbase-0.1.0/ppbase/admin/dist/assets/index-rtXfCyLG.css +1 -0
  21. ppbase-0.1.0/ppbase/admin/dist/assets/input-DnqvRCla.js +1 -0
  22. ppbase-0.1.0/ppbase/admin/dist/assets/label--pbF4JwP.js +1 -0
  23. ppbase-0.1.0/ppbase/admin/dist/assets/layout-aHUI7-5O.js +26 -0
  24. ppbase-0.1.0/ppbase/admin/dist/assets/loading-spinner-C8DEbd0p.js +6 -0
  25. ppbase-0.1.0/ppbase/admin/dist/assets/login-WgIq_Ce2.js +1 -0
  26. ppbase-0.1.0/ppbase/admin/dist/assets/logs-BFWqXhLR.js +26 -0
  27. ppbase-0.1.0/ppbase/admin/dist/assets/migrations-pd8sJONp.js +6 -0
  28. ppbase-0.1.0/ppbase/admin/dist/assets/refresh-cw-CZn6LysZ.js +6 -0
  29. ppbase-0.1.0/ppbase/admin/dist/assets/select-DKsAFmxr.js +11 -0
  30. ppbase-0.1.0/ppbase/admin/dist/assets/settings-D35l31hS.js +1 -0
  31. ppbase-0.1.0/ppbase/admin/dist/assets/setup-B_Q4D5QZ.js +1 -0
  32. ppbase-0.1.0/ppbase/admin/dist/assets/sheet-C__RuiiA.js +1 -0
  33. ppbase-0.1.0/ppbase/admin/dist/assets/table-2-D8RuQ792.js +6 -0
  34. ppbase-0.1.0/ppbase/admin/dist/assets/use-logs-_ol7ma3T.js +1 -0
  35. ppbase-0.1.0/ppbase/admin/dist/assets/use-migrations-CAqT4Y4N.js +1 -0
  36. ppbase-0.1.0/ppbase/admin/dist/assets/use-settings-DMhcnSJU.js +1 -0
  37. ppbase-0.1.0/ppbase/admin/dist/fonts/Inter-Variable.woff2 +0 -0
  38. ppbase-0.1.0/ppbase/admin/dist/index.html +13 -0
  39. ppbase-0.1.0/ppbase/api/__init__.py +1 -0
  40. ppbase-0.1.0/ppbase/api/admins.py +349 -0
  41. ppbase-0.1.0/ppbase/api/collections.py +496 -0
  42. ppbase-0.1.0/ppbase/api/deps.py +246 -0
  43. ppbase-0.1.0/ppbase/api/files.py +647 -0
  44. ppbase-0.1.0/ppbase/api/health.py +13 -0
  45. ppbase-0.1.0/ppbase/api/logs.py +190 -0
  46. ppbase-0.1.0/ppbase/api/migrations.py +265 -0
  47. ppbase-0.1.0/ppbase/api/realtime.py +243 -0
  48. ppbase-0.1.0/ppbase/api/record_auth.py +1469 -0
  49. ppbase-0.1.0/ppbase/api/records.py +1587 -0
  50. ppbase-0.1.0/ppbase/api/router.py +90 -0
  51. ppbase-0.1.0/ppbase/api/settings.py +371 -0
  52. ppbase-0.1.0/ppbase/app.py +356 -0
  53. ppbase-0.1.0/ppbase/config.py +97 -0
  54. ppbase-0.1.0/ppbase/core/__init__.py +5 -0
  55. ppbase-0.1.0/ppbase/core/id_generator.py +25 -0
  56. ppbase-0.1.0/ppbase/db/__init__.py +5 -0
  57. ppbase-0.1.0/ppbase/db/bootstrap.py +399 -0
  58. ppbase-0.1.0/ppbase/db/engine.py +87 -0
  59. ppbase-0.1.0/ppbase/db/schema_manager.py +497 -0
  60. ppbase-0.1.0/ppbase/db/system_tables.py +266 -0
  61. ppbase-0.1.0/ppbase/ext/__init__.py +39 -0
  62. ppbase-0.1.0/ppbase/ext/events.py +501 -0
  63. ppbase-0.1.0/ppbase/ext/flask_like_pb.py +990 -0
  64. ppbase-0.1.0/ppbase/ext/hooks.py +137 -0
  65. ppbase-0.1.0/ppbase/ext/loading.py +47 -0
  66. ppbase-0.1.0/ppbase/ext/record_repository.py +102 -0
  67. ppbase-0.1.0/ppbase/ext/registry.py +334 -0
  68. ppbase-0.1.0/ppbase/middleware/__init__.py +1 -0
  69. ppbase-0.1.0/ppbase/middleware/auth.py +6 -0
  70. ppbase-0.1.0/ppbase/middleware/cors.py +23 -0
  71. ppbase-0.1.0/ppbase/middleware/request_logger.py +84 -0
  72. ppbase-0.1.0/ppbase/models/__init__.py +5 -0
  73. ppbase-0.1.0/ppbase/models/collection.py +237 -0
  74. ppbase-0.1.0/ppbase/models/field_types.py +622 -0
  75. ppbase-0.1.0/ppbase/models/record.py +227 -0
  76. ppbase-0.1.0/ppbase/services/__init__.py +1 -0
  77. ppbase-0.1.0/ppbase/services/admin_service.py +189 -0
  78. ppbase-0.1.0/ppbase/services/auth_service.py +281 -0
  79. ppbase-0.1.0/ppbase/services/collection_service.py +677 -0
  80. ppbase-0.1.0/ppbase/services/expand_service.py +277 -0
  81. ppbase-0.1.0/ppbase/services/file_storage.py +144 -0
  82. ppbase-0.1.0/ppbase/services/filter_parser.py +1111 -0
  83. ppbase-0.1.0/ppbase/services/mail_service.py +142 -0
  84. ppbase-0.1.0/ppbase/services/migration_generator.py +441 -0
  85. ppbase-0.1.0/ppbase/services/migration_runner.py +360 -0
  86. ppbase-0.1.0/ppbase/services/oauth2_service.py +550 -0
  87. ppbase-0.1.0/ppbase/services/realtime_service.py +665 -0
  88. ppbase-0.1.0/ppbase/services/record_auth_service.py +877 -0
  89. ppbase-0.1.0/ppbase/services/record_service.py +1110 -0
  90. ppbase-0.1.0/ppbase/services/rule_engine.py +79 -0
  91. ppbase-0.1.0/ppbase/storage/__init__.py +1 -0
  92. ppbase-0.1.0/ppbase.egg-info/PKG-INFO +20 -0
  93. ppbase-0.1.0/ppbase.egg-info/SOURCES.txt +98 -0
  94. ppbase-0.1.0/ppbase.egg-info/dependency_links.txt +1 -0
  95. ppbase-0.1.0/ppbase.egg-info/entry_points.txt +2 -0
  96. ppbase-0.1.0/ppbase.egg-info/requires.txt +17 -0
  97. ppbase-0.1.0/ppbase.egg-info/top_level.txt +1 -0
  98. ppbase-0.1.0/pyproject.toml +47 -0
  99. ppbase-0.1.0/setup.cfg +4 -0
  100. ppbase-0.1.0/tests/test_deps_optional_auth.py +56 -0
ppbase-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: ppbase
3
+ Version: 0.1.0
4
+ Requires-Python: >=3.11
5
+ Requires-Dist: fastapi>=0.115.0
6
+ Requires-Dist: uvicorn[standard]>=0.32.0
7
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.36
8
+ Requires-Dist: asyncpg>=0.30.0
9
+ Requires-Dist: pydantic>=2.9.0
10
+ Requires-Dist: pydantic-settings>=2.6.0
11
+ Requires-Dist: pyjwt>=2.9.0
12
+ Requires-Dist: passlib[bcrypt]>=1.7.4
13
+ Requires-Dist: lark>=1.2.0
14
+ Requires-Dist: python-multipart>=0.0.12
15
+ Requires-Dist: aiofiles>=24.1.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=8.0; extra == "dev"
18
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
19
+ Requires-Dist: httpx>=0.27.0; extra == "dev"
20
+ Requires-Dist: testcontainers[postgres]>=4.0; extra == "dev"
ppbase-0.1.0/README.md ADDED
@@ -0,0 +1,270 @@
1
+ # PPBase
2
+
3
+ A Python reimplementation of [PocketBase](https://pocketbase.io/) backed by PostgreSQL.
4
+
5
+ PPBase gives you an instant REST API with dynamic collections, admin authentication, a built-in admin dashboard, and PocketBase-compatible endpoints -- all running on PostgreSQL instead of SQLite.
6
+
7
+ ## Quick Start
8
+
9
+ ### 1. Prerequisites
10
+
11
+ - Python 3.11+
12
+ - Docker (for PostgreSQL)
13
+
14
+ ### 2. Install
15
+
16
+ ```bash
17
+ git clone <repo-url> ppbase && cd ppbase
18
+ python3 -m venv .venv
19
+ source .venv/bin/activate
20
+ pip install -e ".[dev]"
21
+ ```
22
+
23
+ ### 3. Start PostgreSQL
24
+
25
+ ```bash
26
+ python -m ppbase db start
27
+ ```
28
+
29
+ This creates a Docker container (`ppbase-pg`) running PostgreSQL 17 on port 5433.
30
+
31
+ ### 4. Start the server
32
+
33
+ ```bash
34
+ python -m ppbase serve
35
+ ```
36
+
37
+ The server starts at **http://localhost:8090**. Admin UI is at **http://localhost:8090/_/**.
38
+
39
+ ### 5. Create an admin account
40
+
41
+ ```bash
42
+ python -m ppbase create-admin --email admin@example.com --password yourpassword
43
+ ```
44
+
45
+ ## CLI Reference
46
+
47
+ ```bash
48
+ # Server
49
+ python -m ppbase serve # foreground
50
+ python -m ppbase serve -d # daemon (background)
51
+ python -m ppbase stop # stop daemon
52
+ python -m ppbase restart # restart daemon
53
+ python -m ppbase status # check if running
54
+
55
+ # Database
56
+ python -m ppbase db start # start PostgreSQL container
57
+ python -m ppbase db stop # stop container
58
+ python -m ppbase db restart # restart container
59
+ python -m ppbase db status # check container status
60
+
61
+ # Admin
62
+ python -m ppbase create-admin --email <email> --password <pass>
63
+ ```
64
+
65
+ A shell script (`ppctl.sh`) is also available:
66
+
67
+ ```bash
68
+ ./ppctl.sh start | stop | restart | status
69
+ ./ppctl.sh db-start | db-stop | db-restart | db-status
70
+ ```
71
+
72
+ ## API Endpoints
73
+
74
+ PPBase implements the PocketBase REST API. All endpoints are under `/api/`.
75
+
76
+ | Method | Endpoint | Description |
77
+ |--------|----------|-------------|
78
+ | `POST` | `/api/admins/auth-with-password` | Admin login |
79
+ | `GET` | `/api/collections` | List collections |
80
+ | `POST` | `/api/collections` | Create collection |
81
+ | `GET` | `/api/collections/:id` | Get collection |
82
+ | `PATCH` | `/api/collections/:id` | Update collection |
83
+ | `DELETE` | `/api/collections/:id` | Delete collection |
84
+ | `GET` | `/api/collections/:col/records` | List records |
85
+ | `POST` | `/api/collections/:col/records` | Create record |
86
+ | `GET` | `/api/collections/:col/records/:id` | Get record |
87
+ | `PATCH` | `/api/collections/:col/records/:id` | Update record |
88
+ | `DELETE` | `/api/collections/:col/records/:id` | Delete record |
89
+ | `GET` | `/api/health` | Health check |
90
+ | `GET` | `/api/settings` | Get settings |
91
+ | `PATCH` | `/api/settings` | Update settings |
92
+
93
+ ### Filtering & Sorting
94
+
95
+ Records support PocketBase filter syntax:
96
+
97
+ ```
98
+ GET /api/collections/posts/records?filter=title~"hello" && views>5&sort=-created&page=1&perPage=20
99
+ ```
100
+
101
+ ### Expand Relations
102
+
103
+ ```
104
+ GET /api/collections/posts/records?expand=author,category
105
+ ```
106
+
107
+ ## Collection Types
108
+
109
+ - **Base** -- Standard data collections with custom fields
110
+ - **Auth** -- Collections with built-in email/password authentication fields
111
+ - **View** -- Read-only collections backed by a SQL SELECT query
112
+
113
+ ## Field Types
114
+
115
+ PPBase supports 14 field types, each mapped to a physical PostgreSQL column:
116
+
117
+ | Type | PostgreSQL | Notes |
118
+ |------|-----------|-------|
119
+ | `text` | `TEXT` | Min/max length, regex pattern |
120
+ | `number` | `DOUBLE PRECISION` / `INTEGER` | Min/max, integer-only option |
121
+ | `bool` | `BOOLEAN` | |
122
+ | `email` | `VARCHAR(255)` | Domain allowlist/blocklist |
123
+ | `url` | `TEXT` | Domain allowlist/blocklist |
124
+ | `date` | `TIMESTAMPTZ` | Min/max date |
125
+ | `select` | `TEXT` / `TEXT[]` | Predefined values, single or multi |
126
+ | `file` | `TEXT` / `TEXT[]` | Max size, MIME types, single or multi |
127
+ | `relation` | `VARCHAR(15)` / `VARCHAR(15)[]` | Links to another collection |
128
+ | `json` | `JSONB` | |
129
+ | `editor` | `TEXT` | Rich text / HTML |
130
+ | `autodate` | `TIMESTAMPTZ` | Auto-set on create/update |
131
+ | `password` | `TEXT` | Stored as bcrypt hash |
132
+ | `geo_point` | `JSONB` | `{lon, lat}` |
133
+
134
+ ## Admin Dashboard
135
+
136
+ The built-in admin UI at `/_/` provides:
137
+
138
+ - Collection management (create, edit, delete)
139
+ - Schema editor with full field options
140
+ - Record CRUD with field-type-aware forms
141
+ - SQL editor with syntax highlighting and autocomplete for view collections
142
+ - View collection support (read-only SQL queries)
143
+
144
+ ## Architecture
145
+
146
+ ```
147
+ HTTP Request -> FastAPI
148
+ -> api/router.py -> api/{endpoint}.py
149
+ -> services/{service}.py (business logic)
150
+ -> db/system_tables.py (ORM for _collections, _admins)
151
+ -> db/schema_manager.py (DDL for dynamic tables)
152
+ -> sqlalchemy.text() (parameterized SQL for records)
153
+ ```
154
+
155
+ - **Hybrid SQLAlchemy**: ORM for system tables, Core for dynamic collection tables
156
+ - **Physical columns**: Each field type maps to a real PostgreSQL column (not JSONB)
157
+ - **Filter parser**: Lark EBNF grammar translates PocketBase filter syntax to parameterized SQL
158
+ - **No migration system**: `_collections` table is the source of truth; DDL is applied directly
159
+
160
+ ## Configuration
161
+
162
+ All settings use the `PPBASE_` environment variable prefix:
163
+
164
+ | Variable | Default | Description |
165
+ |----------|---------|-------------|
166
+ | `PPBASE_DATABASE_URL` | `postgresql+asyncpg://ppbase:ppbase@localhost:5433/ppbase` | PostgreSQL connection |
167
+ | `PPBASE_PORT` | `8090` | Server port |
168
+ | `PPBASE_HOST` | `0.0.0.0` | Bind address |
169
+
170
+ ## Development
171
+
172
+ ```bash
173
+ # Install with dev dependencies
174
+ pip install -e ".[dev]"
175
+
176
+ # Run tests
177
+ pytest tests/ -v
178
+
179
+ # Run a specific test
180
+ pytest tests/test_specific.py::test_name -v
181
+ ```
182
+
183
+ ## Flask-like Extension API
184
+
185
+ PPBase can be used as a Python-extensible app with global decorators.
186
+
187
+ ### Single-file usage
188
+
189
+ ```python
190
+ from ppbase import pb
191
+
192
+
193
+ @pb.get("/hello/{name}")
194
+ async def hello(name: str):
195
+ return {"message": f"Hello {name}"}
196
+
197
+
198
+ @pb.on_record_create_request("posts")
199
+ async def normalize_post(e):
200
+ if "title" in e.data and "slug" not in e.data:
201
+ e.data["slug"] = str(e.data["title"]).strip().lower().replace(" ", "-")
202
+ return await e.next()
203
+
204
+
205
+ pb.start(host="127.0.0.1", port=8090)
206
+ ```
207
+
208
+ ### Access records and current user in custom code
209
+
210
+ Use repository-style helpers in routes and hooks:
211
+
212
+ ```python
213
+ from ppbase import pb
214
+
215
+ @pb.get("/api/me")
216
+ async def me(auth: dict = pb.require_record_auth()):
217
+ user = await pb.records(auth["collectionName"]).get(auth["id"])
218
+ return {"user": user}
219
+
220
+
221
+ @pb.on_record_update_request("users")
222
+ async def before_user_update(e):
223
+ e.require_auth_record() # raises 401/403 with PocketBase-style body
224
+ e.require_same_auth_record(e.record_id or "")
225
+ if not e.is_superuser():
226
+ e.data.setdefault("updatedByHook", True)
227
+ current = await e.get_current_user(fields="id,email")
228
+ if current:
229
+ e.data.setdefault("updatedBy", current["id"]) # mutate payload before default handler
230
+ return await e.next()
231
+ ```
232
+
233
+ ### Multi-file usage
234
+
235
+ - Side-effects style: import modules that register decorators on `pb`.
236
+ - Register function style: expose `register(pb)` and call it manually.
237
+
238
+ ```python
239
+ from ppbase import pb
240
+ import my_hooks_side_effects
241
+ from my_hooks_register import register
242
+
243
+ register(pb)
244
+ pb.start()
245
+ ```
246
+
247
+ ### CLI hooks loading
248
+
249
+ You can load hook modules when starting the server:
250
+
251
+ ```bash
252
+ python -m ppbase serve --hooks myapp.hooks:register
253
+ python -m ppbase serve --hooks myapp.hooks:register --hooks myapp.more_hooks:setup
254
+ ```
255
+
256
+ The hook target format is strict: `module:function`. The function receives `pb`.
257
+
258
+ ## Tech Stack
259
+
260
+ - **FastAPI** -- async web framework
261
+ - **SQLAlchemy 2.0** -- async ORM + Core
262
+ - **asyncpg** -- PostgreSQL async driver
263
+ - **Pydantic** -- request/response validation
264
+ - **PyJWT** -- admin authentication tokens
265
+ - **Lark** -- PocketBase filter syntax parser
266
+ - **PostgreSQL 17** -- database (via Docker)
267
+
268
+ ## License
269
+
270
+ MIT
@@ -0,0 +1,17 @@
1
+ """PPBase -- a Python reimplementation of PocketBase using PostgreSQL."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ from ppbase.ext.flask_like_pb import FlaskLikePB
8
+
9
+
10
+ class PPBase(FlaskLikePB):
11
+ """Main PPBase facade."""
12
+
13
+
14
+ # Process-wide Flask-like singleton facade.
15
+ pb = PPBase()
16
+
17
+ __all__ = ["PPBase", "pb", "__version__"]