ohmyapi 0.1.6__tar.gz → 0.1.7__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.
- ohmyapi-0.1.7/PKG-INFO +310 -0
- ohmyapi-0.1.7/README.md +277 -0
- ohmyapi-0.1.7/pyproject.toml +38 -0
- ohmyapi-0.1.7/src/ohmyapi/__main__.py +3 -0
- {ohmyapi-0.1.6 → ohmyapi-0.1.7}/src/ohmyapi/cli.py +0 -3
- ohmyapi-0.1.7/src/ohmyapi/core/templates/project/README.md.j2 +2 -0
- ohmyapi-0.1.7/src/ohmyapi/core/templates/project/pyproject.toml.j2 +37 -0
- ohmyapi-0.1.7/src/ohmyapi/db/__init__.py +3 -0
- ohmyapi-0.1.7/src/ohmyapi/db/exceptions.py +2 -0
- ohmyapi-0.1.7/src/ohmyapi/db/model/__init__.py +1 -0
- {ohmyapi-0.1.6 → ohmyapi-0.1.7}/src/ohmyapi/db/model/model.py +1 -1
- ohmyapi-0.1.7/src/ohmyapi/router.py +2 -0
- ohmyapi-0.1.6/PKG-INFO +0 -204
- ohmyapi-0.1.6/README.md +0 -177
- ohmyapi-0.1.6/pyproject.toml +0 -36
- ohmyapi-0.1.6/src/ohmyapi/core/templates/project/pyproject.toml.j2 +0 -13
- ohmyapi-0.1.6/src/ohmyapi/db/__init__.py +0 -3
- ohmyapi-0.1.6/src/ohmyapi/db/model/__init__.py +0 -1
- ohmyapi-0.1.6/src/ohmyapi/router.py +0 -2
- {ohmyapi-0.1.6 → ohmyapi-0.1.7}/src/ohmyapi/__init__.py +0 -0
- {ohmyapi-0.1.6 → ohmyapi-0.1.7}/src/ohmyapi/builtin/auth/__init__.py +0 -0
- {ohmyapi-0.1.6 → ohmyapi-0.1.7}/src/ohmyapi/builtin/auth/models.py +0 -0
- {ohmyapi-0.1.6 → ohmyapi-0.1.7}/src/ohmyapi/builtin/auth/permissions.py +0 -0
- {ohmyapi-0.1.6 → ohmyapi-0.1.7}/src/ohmyapi/builtin/auth/routes.py +0 -0
- {ohmyapi-0.1.6 → ohmyapi-0.1.7}/src/ohmyapi/core/__init__.py +0 -0
- {ohmyapi-0.1.6 → ohmyapi-0.1.7}/src/ohmyapi/core/runtime.py +0 -0
- {ohmyapi-0.1.6 → ohmyapi-0.1.7}/src/ohmyapi/core/scaffolding.py +0 -0
- {ohmyapi-0.1.6 → ohmyapi-0.1.7}/src/ohmyapi/core/templates/app/__init__.py.j2 +0 -0
- {ohmyapi-0.1.6 → ohmyapi-0.1.7}/src/ohmyapi/core/templates/app/models.py.j2 +0 -0
- {ohmyapi-0.1.6 → ohmyapi-0.1.7}/src/ohmyapi/core/templates/app/routes.py.j2 +0 -0
- {ohmyapi-0.1.6 → ohmyapi-0.1.7}/src/ohmyapi/core/templates/project/settings.py.j2 +0 -0
ohmyapi-0.1.7/PKG-INFO
ADDED
@@ -0,0 +1,310 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: ohmyapi
|
3
|
+
Version: 0.1.7
|
4
|
+
Summary: A Django-like but async web-framework based on FastAPI and TortoiseORM.
|
5
|
+
License-Expression: MIT
|
6
|
+
Keywords: fastapi,tortoise,orm,async,web-framework
|
7
|
+
Author: Brian Wiborg
|
8
|
+
Author-email: me@brianwib.org
|
9
|
+
Requires-Python: >=3.13
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
13
|
+
Provides-Extra: auth
|
14
|
+
Requires-Dist: aerich (>=0.9.1,<0.10.0)
|
15
|
+
Requires-Dist: argon2-cffi (>=25.1.0,<26.0.0)
|
16
|
+
Requires-Dist: argon2-cffi ; extra == "auth"
|
17
|
+
Requires-Dist: crypto (>=1.4.1,<2.0.0)
|
18
|
+
Requires-Dist: crypto ; extra == "auth"
|
19
|
+
Requires-Dist: fastapi (>=0.117.1,<0.118.0)
|
20
|
+
Requires-Dist: ipython (>=9.5.0,<10.0.0)
|
21
|
+
Requires-Dist: jinja2 (>=3.1.6,<4.0.0)
|
22
|
+
Requires-Dist: passlib (>=1.7.4,<2.0.0)
|
23
|
+
Requires-Dist: passlib ; extra == "auth"
|
24
|
+
Requires-Dist: pyjwt (>=2.10.1,<3.0.0)
|
25
|
+
Requires-Dist: pyjwt ; extra == "auth"
|
26
|
+
Requires-Dist: python-multipart (>=0.0.20,<0.0.21)
|
27
|
+
Requires-Dist: python-multipart ; extra == "auth"
|
28
|
+
Requires-Dist: tortoise-orm (>=0.25.1,<0.26.0)
|
29
|
+
Requires-Dist: typer (>=0.19.1,<0.20.0)
|
30
|
+
Requires-Dist: uvicorn (>=0.36.0,<0.37.0)
|
31
|
+
Description-Content-Type: text/markdown
|
32
|
+
|
33
|
+
# OhMyAPI
|
34
|
+
|
35
|
+
> Think: Micro-Django, but API-first, less clunky and 100% async.
|
36
|
+
|
37
|
+
OhMyAPI is a Django-flavored web-application scaffolding framework and management layer.
|
38
|
+
Built around FastAPI and TortoiseORM, it is 100% async.
|
39
|
+
|
40
|
+
It is ***blazingly fast***, ***fun*** to use and comes with ***batteries included***!
|
41
|
+
|
42
|
+
**Features**
|
43
|
+
|
44
|
+
- Django-like project-layout and -structure
|
45
|
+
- Django-like prject-level settings.py
|
46
|
+
- Django-like models via TortoiseORM
|
47
|
+
- Django-like `Model.Meta` class for model configuration
|
48
|
+
- Easily convert your query results to `pydantic` models via `Model.Schema`
|
49
|
+
- Django-like migrations (makemigrations & migrate) via Aerich
|
50
|
+
- Django-like CLI tooling (`startproject`, `startapp`, `shell`, `serve`, etc)
|
51
|
+
- Various optional builtin apps you can hook into your project
|
52
|
+
- Highly configurable and customizable
|
53
|
+
- 100% async
|
54
|
+
|
55
|
+
---
|
56
|
+
|
57
|
+
## Getting started
|
58
|
+
|
59
|
+
**Creating a Project**
|
60
|
+
|
61
|
+
```
|
62
|
+
pip install ohmyapi
|
63
|
+
ohmyapi startproject myproject
|
64
|
+
cd myproject
|
65
|
+
```
|
66
|
+
|
67
|
+
This will create the following directory structure:
|
68
|
+
|
69
|
+
```
|
70
|
+
myproject/
|
71
|
+
- pyproject.toml
|
72
|
+
- README.md
|
73
|
+
- settings.py
|
74
|
+
```
|
75
|
+
|
76
|
+
Run your project with:
|
77
|
+
|
78
|
+
```
|
79
|
+
ohmyapi serve
|
80
|
+
```
|
81
|
+
|
82
|
+
In your browser go to:
|
83
|
+
- http://localhost:8000/docs
|
84
|
+
|
85
|
+
**Creating an App**
|
86
|
+
|
87
|
+
Create a new app by:
|
88
|
+
|
89
|
+
```
|
90
|
+
ohmyapi startapp tournament
|
91
|
+
```
|
92
|
+
|
93
|
+
This will create the following directory structure:
|
94
|
+
|
95
|
+
```
|
96
|
+
myproject/
|
97
|
+
- tournament/
|
98
|
+
- __init__.py
|
99
|
+
- models.py
|
100
|
+
- routes.py
|
101
|
+
- pyproject.toml
|
102
|
+
- README.md
|
103
|
+
- settings.py
|
104
|
+
```
|
105
|
+
|
106
|
+
Add 'tournament' to your `INSTALLED_APPS` in `settings.py`.
|
107
|
+
|
108
|
+
### Models
|
109
|
+
|
110
|
+
Write your first model in `turnament/models.py`:
|
111
|
+
|
112
|
+
```python
|
113
|
+
from ohmyapi.db import Model, field
|
114
|
+
|
115
|
+
|
116
|
+
class Tournament(Model):
|
117
|
+
id = field.IntField(primary_key=True)
|
118
|
+
name = field.TextField()
|
119
|
+
created = field.DatetimeField(auto_now_add=True)
|
120
|
+
|
121
|
+
def __str__(self):
|
122
|
+
return self.name
|
123
|
+
|
124
|
+
|
125
|
+
class Event(Model):
|
126
|
+
id = field.IntField(primary_key=True)
|
127
|
+
name = field.TextField()
|
128
|
+
tournament = field.ForeignKeyField('tournament.Tournament', related_name='events')
|
129
|
+
participants = field.ManyToManyField('torunament.Team', related_name='events', through='event_team')
|
130
|
+
modified = field.DatetimeField(auto_now=True)
|
131
|
+
prize = field.DecimalField(max_digits=10, decimal_places=2, null=True)
|
132
|
+
|
133
|
+
def __str__(self):
|
134
|
+
return self.name
|
135
|
+
|
136
|
+
|
137
|
+
class Team(Model):
|
138
|
+
id = field.IntField(primary_key=True)
|
139
|
+
name = field.TextField()
|
140
|
+
|
141
|
+
def __str__(self):
|
142
|
+
return self.name
|
143
|
+
```
|
144
|
+
|
145
|
+
### API Routes
|
146
|
+
|
147
|
+
Next, create your endpoints in `tournament/routes.py`:
|
148
|
+
|
149
|
+
```python
|
150
|
+
from ohmyapi.router import APIRouter, HTTPException
|
151
|
+
from ohmyapi.db.exceptions import DoesNotExist
|
152
|
+
|
153
|
+
from .models import Tournament
|
154
|
+
|
155
|
+
router = APIRouter(prefix="/tournament")
|
156
|
+
|
157
|
+
|
158
|
+
@router.get("/")
|
159
|
+
async def list():
|
160
|
+
queryset = Tournament.all()
|
161
|
+
return await Tournament.Schema.many.from_queryset(queryset)
|
162
|
+
|
163
|
+
|
164
|
+
@router.get("/:id")
|
165
|
+
async def get(id: int):
|
166
|
+
try:
|
167
|
+
queryset = Tournament.get(pk=id)
|
168
|
+
return await Tournament.Schema.one(queryset)
|
169
|
+
except DoesNotExist:
|
170
|
+
raise HTTPException(status_code=404, detail="item not found")
|
171
|
+
|
172
|
+
...
|
173
|
+
```
|
174
|
+
|
175
|
+
## Migrations
|
176
|
+
|
177
|
+
Before we can run the app, we need to create and initialize the database.
|
178
|
+
|
179
|
+
Similar to Django, first run:
|
180
|
+
|
181
|
+
```
|
182
|
+
ohmyapi makemigrations [ <app> ] # no app means all INSTALLED_APPS
|
183
|
+
```
|
184
|
+
|
185
|
+
This will create a `migrations/` folder in you project root.
|
186
|
+
|
187
|
+
```
|
188
|
+
myproject/
|
189
|
+
- tournament/
|
190
|
+
- __init__.py
|
191
|
+
- models.py
|
192
|
+
- routes.py
|
193
|
+
- migrations/
|
194
|
+
- tournament/
|
195
|
+
- pyproject.toml
|
196
|
+
- README.md
|
197
|
+
- settings.py
|
198
|
+
```
|
199
|
+
|
200
|
+
Apply your migrations via:
|
201
|
+
|
202
|
+
```
|
203
|
+
ohmyapi migrate [ <app> ] # no app means all INSTALLED_APPS
|
204
|
+
```
|
205
|
+
|
206
|
+
Run your project:
|
207
|
+
|
208
|
+
```
|
209
|
+
ohmyapi serve
|
210
|
+
```
|
211
|
+
|
212
|
+
## Shell
|
213
|
+
|
214
|
+
Similar to Django, you can attach to an interactive shell with your project already loaded inside.
|
215
|
+
|
216
|
+
```
|
217
|
+
ohmyapi shell
|
218
|
+
```
|
219
|
+
|
220
|
+
## Authentication
|
221
|
+
|
222
|
+
A builtin auth app is available.
|
223
|
+
|
224
|
+
Simply add `ohmyapi_auth` to your INSTALLED_APPS and define a JWT_SECRET in your `settings.py`.
|
225
|
+
Remember to `makemigrations` and `migrate` for the necessary tables to be created in the database.
|
226
|
+
|
227
|
+
`settings.py`:
|
228
|
+
|
229
|
+
```
|
230
|
+
INSTALLED_APPS = [
|
231
|
+
'ohmyapi_auth',
|
232
|
+
...
|
233
|
+
]
|
234
|
+
|
235
|
+
JWT_SECRET = "t0ps3cr3t"
|
236
|
+
```
|
237
|
+
|
238
|
+
After restarting your project you will have access to the `ohmyapi_auth` app.
|
239
|
+
It comes with a `User` and `Group` model, as well as endpoints for JWT auth.
|
240
|
+
|
241
|
+
You can use the models as `ForeignKeyField` in your application models:
|
242
|
+
|
243
|
+
```python
|
244
|
+
class Team(Model):
|
245
|
+
[...]
|
246
|
+
members = field.ManyToManyField('ohmyapi_auth.User', related_name='tournament_teams', through='tournament_teams')
|
247
|
+
[...]
|
248
|
+
```
|
249
|
+
|
250
|
+
Remember to run `makemigrations` and `migrate` in order for your model changes to take effect in the database.
|
251
|
+
|
252
|
+
Create a super-user:
|
253
|
+
|
254
|
+
```
|
255
|
+
ohmyapi createsuperuser
|
256
|
+
```
|
257
|
+
|
258
|
+
## Permissions
|
259
|
+
|
260
|
+
### API-Level Permissions
|
261
|
+
|
262
|
+
Use FastAPI's `Depends` pattern to implement API-level access-control.
|
263
|
+
|
264
|
+
|
265
|
+
In your `routes.py`:
|
266
|
+
|
267
|
+
```python
|
268
|
+
from ohmyapi.router import APIRouter, Depends
|
269
|
+
|
270
|
+
from ohmyapi_auth.models import User
|
271
|
+
from ohmyapi_auth import (
|
272
|
+
models as auth,
|
273
|
+
permissions,
|
274
|
+
)
|
275
|
+
|
276
|
+
from .models import Tournament
|
277
|
+
|
278
|
+
router = APIRouter(prefix="/tournament")
|
279
|
+
|
280
|
+
|
281
|
+
@router.get("/")
|
282
|
+
async def list(user: auth.User = Depends(permissions.require_authenticated)):
|
283
|
+
queryset = Tournament.all()
|
284
|
+
return await Tournament.Schema.many.from_queryset(queryset)
|
285
|
+
|
286
|
+
|
287
|
+
...
|
288
|
+
```
|
289
|
+
|
290
|
+
### Model-Level Permissions
|
291
|
+
|
292
|
+
Use Tortoise's `Manager` to implement model-layer permissions.
|
293
|
+
|
294
|
+
```python
|
295
|
+
from ohmyapi.db import Manager
|
296
|
+
from typing import Callable
|
297
|
+
|
298
|
+
|
299
|
+
class TeamManager(Manager):
|
300
|
+
async def for_user(self, user):
|
301
|
+
return await self.filter(members=user).all()
|
302
|
+
|
303
|
+
|
304
|
+
class Team(Model):
|
305
|
+
[...]
|
306
|
+
|
307
|
+
class Meta:
|
308
|
+
manager = TeamManager()
|
309
|
+
```
|
310
|
+
|
ohmyapi-0.1.7/README.md
ADDED
@@ -0,0 +1,277 @@
|
|
1
|
+
# OhMyAPI
|
2
|
+
|
3
|
+
> Think: Micro-Django, but API-first, less clunky and 100% async.
|
4
|
+
|
5
|
+
OhMyAPI is a Django-flavored web-application scaffolding framework and management layer.
|
6
|
+
Built around FastAPI and TortoiseORM, it is 100% async.
|
7
|
+
|
8
|
+
It is ***blazingly fast***, ***fun*** to use and comes with ***batteries included***!
|
9
|
+
|
10
|
+
**Features**
|
11
|
+
|
12
|
+
- Django-like project-layout and -structure
|
13
|
+
- Django-like prject-level settings.py
|
14
|
+
- Django-like models via TortoiseORM
|
15
|
+
- Django-like `Model.Meta` class for model configuration
|
16
|
+
- Easily convert your query results to `pydantic` models via `Model.Schema`
|
17
|
+
- Django-like migrations (makemigrations & migrate) via Aerich
|
18
|
+
- Django-like CLI tooling (`startproject`, `startapp`, `shell`, `serve`, etc)
|
19
|
+
- Various optional builtin apps you can hook into your project
|
20
|
+
- Highly configurable and customizable
|
21
|
+
- 100% async
|
22
|
+
|
23
|
+
---
|
24
|
+
|
25
|
+
## Getting started
|
26
|
+
|
27
|
+
**Creating a Project**
|
28
|
+
|
29
|
+
```
|
30
|
+
pip install ohmyapi
|
31
|
+
ohmyapi startproject myproject
|
32
|
+
cd myproject
|
33
|
+
```
|
34
|
+
|
35
|
+
This will create the following directory structure:
|
36
|
+
|
37
|
+
```
|
38
|
+
myproject/
|
39
|
+
- pyproject.toml
|
40
|
+
- README.md
|
41
|
+
- settings.py
|
42
|
+
```
|
43
|
+
|
44
|
+
Run your project with:
|
45
|
+
|
46
|
+
```
|
47
|
+
ohmyapi serve
|
48
|
+
```
|
49
|
+
|
50
|
+
In your browser go to:
|
51
|
+
- http://localhost:8000/docs
|
52
|
+
|
53
|
+
**Creating an App**
|
54
|
+
|
55
|
+
Create a new app by:
|
56
|
+
|
57
|
+
```
|
58
|
+
ohmyapi startapp tournament
|
59
|
+
```
|
60
|
+
|
61
|
+
This will create the following directory structure:
|
62
|
+
|
63
|
+
```
|
64
|
+
myproject/
|
65
|
+
- tournament/
|
66
|
+
- __init__.py
|
67
|
+
- models.py
|
68
|
+
- routes.py
|
69
|
+
- pyproject.toml
|
70
|
+
- README.md
|
71
|
+
- settings.py
|
72
|
+
```
|
73
|
+
|
74
|
+
Add 'tournament' to your `INSTALLED_APPS` in `settings.py`.
|
75
|
+
|
76
|
+
### Models
|
77
|
+
|
78
|
+
Write your first model in `turnament/models.py`:
|
79
|
+
|
80
|
+
```python
|
81
|
+
from ohmyapi.db import Model, field
|
82
|
+
|
83
|
+
|
84
|
+
class Tournament(Model):
|
85
|
+
id = field.IntField(primary_key=True)
|
86
|
+
name = field.TextField()
|
87
|
+
created = field.DatetimeField(auto_now_add=True)
|
88
|
+
|
89
|
+
def __str__(self):
|
90
|
+
return self.name
|
91
|
+
|
92
|
+
|
93
|
+
class Event(Model):
|
94
|
+
id = field.IntField(primary_key=True)
|
95
|
+
name = field.TextField()
|
96
|
+
tournament = field.ForeignKeyField('tournament.Tournament', related_name='events')
|
97
|
+
participants = field.ManyToManyField('torunament.Team', related_name='events', through='event_team')
|
98
|
+
modified = field.DatetimeField(auto_now=True)
|
99
|
+
prize = field.DecimalField(max_digits=10, decimal_places=2, null=True)
|
100
|
+
|
101
|
+
def __str__(self):
|
102
|
+
return self.name
|
103
|
+
|
104
|
+
|
105
|
+
class Team(Model):
|
106
|
+
id = field.IntField(primary_key=True)
|
107
|
+
name = field.TextField()
|
108
|
+
|
109
|
+
def __str__(self):
|
110
|
+
return self.name
|
111
|
+
```
|
112
|
+
|
113
|
+
### API Routes
|
114
|
+
|
115
|
+
Next, create your endpoints in `tournament/routes.py`:
|
116
|
+
|
117
|
+
```python
|
118
|
+
from ohmyapi.router import APIRouter, HTTPException
|
119
|
+
from ohmyapi.db.exceptions import DoesNotExist
|
120
|
+
|
121
|
+
from .models import Tournament
|
122
|
+
|
123
|
+
router = APIRouter(prefix="/tournament")
|
124
|
+
|
125
|
+
|
126
|
+
@router.get("/")
|
127
|
+
async def list():
|
128
|
+
queryset = Tournament.all()
|
129
|
+
return await Tournament.Schema.many.from_queryset(queryset)
|
130
|
+
|
131
|
+
|
132
|
+
@router.get("/:id")
|
133
|
+
async def get(id: int):
|
134
|
+
try:
|
135
|
+
queryset = Tournament.get(pk=id)
|
136
|
+
return await Tournament.Schema.one(queryset)
|
137
|
+
except DoesNotExist:
|
138
|
+
raise HTTPException(status_code=404, detail="item not found")
|
139
|
+
|
140
|
+
...
|
141
|
+
```
|
142
|
+
|
143
|
+
## Migrations
|
144
|
+
|
145
|
+
Before we can run the app, we need to create and initialize the database.
|
146
|
+
|
147
|
+
Similar to Django, first run:
|
148
|
+
|
149
|
+
```
|
150
|
+
ohmyapi makemigrations [ <app> ] # no app means all INSTALLED_APPS
|
151
|
+
```
|
152
|
+
|
153
|
+
This will create a `migrations/` folder in you project root.
|
154
|
+
|
155
|
+
```
|
156
|
+
myproject/
|
157
|
+
- tournament/
|
158
|
+
- __init__.py
|
159
|
+
- models.py
|
160
|
+
- routes.py
|
161
|
+
- migrations/
|
162
|
+
- tournament/
|
163
|
+
- pyproject.toml
|
164
|
+
- README.md
|
165
|
+
- settings.py
|
166
|
+
```
|
167
|
+
|
168
|
+
Apply your migrations via:
|
169
|
+
|
170
|
+
```
|
171
|
+
ohmyapi migrate [ <app> ] # no app means all INSTALLED_APPS
|
172
|
+
```
|
173
|
+
|
174
|
+
Run your project:
|
175
|
+
|
176
|
+
```
|
177
|
+
ohmyapi serve
|
178
|
+
```
|
179
|
+
|
180
|
+
## Shell
|
181
|
+
|
182
|
+
Similar to Django, you can attach to an interactive shell with your project already loaded inside.
|
183
|
+
|
184
|
+
```
|
185
|
+
ohmyapi shell
|
186
|
+
```
|
187
|
+
|
188
|
+
## Authentication
|
189
|
+
|
190
|
+
A builtin auth app is available.
|
191
|
+
|
192
|
+
Simply add `ohmyapi_auth` to your INSTALLED_APPS and define a JWT_SECRET in your `settings.py`.
|
193
|
+
Remember to `makemigrations` and `migrate` for the necessary tables to be created in the database.
|
194
|
+
|
195
|
+
`settings.py`:
|
196
|
+
|
197
|
+
```
|
198
|
+
INSTALLED_APPS = [
|
199
|
+
'ohmyapi_auth',
|
200
|
+
...
|
201
|
+
]
|
202
|
+
|
203
|
+
JWT_SECRET = "t0ps3cr3t"
|
204
|
+
```
|
205
|
+
|
206
|
+
After restarting your project you will have access to the `ohmyapi_auth` app.
|
207
|
+
It comes with a `User` and `Group` model, as well as endpoints for JWT auth.
|
208
|
+
|
209
|
+
You can use the models as `ForeignKeyField` in your application models:
|
210
|
+
|
211
|
+
```python
|
212
|
+
class Team(Model):
|
213
|
+
[...]
|
214
|
+
members = field.ManyToManyField('ohmyapi_auth.User', related_name='tournament_teams', through='tournament_teams')
|
215
|
+
[...]
|
216
|
+
```
|
217
|
+
|
218
|
+
Remember to run `makemigrations` and `migrate` in order for your model changes to take effect in the database.
|
219
|
+
|
220
|
+
Create a super-user:
|
221
|
+
|
222
|
+
```
|
223
|
+
ohmyapi createsuperuser
|
224
|
+
```
|
225
|
+
|
226
|
+
## Permissions
|
227
|
+
|
228
|
+
### API-Level Permissions
|
229
|
+
|
230
|
+
Use FastAPI's `Depends` pattern to implement API-level access-control.
|
231
|
+
|
232
|
+
|
233
|
+
In your `routes.py`:
|
234
|
+
|
235
|
+
```python
|
236
|
+
from ohmyapi.router import APIRouter, Depends
|
237
|
+
|
238
|
+
from ohmyapi_auth.models import User
|
239
|
+
from ohmyapi_auth import (
|
240
|
+
models as auth,
|
241
|
+
permissions,
|
242
|
+
)
|
243
|
+
|
244
|
+
from .models import Tournament
|
245
|
+
|
246
|
+
router = APIRouter(prefix="/tournament")
|
247
|
+
|
248
|
+
|
249
|
+
@router.get("/")
|
250
|
+
async def list(user: auth.User = Depends(permissions.require_authenticated)):
|
251
|
+
queryset = Tournament.all()
|
252
|
+
return await Tournament.Schema.many.from_queryset(queryset)
|
253
|
+
|
254
|
+
|
255
|
+
...
|
256
|
+
```
|
257
|
+
|
258
|
+
### Model-Level Permissions
|
259
|
+
|
260
|
+
Use Tortoise's `Manager` to implement model-layer permissions.
|
261
|
+
|
262
|
+
```python
|
263
|
+
from ohmyapi.db import Manager
|
264
|
+
from typing import Callable
|
265
|
+
|
266
|
+
|
267
|
+
class TeamManager(Manager):
|
268
|
+
async def for_user(self, user):
|
269
|
+
return await self.filter(members=user).all()
|
270
|
+
|
271
|
+
|
272
|
+
class Team(Model):
|
273
|
+
[...]
|
274
|
+
|
275
|
+
class Meta:
|
276
|
+
manager = TeamManager()
|
277
|
+
```
|
@@ -0,0 +1,38 @@
|
|
1
|
+
[project]
|
2
|
+
name = "ohmyapi"
|
3
|
+
version = "0.1.7"
|
4
|
+
description = "A Django-like but async web-framework based on FastAPI and TortoiseORM."
|
5
|
+
license = "MIT"
|
6
|
+
keywords = ["fastapi", "tortoise", "orm", "async", "web-framework"]
|
7
|
+
authors = [
|
8
|
+
{name = "Brian Wiborg", email = "me@brianwib.org"}
|
9
|
+
]
|
10
|
+
readme = "README.md"
|
11
|
+
requires-python = ">=3.13"
|
12
|
+
|
13
|
+
dependencies = [
|
14
|
+
"typer >=0.19.1,<0.20.0",
|
15
|
+
"jinja2 >=3.1.6,<4.0.0",
|
16
|
+
"fastapi >=0.117.1,<0.118.0",
|
17
|
+
"tortoise-orm >=0.25.1,<0.26.0",
|
18
|
+
"aerich >=0.9.1,<0.10.0",
|
19
|
+
"uvicorn >=0.36.0,<0.37.0",
|
20
|
+
"ipython >=9.5.0,<10.0.0",
|
21
|
+
"passlib >=1.7.4,<2.0.0",
|
22
|
+
"pyjwt >=2.10.1,<3.0.0",
|
23
|
+
"python-multipart >=0.0.20,<0.0.21",
|
24
|
+
"crypto >=1.4.1,<2.0.0",
|
25
|
+
"argon2-cffi >=25.1.0,<26.0.0",
|
26
|
+
]
|
27
|
+
|
28
|
+
[tool.poetry.group.dev.dependencies]
|
29
|
+
ipython = ">=9.5.0,<10.0.0"
|
30
|
+
|
31
|
+
[project.optional-dependencies]
|
32
|
+
auth = ["passlib", "pyjwt", "crypto", "argon2-cffi", "python-multipart"]
|
33
|
+
|
34
|
+
[tool.poetry]
|
35
|
+
packages = [ { include = "ohmyapi", from = "src" } ]
|
36
|
+
|
37
|
+
[project.scripts]
|
38
|
+
ohmyapi = "ohmyapi.cli:app"
|
@@ -0,0 +1,37 @@
|
|
1
|
+
[project]
|
2
|
+
name = "{{ project_name }}"
|
3
|
+
version = "0.1.0"
|
4
|
+
description = "OhMyAPI project"
|
5
|
+
authors = [
|
6
|
+
{ name = "You", email = "you@you.tld" }
|
7
|
+
]
|
8
|
+
requires-python = ">=3.13"
|
9
|
+
readme = "README.md"
|
10
|
+
license = { text = "MIT" }
|
11
|
+
|
12
|
+
dependencies = [
|
13
|
+
"typer >=0.19.1,<0.20.0",
|
14
|
+
"jinja2 >=3.1.6,<4.0.0",
|
15
|
+
"fastapi >=0.117.1,<0.118.0",
|
16
|
+
"tortoise-orm >=0.25.1,<0.26.0",
|
17
|
+
"aerich >=0.9.1,<0.10.0",
|
18
|
+
"uvicorn >=0.36.0,<0.37.0",
|
19
|
+
"ipython >=9.5.0,<10.0.0",
|
20
|
+
"passlib >=1.7.4,<2.0.0",
|
21
|
+
"pyjwt >=2.10.1,<3.0.0",
|
22
|
+
"python-multipart >=0.0.20,<0.0.21",
|
23
|
+
"crypto >=1.4.1,<2.0.0",
|
24
|
+
"argon2-cffi >=25.1.0,<26.0.0",
|
25
|
+
]
|
26
|
+
|
27
|
+
[tool.poetry.group.dev.dependencies]
|
28
|
+
ipython = ">=9.5.0,<10.0.0"
|
29
|
+
|
30
|
+
[project.optional-dependencies]
|
31
|
+
auth = ["passlib", "pyjwt", "crypto", "argon2-cffi", "python-multipart"]
|
32
|
+
|
33
|
+
[tool.poetry]
|
34
|
+
package-mode = false
|
35
|
+
|
36
|
+
[project.scripts]
|
37
|
+
{{ project_name }} = "ohmyapi.cli:app"
|
@@ -0,0 +1 @@
|
|
1
|
+
from .model import Model, field
|
ohmyapi-0.1.6/PKG-INFO
DELETED
@@ -1,204 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: ohmyapi
|
3
|
-
Version: 0.1.6
|
4
|
-
Summary: A Django-like but async web-framework based on FastAPI and TortoiseORM.
|
5
|
-
License-Expression: MIT
|
6
|
-
Keywords: fastapi,tortoise,orm,async,web-framework
|
7
|
-
Author: Brian Wiborg
|
8
|
-
Author-email: me@brianwib.org
|
9
|
-
Requires-Python: >=3.13
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
11
|
-
Classifier: Programming Language :: Python :: 3.13
|
12
|
-
Classifier: Programming Language :: Python :: 3.14
|
13
|
-
Requires-Dist: aerich (>=0.9.1,<0.10.0)
|
14
|
-
Requires-Dist: argon2-cffi (>=25.1.0,<26.0.0)
|
15
|
-
Requires-Dist: crypto (>=1.4.1,<2.0.0)
|
16
|
-
Requires-Dist: fastapi (>=0.117.1,<0.118.0)
|
17
|
-
Requires-Dist: ipython (>=9.5.0,<10.0.0)
|
18
|
-
Requires-Dist: jinja2 (>=3.1.6,<4.0.0)
|
19
|
-
Requires-Dist: passlib (>=1.7.4,<2.0.0)
|
20
|
-
Requires-Dist: pyjwt (>=2.10.1,<3.0.0)
|
21
|
-
Requires-Dist: python-multipart (>=0.0.20,<0.0.21)
|
22
|
-
Requires-Dist: tortoise-orm (>=0.25.1,<0.26.0)
|
23
|
-
Requires-Dist: typer (>=0.19.1,<0.20.0)
|
24
|
-
Requires-Dist: uvicorn (>=0.36.0,<0.37.0)
|
25
|
-
Description-Content-Type: text/markdown
|
26
|
-
|
27
|
-
# OhMyAPI
|
28
|
-
|
29
|
-
> OhMyAPI == Application scaffolding for FastAPI+TortoiseORM.
|
30
|
-
|
31
|
-
OhMyAPI is a Django-flavored web-application scaffolding framework.
|
32
|
-
Built around FastAPI and TortoiseORM, it 100% async.
|
33
|
-
It is blazingly fast and has batteries included.
|
34
|
-
|
35
|
-
Features:
|
36
|
-
|
37
|
-
- Django-like project-layout and -structure
|
38
|
-
- Django-like settings.py
|
39
|
-
- Django-like models via TortoiseORM
|
40
|
-
- Django-like model.Meta class for model configuration
|
41
|
-
- Django-like advanced permissions system
|
42
|
-
- Django-like migrations (makemigrations & migrate) via Aerich
|
43
|
-
- Django-like CLI for interfacing with your projects (startproject, startapp, shell, serve, etc)
|
44
|
-
- various optional builtin apps
|
45
|
-
- highly configurable and customizable
|
46
|
-
- 100% async
|
47
|
-
|
48
|
-
## Getting started
|
49
|
-
|
50
|
-
**Creating a Project**
|
51
|
-
|
52
|
-
```
|
53
|
-
pip install ohmyapi
|
54
|
-
ohmyapi startproject myproject
|
55
|
-
cd myproject
|
56
|
-
```
|
57
|
-
|
58
|
-
This will create the following directory structure:
|
59
|
-
|
60
|
-
```
|
61
|
-
myproject/
|
62
|
-
- pyproject.toml
|
63
|
-
- settings.py
|
64
|
-
```
|
65
|
-
|
66
|
-
Run your project with:
|
67
|
-
|
68
|
-
```
|
69
|
-
ohmyapi serve
|
70
|
-
```
|
71
|
-
|
72
|
-
In your browser go to:
|
73
|
-
- http://localhost:8000/docs
|
74
|
-
|
75
|
-
**Creating an App**
|
76
|
-
|
77
|
-
Create a new app by:
|
78
|
-
|
79
|
-
```
|
80
|
-
ohmyapi startapp myapp
|
81
|
-
```
|
82
|
-
|
83
|
-
This will lead to the following directory structure:
|
84
|
-
|
85
|
-
```
|
86
|
-
myproject/
|
87
|
-
- myapp/
|
88
|
-
- __init__.py
|
89
|
-
- models.py
|
90
|
-
- routes.py
|
91
|
-
- pyproject.toml
|
92
|
-
- settings.py
|
93
|
-
```
|
94
|
-
|
95
|
-
Add 'myapp' to your `INSTALLED_APPS` in `settings.py`.
|
96
|
-
|
97
|
-
Write your first model in `myapp/models.py`:
|
98
|
-
|
99
|
-
```python
|
100
|
-
from ohmyapi.db import Model, field
|
101
|
-
|
102
|
-
|
103
|
-
class Person(Model):
|
104
|
-
id: int = field.IntField(min=1, pk=True)
|
105
|
-
name: str = field.CharField(min_length=1, max_length=255)
|
106
|
-
username: str = field.CharField(min_length=1, max_length=255, unique=True)
|
107
|
-
age: int = field.IntField(min=0)
|
108
|
-
```
|
109
|
-
|
110
|
-
Next, create your endpoints in `myapp/routes.py`:
|
111
|
-
|
112
|
-
```python
|
113
|
-
from fastapi import APIRouter, HTTPException
|
114
|
-
from tortoise.exceptions import DoesNotExist
|
115
|
-
|
116
|
-
from .models import Person
|
117
|
-
|
118
|
-
router = APIRouter(prefix="/myapp")
|
119
|
-
|
120
|
-
|
121
|
-
@router.get("/")
|
122
|
-
async def list():
|
123
|
-
return await Person.Schema.many.from_queryset(Person.all())
|
124
|
-
|
125
|
-
|
126
|
-
@router.get("/:id")
|
127
|
-
async def get(id: int):
|
128
|
-
try:
|
129
|
-
return await Person.Schema.one(Person.get(pk=id))
|
130
|
-
except DoesNotExist:
|
131
|
-
raise HTTPException(status_code=404, detail="item not found")
|
132
|
-
|
133
|
-
...
|
134
|
-
```
|
135
|
-
|
136
|
-
## Migrations
|
137
|
-
|
138
|
-
Before we can run the app, we need to create and initialize the database.
|
139
|
-
|
140
|
-
Similar to Django, first run:
|
141
|
-
|
142
|
-
```
|
143
|
-
ohmyapi makemigrations [ <app> ] # no app means all INSTALLED_APPS
|
144
|
-
```
|
145
|
-
|
146
|
-
This will create a `migrations/` folder in you project root.
|
147
|
-
|
148
|
-
```
|
149
|
-
myproject/
|
150
|
-
- myapp/
|
151
|
-
- __init__.py
|
152
|
-
- models.py
|
153
|
-
- routes.py
|
154
|
-
- migrations/
|
155
|
-
- myapp/
|
156
|
-
- pyproject.toml
|
157
|
-
- settings.py
|
158
|
-
```
|
159
|
-
|
160
|
-
Apply your migrations via:
|
161
|
-
|
162
|
-
```
|
163
|
-
ohmyapi migrate [ <app> ] # no app means all INSTALLED_APPS
|
164
|
-
```
|
165
|
-
|
166
|
-
Run your project:
|
167
|
-
|
168
|
-
```
|
169
|
-
ohmyapi serve
|
170
|
-
```
|
171
|
-
|
172
|
-
## Shell
|
173
|
-
|
174
|
-
Similar to Django, you can attach to an interactive shell with your project already loaded inside.
|
175
|
-
|
176
|
-
```
|
177
|
-
ohmyapi shell
|
178
|
-
```
|
179
|
-
|
180
|
-
## Authentication
|
181
|
-
|
182
|
-
A builtin auth app is available.
|
183
|
-
|
184
|
-
Simply add `ohmyapi_auth` to your INSTALLED_APPS and define a JWT_SECRET in your `settings.py`.
|
185
|
-
Remember to `makemigrations` and `migrate` for the auth tables to be created in the database.
|
186
|
-
|
187
|
-
`settings.py`:
|
188
|
-
|
189
|
-
```
|
190
|
-
INSTALLED_APPS = [
|
191
|
-
'ohmyapi_auth',
|
192
|
-
...
|
193
|
-
]
|
194
|
-
|
195
|
-
JWT_SECRET = "t0ps3cr3t"
|
196
|
-
```
|
197
|
-
|
198
|
-
Create a super-user:
|
199
|
-
|
200
|
-
```
|
201
|
-
ohmyapi createsuperuser
|
202
|
-
```
|
203
|
-
|
204
|
-
|
ohmyapi-0.1.6/README.md
DELETED
@@ -1,177 +0,0 @@
|
|
1
|
-
# OhMyAPI
|
2
|
-
|
3
|
-
> OhMyAPI == Application scaffolding for FastAPI+TortoiseORM.
|
4
|
-
|
5
|
-
OhMyAPI is a Django-flavored web-application scaffolding framework.
|
6
|
-
Built around FastAPI and TortoiseORM, it 100% async.
|
7
|
-
It is blazingly fast and has batteries included.
|
8
|
-
|
9
|
-
Features:
|
10
|
-
|
11
|
-
- Django-like project-layout and -structure
|
12
|
-
- Django-like settings.py
|
13
|
-
- Django-like models via TortoiseORM
|
14
|
-
- Django-like model.Meta class for model configuration
|
15
|
-
- Django-like advanced permissions system
|
16
|
-
- Django-like migrations (makemigrations & migrate) via Aerich
|
17
|
-
- Django-like CLI for interfacing with your projects (startproject, startapp, shell, serve, etc)
|
18
|
-
- various optional builtin apps
|
19
|
-
- highly configurable and customizable
|
20
|
-
- 100% async
|
21
|
-
|
22
|
-
## Getting started
|
23
|
-
|
24
|
-
**Creating a Project**
|
25
|
-
|
26
|
-
```
|
27
|
-
pip install ohmyapi
|
28
|
-
ohmyapi startproject myproject
|
29
|
-
cd myproject
|
30
|
-
```
|
31
|
-
|
32
|
-
This will create the following directory structure:
|
33
|
-
|
34
|
-
```
|
35
|
-
myproject/
|
36
|
-
- pyproject.toml
|
37
|
-
- settings.py
|
38
|
-
```
|
39
|
-
|
40
|
-
Run your project with:
|
41
|
-
|
42
|
-
```
|
43
|
-
ohmyapi serve
|
44
|
-
```
|
45
|
-
|
46
|
-
In your browser go to:
|
47
|
-
- http://localhost:8000/docs
|
48
|
-
|
49
|
-
**Creating an App**
|
50
|
-
|
51
|
-
Create a new app by:
|
52
|
-
|
53
|
-
```
|
54
|
-
ohmyapi startapp myapp
|
55
|
-
```
|
56
|
-
|
57
|
-
This will lead to the following directory structure:
|
58
|
-
|
59
|
-
```
|
60
|
-
myproject/
|
61
|
-
- myapp/
|
62
|
-
- __init__.py
|
63
|
-
- models.py
|
64
|
-
- routes.py
|
65
|
-
- pyproject.toml
|
66
|
-
- settings.py
|
67
|
-
```
|
68
|
-
|
69
|
-
Add 'myapp' to your `INSTALLED_APPS` in `settings.py`.
|
70
|
-
|
71
|
-
Write your first model in `myapp/models.py`:
|
72
|
-
|
73
|
-
```python
|
74
|
-
from ohmyapi.db import Model, field
|
75
|
-
|
76
|
-
|
77
|
-
class Person(Model):
|
78
|
-
id: int = field.IntField(min=1, pk=True)
|
79
|
-
name: str = field.CharField(min_length=1, max_length=255)
|
80
|
-
username: str = field.CharField(min_length=1, max_length=255, unique=True)
|
81
|
-
age: int = field.IntField(min=0)
|
82
|
-
```
|
83
|
-
|
84
|
-
Next, create your endpoints in `myapp/routes.py`:
|
85
|
-
|
86
|
-
```python
|
87
|
-
from fastapi import APIRouter, HTTPException
|
88
|
-
from tortoise.exceptions import DoesNotExist
|
89
|
-
|
90
|
-
from .models import Person
|
91
|
-
|
92
|
-
router = APIRouter(prefix="/myapp")
|
93
|
-
|
94
|
-
|
95
|
-
@router.get("/")
|
96
|
-
async def list():
|
97
|
-
return await Person.Schema.many.from_queryset(Person.all())
|
98
|
-
|
99
|
-
|
100
|
-
@router.get("/:id")
|
101
|
-
async def get(id: int):
|
102
|
-
try:
|
103
|
-
return await Person.Schema.one(Person.get(pk=id))
|
104
|
-
except DoesNotExist:
|
105
|
-
raise HTTPException(status_code=404, detail="item not found")
|
106
|
-
|
107
|
-
...
|
108
|
-
```
|
109
|
-
|
110
|
-
## Migrations
|
111
|
-
|
112
|
-
Before we can run the app, we need to create and initialize the database.
|
113
|
-
|
114
|
-
Similar to Django, first run:
|
115
|
-
|
116
|
-
```
|
117
|
-
ohmyapi makemigrations [ <app> ] # no app means all INSTALLED_APPS
|
118
|
-
```
|
119
|
-
|
120
|
-
This will create a `migrations/` folder in you project root.
|
121
|
-
|
122
|
-
```
|
123
|
-
myproject/
|
124
|
-
- myapp/
|
125
|
-
- __init__.py
|
126
|
-
- models.py
|
127
|
-
- routes.py
|
128
|
-
- migrations/
|
129
|
-
- myapp/
|
130
|
-
- pyproject.toml
|
131
|
-
- settings.py
|
132
|
-
```
|
133
|
-
|
134
|
-
Apply your migrations via:
|
135
|
-
|
136
|
-
```
|
137
|
-
ohmyapi migrate [ <app> ] # no app means all INSTALLED_APPS
|
138
|
-
```
|
139
|
-
|
140
|
-
Run your project:
|
141
|
-
|
142
|
-
```
|
143
|
-
ohmyapi serve
|
144
|
-
```
|
145
|
-
|
146
|
-
## Shell
|
147
|
-
|
148
|
-
Similar to Django, you can attach to an interactive shell with your project already loaded inside.
|
149
|
-
|
150
|
-
```
|
151
|
-
ohmyapi shell
|
152
|
-
```
|
153
|
-
|
154
|
-
## Authentication
|
155
|
-
|
156
|
-
A builtin auth app is available.
|
157
|
-
|
158
|
-
Simply add `ohmyapi_auth` to your INSTALLED_APPS and define a JWT_SECRET in your `settings.py`.
|
159
|
-
Remember to `makemigrations` and `migrate` for the auth tables to be created in the database.
|
160
|
-
|
161
|
-
`settings.py`:
|
162
|
-
|
163
|
-
```
|
164
|
-
INSTALLED_APPS = [
|
165
|
-
'ohmyapi_auth',
|
166
|
-
...
|
167
|
-
]
|
168
|
-
|
169
|
-
JWT_SECRET = "t0ps3cr3t"
|
170
|
-
```
|
171
|
-
|
172
|
-
Create a super-user:
|
173
|
-
|
174
|
-
```
|
175
|
-
ohmyapi createsuperuser
|
176
|
-
```
|
177
|
-
|
ohmyapi-0.1.6/pyproject.toml
DELETED
@@ -1,36 +0,0 @@
|
|
1
|
-
[project]
|
2
|
-
name = "ohmyapi"
|
3
|
-
version = "0.1.6"
|
4
|
-
description = "A Django-like but async web-framework based on FastAPI and TortoiseORM."
|
5
|
-
license = "MIT"
|
6
|
-
keywords = ["fastapi", "tortoise", "orm", "async", "web-framework"]
|
7
|
-
authors = [
|
8
|
-
{name = "Brian Wiborg", email = "me@brianwib.org"}
|
9
|
-
]
|
10
|
-
readme = "README.md"
|
11
|
-
requires-python = ">=3.13"
|
12
|
-
dependencies = [
|
13
|
-
"typer (>=0.19.1,<0.20.0)",
|
14
|
-
"jinja2 (>=3.1.6,<4.0.0)",
|
15
|
-
"fastapi (>=0.117.1,<0.118.0)",
|
16
|
-
"tortoise-orm (>=0.25.1,<0.26.0)",
|
17
|
-
"aerich (>=0.9.1,<0.10.0)",
|
18
|
-
"uvicorn (>=0.36.0,<0.37.0)",
|
19
|
-
"ipython (>=9.5.0,<10.0.0)",
|
20
|
-
"passlib (>=1.7.4,<2.0.0)",
|
21
|
-
"pyjwt (>=2.10.1,<3.0.0)",
|
22
|
-
"python-multipart (>=0.0.20,<0.0.21)",
|
23
|
-
"crypto (>=1.4.1,<2.0.0)",
|
24
|
-
"argon2-cffi (>=25.1.0,<26.0.0)",
|
25
|
-
]
|
26
|
-
|
27
|
-
[tool.poetry]
|
28
|
-
packages = [{include = "ohmyapi", from = "src"}]
|
29
|
-
|
30
|
-
[tool.poetry.scripts]
|
31
|
-
ohmyapi = "ohmyapi.cli:main"
|
32
|
-
|
33
|
-
[build-system]
|
34
|
-
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
35
|
-
build-backend = "poetry.core.masonry.api"
|
36
|
-
|
@@ -1,13 +0,0 @@
|
|
1
|
-
[tool.poetry]
|
2
|
-
name = "{{ project_name }}"
|
3
|
-
version = "0.1.0"
|
4
|
-
description = "OhMyAPI project"
|
5
|
-
authors = ["You <you@example.com>"]
|
6
|
-
|
7
|
-
[tool.poetry.dependencies]
|
8
|
-
python = "^3.10"
|
9
|
-
fastapi = "^0.115"
|
10
|
-
uvicorn = "^0.30"
|
11
|
-
tortoise-orm = "^0.20"
|
12
|
-
aerich = "^0.7"
|
13
|
-
|
@@ -1 +0,0 @@
|
|
1
|
-
from .model import Model, fields
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|