ohmyapi 0.1.21__py3-none-any.whl → 0.1.23__py3-none-any.whl
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/builtin/auth/routes.py +69 -42
- ohmyapi/builtin/demo/__init__.py +2 -0
- ohmyapi/builtin/demo/models.py +50 -0
- ohmyapi/builtin/demo/routes.py +54 -0
- ohmyapi/core/templates/app/routes.py.j2 +31 -7
- ohmyapi/router.py +1 -0
- {ohmyapi-0.1.21.dist-info → ohmyapi-0.1.23.dist-info}/METADATA +67 -24
- {ohmyapi-0.1.21.dist-info → ohmyapi-0.1.23.dist-info}/RECORD +10 -7
- {ohmyapi-0.1.21.dist-info → ohmyapi-0.1.23.dist-info}/WHEEL +0 -0
- {ohmyapi-0.1.21.dist-info → ohmyapi-0.1.23.dist-info}/entry_points.txt +0 -0
ohmyapi/builtin/auth/routes.py
CHANGED
@@ -26,9 +26,64 @@ REFRESH_TOKEN_EXPIRE_SECONDS = getattr(
|
|
26
26
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
|
27
27
|
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
class ClaimsUser(BaseModel):
|
30
|
+
username: str
|
31
|
+
email: str
|
32
|
+
is_admin: bool
|
33
|
+
is_staff: bool
|
34
|
+
|
35
|
+
|
36
|
+
class Claims(BaseModel):
|
37
|
+
type: str
|
38
|
+
sub: str
|
39
|
+
user: ClaimsUser
|
40
|
+
roles: List[str]
|
41
|
+
exp: str
|
42
|
+
|
43
|
+
|
44
|
+
class AccessToken(BaseModel):
|
45
|
+
token_type: str
|
46
|
+
access_token: str
|
47
|
+
|
48
|
+
|
49
|
+
class RefreshToken(AccessToken):
|
50
|
+
refresh_token: str
|
51
|
+
|
52
|
+
|
53
|
+
class LoginRequest(BaseModel):
|
54
|
+
username: str
|
55
|
+
password: str
|
56
|
+
|
57
|
+
|
58
|
+
class TokenType(str, Enum):
|
59
|
+
"""
|
60
|
+
Helper for indicating the token type when generating claims.
|
61
|
+
"""
|
62
|
+
|
63
|
+
access = "access"
|
64
|
+
refresh = "refresh"
|
65
|
+
|
66
|
+
|
67
|
+
def claims(
|
68
|
+
token_type: TokenType, user: User, groups: List[Group] = []
|
69
|
+
) -> Claims:
|
70
|
+
return Claims(
|
71
|
+
type=token_type,
|
72
|
+
sub=str(user.id),
|
73
|
+
user=ClaimsUser(
|
74
|
+
username=user.username,
|
75
|
+
email=user.email,
|
76
|
+
is_admin=user.is_admin,
|
77
|
+
is_staff=user.is_staff,
|
78
|
+
),
|
79
|
+
roles=[g.name for g in groups],
|
80
|
+
exp="",
|
81
|
+
)
|
82
|
+
|
83
|
+
|
84
|
+
def create_token(claims: Claims, expires_in: int) -> str:
|
85
|
+
to_encode = claims.model_dump()
|
86
|
+
to_encode['exp'] = int(time.time()) + expires_in
|
32
87
|
token = jwt.encode(to_encode, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
33
88
|
if isinstance(token, bytes):
|
34
89
|
token = token.decode("utf-8")
|
@@ -48,29 +103,6 @@ def decode_token(token: str) -> Dict:
|
|
48
103
|
)
|
49
104
|
|
50
105
|
|
51
|
-
class TokenType(str, Enum):
|
52
|
-
"""
|
53
|
-
Helper for indicating the token type when generating claims.
|
54
|
-
"""
|
55
|
-
|
56
|
-
access = "access"
|
57
|
-
refresh = "refresh"
|
58
|
-
|
59
|
-
|
60
|
-
def claims(
|
61
|
-
token_type: TokenType, user: User, groups: List[Group] = []
|
62
|
-
) -> Dict[str, Any]:
|
63
|
-
return {
|
64
|
-
"type": token_type,
|
65
|
-
"sub": str(user.id),
|
66
|
-
"user": {
|
67
|
-
"username": user.username,
|
68
|
-
"email": user.email,
|
69
|
-
},
|
70
|
-
"roles": [g.name for g in groups],
|
71
|
-
}
|
72
|
-
|
73
|
-
|
74
106
|
async def get_token(token: str = Depends(oauth2_scheme)) -> Dict:
|
75
107
|
"""Dependency: token introspection"""
|
76
108
|
payload = decode_token(token)
|
@@ -127,12 +159,7 @@ async def require_group(
|
|
127
159
|
return current_user
|
128
160
|
|
129
161
|
|
130
|
-
|
131
|
-
username: str
|
132
|
-
password: str
|
133
|
-
|
134
|
-
|
135
|
-
@router.post("/login")
|
162
|
+
@router.post("/login", response_model=RefreshToken)
|
136
163
|
async def login(form_data: LoginRequest = Body(...)):
|
137
164
|
"""Login with username & password, returns access and refresh tokens."""
|
138
165
|
user = await User.authenticate(form_data.username, form_data.password)
|
@@ -148,14 +175,14 @@ async def login(form_data: LoginRequest = Body(...)):
|
|
148
175
|
claims(TokenType.refresh, user), REFRESH_TOKEN_EXPIRE_SECONDS
|
149
176
|
)
|
150
177
|
|
151
|
-
return
|
152
|
-
"
|
153
|
-
|
154
|
-
|
155
|
-
|
178
|
+
return RefreshToken(
|
179
|
+
token_type="bearer",
|
180
|
+
access_token=access_token,
|
181
|
+
refresh_token=refresh_token,
|
182
|
+
)
|
156
183
|
|
157
184
|
|
158
|
-
@router.post("/refresh")
|
185
|
+
@router.post("/refresh", response_model=AccessToken)
|
159
186
|
async def refresh_token(refresh_token: str):
|
160
187
|
"""Exchange refresh token for new access token."""
|
161
188
|
payload = decode_token(refresh_token)
|
@@ -174,15 +201,15 @@ async def refresh_token(refresh_token: str):
|
|
174
201
|
new_access = create_token(
|
175
202
|
claims(TokenType.access, user), ACCESS_TOKEN_EXPIRE_SECONDS
|
176
203
|
)
|
177
|
-
return
|
204
|
+
return AccessToken(token_type="bearer", access_token=access_token)
|
178
205
|
|
179
206
|
|
180
|
-
@router.get("/introspect")
|
207
|
+
@router.get("/introspect", response_model=Dict[str, Any])
|
181
208
|
async def introspect(token: Dict = Depends(get_token)):
|
182
209
|
return token
|
183
210
|
|
184
211
|
|
185
|
-
@router.get("/me")
|
212
|
+
@router.get("/me", response_model=User.Schema.model)
|
186
213
|
async def me(user: User = Depends(get_current_user)):
|
187
214
|
"""Return the currently authenticated user."""
|
188
|
-
return User.Schema.
|
215
|
+
return await User.Schema.model.from_tortoise_orm(user)
|
@@ -0,0 +1,50 @@
|
|
1
|
+
from ohmyapi.db import Model, field
|
2
|
+
from ohmyapi_auth.models import User
|
3
|
+
|
4
|
+
from datetime import datetime
|
5
|
+
from decimal import Decimal
|
6
|
+
from uuid import UUID
|
7
|
+
|
8
|
+
|
9
|
+
class Team(Model):
|
10
|
+
id: UUID = field.data.UUIDField(primary_key=True)
|
11
|
+
name: str = field.TextField()
|
12
|
+
members: field.ManyToManyRelation[User] = field.ManyToManyField(
|
13
|
+
'ohmyapi_auth.User',
|
14
|
+
related_name="tournament_teams",
|
15
|
+
through='user_tournament_teams',
|
16
|
+
)
|
17
|
+
|
18
|
+
def __str__(self):
|
19
|
+
return self.name
|
20
|
+
|
21
|
+
|
22
|
+
class Tournament(Model):
|
23
|
+
id: UUID = field.data.UUIDField(primary_key=True)
|
24
|
+
name: str = field.TextField()
|
25
|
+
created: datetime = field.DatetimeField(auto_now_add=True)
|
26
|
+
|
27
|
+
def __str__(self):
|
28
|
+
return self.name
|
29
|
+
|
30
|
+
|
31
|
+
class Event(Model):
|
32
|
+
id: UUID = field.data.UUIDField(primary_key=True)
|
33
|
+
name: str = field.TextField()
|
34
|
+
tournament: field.ForeignKeyRelation[Tournament] = field.ForeignKeyField(
|
35
|
+
'ohmyapi_demo.Tournament',
|
36
|
+
related_name='events',
|
37
|
+
)
|
38
|
+
participants: field.ManyToManyRelation[Team] = field.ManyToManyField(
|
39
|
+
'ohmyapi_demo.Team',
|
40
|
+
related_name='events',
|
41
|
+
through='event_team',
|
42
|
+
)
|
43
|
+
modified: datetime = field.DatetimeField(auto_now=True)
|
44
|
+
prize: Decimal = field.DecimalField(max_digits=10, decimal_places=2, null=True)
|
45
|
+
|
46
|
+
class Schema:
|
47
|
+
exclude = ['tournament_id']
|
48
|
+
|
49
|
+
def __str__(self):
|
50
|
+
return self.name
|
@@ -0,0 +1,54 @@
|
|
1
|
+
from ohmyapi.router import APIRouter, HTTPException, HTTPStatus
|
2
|
+
from ohmyapi.db.exceptions import DoesNotExist
|
3
|
+
|
4
|
+
from . import models
|
5
|
+
|
6
|
+
from typing import List
|
7
|
+
|
8
|
+
# Expose your app's routes via `router = fastapi.APIRouter`.
|
9
|
+
# Use prefixes wisely to avoid cross-app namespace-collisions.
|
10
|
+
# Tags improve the UX of the OpenAPI docs at /docs.
|
11
|
+
router = APIRouter(prefix="/tournemant")
|
12
|
+
|
13
|
+
|
14
|
+
@router.get("/",
|
15
|
+
tags=["tournament"],
|
16
|
+
response_model=List[models.Tournament.Schema.model])
|
17
|
+
async def list():
|
18
|
+
"""List all tournaments."""
|
19
|
+
return await models.Tournament.Schema.model.from_queryset(Tournament.all())
|
20
|
+
|
21
|
+
|
22
|
+
@router.post("/",
|
23
|
+
tags=["tournament"],
|
24
|
+
status_code=HTTPStatus.CREATED)
|
25
|
+
async def post(tournament: models.Tournament.Schema.readonly):
|
26
|
+
"""Create tournament."""
|
27
|
+
return await models.Tournament.Schema.model.from_queryset(models.Tournament.create(**tournament.model_dump()))
|
28
|
+
|
29
|
+
|
30
|
+
@router.get("/{id}",
|
31
|
+
tags=["tournament"],
|
32
|
+
response_model=models.Tournament.Schema.model)
|
33
|
+
async def get(id: str):
|
34
|
+
"""Get tournament by id."""
|
35
|
+
return await models.Tournament.Schema.model.from_queryset(models.Tournament.get(id=id))
|
36
|
+
|
37
|
+
|
38
|
+
@router.put("/{id}",
|
39
|
+
tags=["tournament"],
|
40
|
+
response_model=models.Tournament.Schema.model,
|
41
|
+
status_code=HTTPStatus.ACCEPTED)
|
42
|
+
async def put(tournament: models.Tournament.Schema.model):
|
43
|
+
"""Update tournament."""
|
44
|
+
return await models.Tournament.Schema.model.from_queryset(models.Tournament.update(**tournament.model_dump()))
|
45
|
+
|
46
|
+
|
47
|
+
@router.delete("/{id}", tags=["tournament"])
|
48
|
+
async def delete(id: str):
|
49
|
+
try:
|
50
|
+
tournament = await models.Tournament.get(id=id)
|
51
|
+
return await tournament.delete()
|
52
|
+
except DoesNotExist:
|
53
|
+
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="not found")
|
54
|
+
|
@@ -1,6 +1,8 @@
|
|
1
|
-
from ohmyapi.router import APIRouter
|
1
|
+
from ohmyapi.router import APIRouter, HTTPException, HTTPStatus
|
2
2
|
|
3
|
-
from .
|
3
|
+
from . import models
|
4
|
+
|
5
|
+
from typing import List
|
4
6
|
|
5
7
|
# Expose your app's routes via `router = fastapi.APIRouter`.
|
6
8
|
# Use prefixes wisely to avoid cross-app namespace-collisions.
|
@@ -8,10 +10,32 @@ from .models import ...
|
|
8
10
|
router = APIRouter(prefix="/{{ app_name }}", tags=['{{ app_name }}'])
|
9
11
|
|
10
12
|
|
13
|
+
|
11
14
|
@router.get("/")
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
async def list():
|
16
|
+
"""List all ..."""
|
17
|
+
return []
|
18
|
+
|
19
|
+
|
20
|
+
@router.post("/")
|
21
|
+
async def post():
|
22
|
+
"""Create ..."""
|
23
|
+
return HTTPException(status_code=HTTPStatus.CREATED)
|
24
|
+
|
25
|
+
|
26
|
+
@router.get("/{id}")
|
27
|
+
async def get(id: str):
|
28
|
+
"""Get single ..."""
|
29
|
+
return {}
|
30
|
+
|
31
|
+
|
32
|
+
@router.put("/{id}")
|
33
|
+
async def put(id: str):
|
34
|
+
"""Update ..."""
|
35
|
+
return HTTPException(status_code=HTTPStatus.ACCEPTED)
|
36
|
+
|
37
|
+
|
38
|
+
@router.delete("/{id}")
|
39
|
+
async def delete(id: str):
|
40
|
+
return HTTPException(status_code=HTTPStatus.ACCEPTED)
|
17
41
|
|
ohmyapi/router.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ohmyapi
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.23
|
4
4
|
Summary: A Django-like but async web-framework based on FastAPI and TortoiseORM.
|
5
5
|
License-Expression: MIT
|
6
6
|
Keywords: fastapi,tortoise,orm,async,web-framework
|
@@ -157,7 +157,7 @@ class Team(Model):
|
|
157
157
|
Next, create your endpoints in `tournament/routes.py`:
|
158
158
|
|
159
159
|
```python
|
160
|
-
from ohmyapi.router import APIRouter, HTTPException
|
160
|
+
from ohmyapi.router import APIRouter, HTTPException, HTTPStatus
|
161
161
|
from ohmyapi.db.exceptions import DoesNotExist
|
162
162
|
|
163
163
|
from .models import Tournament
|
@@ -167,20 +167,25 @@ from .models import Tournament
|
|
167
167
|
# Tags improve the UX of the OpenAPI docs at /docs.
|
168
168
|
router = APIRouter(prefix="/tournament", tags=['Tournament'])
|
169
169
|
|
170
|
-
|
171
170
|
@router.get("/")
|
172
171
|
async def list():
|
173
172
|
queryset = Tournament.all()
|
174
173
|
return await Tournament.Schema.model.from_queryset(queryset)
|
175
174
|
|
176
175
|
|
176
|
+
@router.post("/", status_code=HTTPStatus.CREATED)
|
177
|
+
async def post(tournament: Tournament.Schema.readonly):
|
178
|
+
queryset = Tournament.create(**payload.model_dump())
|
179
|
+
return await Tournament.Schema.model.from_queryset(queryset)
|
180
|
+
|
181
|
+
|
177
182
|
@router.get("/:id")
|
178
183
|
async def get(id: str):
|
179
184
|
try:
|
180
|
-
|
185
|
+
queryset = Tournament.get(id=id)
|
181
186
|
return await Tournament.Schema.model.from_queryset_single(tournament)
|
182
187
|
except DoesNotExist:
|
183
|
-
raise HTTPException(status_code=404, detail="
|
188
|
+
raise HTTPException(status_code=404, detail="not found")
|
184
189
|
|
185
190
|
...
|
186
191
|
```
|
@@ -350,28 +355,66 @@ Find your loaded project singleton via identifier: `p`
|
|
350
355
|
|
351
356
|
```python
|
352
357
|
In [1]: p
|
353
|
-
Out[1]: <ohmyapi.core.runtime.Project at
|
358
|
+
Out[1]: <ohmyapi.core.runtime.Project at 0x7f00c43dbcb0>
|
354
359
|
|
355
360
|
In [2]: p.apps
|
356
361
|
Out[2]:
|
357
|
-
{'ohmyapi_auth':
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
362
|
+
{'ohmyapi_auth': {
|
363
|
+
"models": [
|
364
|
+
"Group",
|
365
|
+
"User"
|
366
|
+
],
|
367
|
+
"routes": [
|
368
|
+
{
|
369
|
+
"path": "/auth/login",
|
370
|
+
"name": "login",
|
371
|
+
"methods": [
|
372
|
+
"POST"
|
373
|
+
],
|
374
|
+
"endpoint": "login",
|
375
|
+
"response_model": null,
|
376
|
+
"tags": [
|
377
|
+
"auth"
|
378
|
+
]
|
379
|
+
},
|
380
|
+
{
|
381
|
+
"path": "/auth/refresh",
|
382
|
+
"name": "refresh_token",
|
383
|
+
"methods": [
|
384
|
+
"POST"
|
385
|
+
],
|
386
|
+
"endpoint": "refresh_token",
|
387
|
+
"response_model": null,
|
388
|
+
"tags": [
|
389
|
+
"auth"
|
390
|
+
]
|
391
|
+
},
|
392
|
+
{
|
393
|
+
"path": "/auth/introspect",
|
394
|
+
"name": "introspect",
|
395
|
+
"methods": [
|
396
|
+
"GET"
|
397
|
+
],
|
398
|
+
"endpoint": "introspect",
|
399
|
+
"response_model": null,
|
400
|
+
"tags": [
|
401
|
+
"auth"
|
402
|
+
]
|
403
|
+
},
|
404
|
+
{
|
405
|
+
"path": "/auth/me",
|
406
|
+
"name": "me",
|
407
|
+
"methods": [
|
408
|
+
"GET"
|
409
|
+
],
|
410
|
+
"endpoint": "me",
|
411
|
+
"response_model": null,
|
412
|
+
"tags": [
|
413
|
+
"auth"
|
414
|
+
]
|
415
|
+
}
|
416
|
+
]
|
417
|
+
}}
|
375
418
|
```
|
376
419
|
|
377
420
|
|
@@ -3,14 +3,17 @@ ohmyapi/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
|
|
3
3
|
ohmyapi/builtin/auth/__init__.py,sha256=vOVCSJX8BALzs8h5ZW9507bjoscP37bncMjdMmBXcMM,42
|
4
4
|
ohmyapi/builtin/auth/models.py,sha256=Fggg3GDVydKoZQOlXXNDsWKxehvsp8BXC1xedv0Qr34,1729
|
5
5
|
ohmyapi/builtin/auth/permissions.py,sha256=mxsnhF_UGesTFle7v1JHORkNODtQ0qanAL3FtOcMCEY,145
|
6
|
-
ohmyapi/builtin/auth/routes.py,sha256=
|
6
|
+
ohmyapi/builtin/auth/routes.py,sha256=e6i9AanJqj1RK8VQVXwNV-Pv8R3X8I70SgaDtbCYUIE,6318
|
7
|
+
ohmyapi/builtin/demo/__init__.py,sha256=k1rGtOmMPVZJ1fMPELY0v3k70WyzSp18pstJTkCdFr0,42
|
8
|
+
ohmyapi/builtin/demo/models.py,sha256=8Id0dHPuow_Dtt_337UV3VTxvtVAllaVeImvstqiqdE,1403
|
9
|
+
ohmyapi/builtin/demo/routes.py,sha256=MQaPCz43v4kTv9_oz44up1j77Ra1SqSN9i3vN26xllU,1843
|
7
10
|
ohmyapi/cli.py,sha256=dJVNgpW5S4rCc619AEEKBKuEIAmQs153Ls0ZVaea48w,4173
|
8
11
|
ohmyapi/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
12
|
ohmyapi/core/runtime.py,sha256=i4dI7mC59rw0blMOeVGawUm8v5id3ZllLlP8RLo1GT0,9419
|
10
13
|
ohmyapi/core/scaffolding.py,sha256=SA0SYFd7VcqkOn9xuXgj-yOoVqCZMJo68GGFbm34GE4,2663
|
11
14
|
ohmyapi/core/templates/app/__init__.py.j2,sha256=QwVIQVUGZVhdH1d4NrvL7NTsK4-T4cihzYs8UVX2dt4,43
|
12
15
|
ohmyapi/core/templates/app/models.py.j2,sha256=_3w-vFJ5fgsmncsCv34k_wyCMF78jufbSSglns4gbb0,119
|
13
|
-
ohmyapi/core/templates/app/routes.py.j2,sha256=
|
16
|
+
ohmyapi/core/templates/app/routes.py.j2,sha256=dFpmfrfN1pwOsD6MAa_MmI7aP4kKJ2ZiijobWHsfyDs,873
|
14
17
|
ohmyapi/core/templates/project/README.md.j2,sha256=SjR4JIrg-8XRE-UntUDwiw8jDpYitD_UjwoKkYJ7GLw,22
|
15
18
|
ohmyapi/core/templates/project/pyproject.toml.j2,sha256=X0VS6YT9aL3vpHFKPTfLFsdpD8423nY57ySQpSTMxmQ,895
|
16
19
|
ohmyapi/core/templates/project/settings.py.j2,sha256=RBKGB8MZWPM3Bp0a57Y1YrSvSXxh502TUnJqbbu48Ig,138
|
@@ -18,8 +21,8 @@ ohmyapi/db/__init__.py,sha256=5QKUycxnN83DOUD_Etoee9tEOYjnZ74deqrSOOx_MiQ,204
|
|
18
21
|
ohmyapi/db/exceptions.py,sha256=vb4IIUoeYAY6sK42zRtjMy-39IFVi_Qb6mWySTY0jYw,34
|
19
22
|
ohmyapi/db/model/__init__.py,sha256=k3StTNuKatpwZo_Z5JBFa-927eJrzibFE8U4SA82asc,32
|
20
23
|
ohmyapi/db/model/model.py,sha256=WTf41ByCtfk9c_O6QCsO9KA0avHL3zGMZ6SEdw5GOuc,2420
|
21
|
-
ohmyapi/router.py,sha256=
|
22
|
-
ohmyapi-0.1.
|
23
|
-
ohmyapi-0.1.
|
24
|
-
ohmyapi-0.1.
|
25
|
-
ohmyapi-0.1.
|
24
|
+
ohmyapi/router.py,sha256=6Exv6sVPVyiIYxxAQbxQhFRX74MKTUPWXIBwC7UZ-ww,82
|
25
|
+
ohmyapi-0.1.23.dist-info/METADATA,sha256=HPmvXFChCOfKmKOn6u6JlcItr_2_UrXk9MoA7aCBl2Y,9727
|
26
|
+
ohmyapi-0.1.23.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
|
27
|
+
ohmyapi-0.1.23.dist-info/entry_points.txt,sha256=wb3lw8-meAlpiv1mqcQ3m25ukL7djagU_w89GkrC37k,43
|
28
|
+
ohmyapi-0.1.23.dist-info/RECORD,,
|
File without changes
|
File without changes
|