evserver 0.1.0__tar.gz → 0.3.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.
- evserver-0.3.0/PKG-INFO +65 -0
- evserver-0.3.0/README.md +52 -0
- evserver-0.3.0/pyproject.toml +44 -0
- evserver-0.1.0/pyproject.toml → evserver-0.3.0/pyproject.toml.orig +1 -2
- evserver-0.3.0/src/evserver/__init__.py +351 -0
- evserver-0.3.0/src/evserver/stores.py +33 -0
- evserver-0.3.0/src/evserver/types.py +70 -0
- evserver-0.1.0/PKG-INFO +0 -156
- evserver-0.1.0/README.md +0 -142
- evserver-0.1.0/src/evserver/__init__.py +0 -308
- evserver-0.1.0/src/evserver/stores.py +0 -152
- evserver-0.1.0/src/evserver/types.py +0 -67
evserver-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: evserver
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Wannes Vantorre
|
|
6
|
+
Author-email: Wannes Vantorre <vantorrewannes@gmail.com>
|
|
7
|
+
Requires-Dist: aiofiles>=25.1.0
|
|
8
|
+
Requires-Dist: dill>=0.4.1
|
|
9
|
+
Requires-Dist: fastapi[standard]>=0.140.7
|
|
10
|
+
Requires-Dist: pydantic>=2.13.4
|
|
11
|
+
Requires-Python: >=3.14
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Easysnapshot
|
|
15
|
+
|
|
16
|
+
### Goals
|
|
17
|
+
|
|
18
|
+
- **Many Archives:** Store in many archives at once.
|
|
19
|
+
- **Emergent Complexity:** Small implementation surface with reasonable usage requirements.
|
|
20
|
+
|
|
21
|
+
## Client
|
|
22
|
+
|
|
23
|
+
Clients gather and send information to archive endpoints.
|
|
24
|
+
|
|
25
|
+
### Commands
|
|
26
|
+
|
|
27
|
+
- All commands accept `--help` / `-h`.
|
|
28
|
+
- All commands accept `--verbose` / `-v`.
|
|
29
|
+
- **`ev archive register <archive:URL> [user:ID]`:** Claim a specific User ID on the given archive. User ID is random if omitted.
|
|
30
|
+
- **`ev archive unregister <archive:URL> <user:ID>`:** Requests the archive to forget this User ID.
|
|
31
|
+
- **`ev archive login <archive:URL> <user:ID> [workspace:PATH]`:** Sets this combination of Archive URL + User ID as active for this workspace.
|
|
32
|
+
- **`ev archive logout <archive:URL> <user:ID> [workspace:PATH]`:** Sets this combination of Archive URL + User ID as unactive for this workspace.
|
|
33
|
+
- **`ev save <--note <note:TEXT> | -n <note:TEXT> [workspace:PATH]`:** Save the workspace's current state as a new Snapshot, with the note when provided.
|
|
34
|
+
- **`ev list [workspace:PATH]`:** List the Workspace's Snapshots. Also showing notes where set.
|
|
35
|
+
- **`ev clone <source:PATH> <target:PATH> [--snapshot <snapshot:ID> | -s <snapshot:ID>]`:** Clone the source Workspace to the target path, which may not exist yet. Clone the current workspace state if omitted.
|
|
36
|
+
- **`ev forget {--snapshot <snapshot:ID> | -v <snapshot:ID> | --all | -a} [workspace:PATH]`:** Forget the specified snapshot(s) for the given Workspace.
|
|
37
|
+
|
|
38
|
+
## Archive
|
|
39
|
+
|
|
40
|
+
Clients recieve and save information from clients through their URL.
|
|
41
|
+
|
|
42
|
+
### Types
|
|
43
|
+
|
|
44
|
+
Five distinct types, forming one chain from workspace to content:
|
|
45
|
+
|
|
46
|
+
| Type | Contents |
|
|
47
|
+
| ----------- | --------------------------------------- |
|
|
48
|
+
| `user` | Metadata about the user account |
|
|
49
|
+
| `workspace` | An array of snapshot hashes |
|
|
50
|
+
| `snapshot` | A manifest hash and an optional note |
|
|
51
|
+
| `manifest` | An array of reference hashes |
|
|
52
|
+
| `reference` | A relative file path and a content hash |
|
|
53
|
+
| `content` | Raw byte data |
|
|
54
|
+
|
|
55
|
+
### Endpoints
|
|
56
|
+
|
|
57
|
+
- **`HEAD /type/<type:ID>`:** Check whether the Type ID already exists.
|
|
58
|
+
- **`GET /type/<type:ID>`:** Returns an object with the Type's data.
|
|
59
|
+
- **`PUT /type/<type:ID>`:** Sets the object with the Type's data.
|
|
60
|
+
- **`DELETE /type/<type:ID>`:** Forget the given Type ID.
|
|
61
|
+
|
|
62
|
+
### Invariants
|
|
63
|
+
|
|
64
|
+
- All in workspace configuration, will be done under `./ev` in the workspace root.
|
|
65
|
+
- Archive URL + User ID login configurations are unique per workspace.
|
evserver-0.3.0/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Easysnapshot
|
|
2
|
+
|
|
3
|
+
### Goals
|
|
4
|
+
|
|
5
|
+
- **Many Archives:** Store in many archives at once.
|
|
6
|
+
- **Emergent Complexity:** Small implementation surface with reasonable usage requirements.
|
|
7
|
+
|
|
8
|
+
## Client
|
|
9
|
+
|
|
10
|
+
Clients gather and send information to archive endpoints.
|
|
11
|
+
|
|
12
|
+
### Commands
|
|
13
|
+
|
|
14
|
+
- All commands accept `--help` / `-h`.
|
|
15
|
+
- All commands accept `--verbose` / `-v`.
|
|
16
|
+
- **`ev archive register <archive:URL> [user:ID]`:** Claim a specific User ID on the given archive. User ID is random if omitted.
|
|
17
|
+
- **`ev archive unregister <archive:URL> <user:ID>`:** Requests the archive to forget this User ID.
|
|
18
|
+
- **`ev archive login <archive:URL> <user:ID> [workspace:PATH]`:** Sets this combination of Archive URL + User ID as active for this workspace.
|
|
19
|
+
- **`ev archive logout <archive:URL> <user:ID> [workspace:PATH]`:** Sets this combination of Archive URL + User ID as unactive for this workspace.
|
|
20
|
+
- **`ev save <--note <note:TEXT> | -n <note:TEXT> [workspace:PATH]`:** Save the workspace's current state as a new Snapshot, with the note when provided.
|
|
21
|
+
- **`ev list [workspace:PATH]`:** List the Workspace's Snapshots. Also showing notes where set.
|
|
22
|
+
- **`ev clone <source:PATH> <target:PATH> [--snapshot <snapshot:ID> | -s <snapshot:ID>]`:** Clone the source Workspace to the target path, which may not exist yet. Clone the current workspace state if omitted.
|
|
23
|
+
- **`ev forget {--snapshot <snapshot:ID> | -v <snapshot:ID> | --all | -a} [workspace:PATH]`:** Forget the specified snapshot(s) for the given Workspace.
|
|
24
|
+
|
|
25
|
+
## Archive
|
|
26
|
+
|
|
27
|
+
Clients recieve and save information from clients through their URL.
|
|
28
|
+
|
|
29
|
+
### Types
|
|
30
|
+
|
|
31
|
+
Five distinct types, forming one chain from workspace to content:
|
|
32
|
+
|
|
33
|
+
| Type | Contents |
|
|
34
|
+
| ----------- | --------------------------------------- |
|
|
35
|
+
| `user` | Metadata about the user account |
|
|
36
|
+
| `workspace` | An array of snapshot hashes |
|
|
37
|
+
| `snapshot` | A manifest hash and an optional note |
|
|
38
|
+
| `manifest` | An array of reference hashes |
|
|
39
|
+
| `reference` | A relative file path and a content hash |
|
|
40
|
+
| `content` | Raw byte data |
|
|
41
|
+
|
|
42
|
+
### Endpoints
|
|
43
|
+
|
|
44
|
+
- **`HEAD /type/<type:ID>`:** Check whether the Type ID already exists.
|
|
45
|
+
- **`GET /type/<type:ID>`:** Returns an object with the Type's data.
|
|
46
|
+
- **`PUT /type/<type:ID>`:** Sets the object with the Type's data.
|
|
47
|
+
- **`DELETE /type/<type:ID>`:** Forget the given Type ID.
|
|
48
|
+
|
|
49
|
+
### Invariants
|
|
50
|
+
|
|
51
|
+
- All in workspace configuration, will be done under `./ev` in the workspace root.
|
|
52
|
+
- Archive URL + User ID login configurations are unique per workspace.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "evserver"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"aiofiles>=25.1.0",
|
|
9
|
+
"dill>=0.4.1",
|
|
10
|
+
"fastapi[standard]>=0.140.7",
|
|
11
|
+
"pydantic>=2.13.4",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[project.authors]]
|
|
15
|
+
name = "Wannes Vantorre"
|
|
16
|
+
email = "vantorrewannes@gmail.com"
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
evserver = "evserver:main"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["uv_build>=0.11.32,<0.12.0"]
|
|
23
|
+
build-backend = "uv_build"
|
|
24
|
+
|
|
25
|
+
[tool.ruff.lint]
|
|
26
|
+
select = ["ALL"]
|
|
27
|
+
ignore = [
|
|
28
|
+
"D203",
|
|
29
|
+
"D213",
|
|
30
|
+
"COM812",
|
|
31
|
+
"ISC001",
|
|
32
|
+
"CPY001",
|
|
33
|
+
"D419",
|
|
34
|
+
"D100",
|
|
35
|
+
"D101",
|
|
36
|
+
"D102",
|
|
37
|
+
"D103",
|
|
38
|
+
"D104",
|
|
39
|
+
"D107",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint.per-file-ignores]
|
|
43
|
+
"tests/**" = ["S101"]
|
|
44
|
+
"tests/unit/**" = ["SLF001"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "evserver"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Add your description here"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -9,7 +9,6 @@ authors = [
|
|
|
9
9
|
requires-python = ">=3.14"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"aiofiles>=25.1.0",
|
|
12
|
-
"blake3>=1.0.9",
|
|
13
12
|
"dill>=0.4.1",
|
|
14
13
|
"fastapi[standard]>=0.140.7",
|
|
15
14
|
"pydantic>=2.13.4",
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator # noqa: TC003
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, TypedDict
|
|
5
|
+
|
|
6
|
+
import dill
|
|
7
|
+
import uvicorn
|
|
8
|
+
from fastapi import Depends, FastAPI, HTTPException, Request, Response
|
|
9
|
+
|
|
10
|
+
from evserver.stores import DirectoryStore
|
|
11
|
+
from evserver.types import (
|
|
12
|
+
Content,
|
|
13
|
+
ContentId,
|
|
14
|
+
Manifest,
|
|
15
|
+
ManifestId,
|
|
16
|
+
Reference,
|
|
17
|
+
ReferenceId,
|
|
18
|
+
Snapshot,
|
|
19
|
+
SnapshotId,
|
|
20
|
+
User,
|
|
21
|
+
UserId,
|
|
22
|
+
Workspace,
|
|
23
|
+
WorkspaceId,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class State(TypedDict):
|
|
28
|
+
user_store: DirectoryStore[UserId, User]
|
|
29
|
+
workspace_store: DirectoryStore[WorkspaceId, Workspace]
|
|
30
|
+
snapshot_store: DirectoryStore[SnapshotId, Snapshot]
|
|
31
|
+
manifest_store: DirectoryStore[ManifestId, Manifest]
|
|
32
|
+
reference_store: DirectoryStore[ReferenceId, Reference]
|
|
33
|
+
content_store: DirectoryStore[ContentId, Content]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@asynccontextmanager
|
|
37
|
+
async def lifespan(_: FastAPI) -> AsyncGenerator[State]:
|
|
38
|
+
root_path = Path("data")
|
|
39
|
+
user_store = DirectoryStore[UserId, User](root_path / "users")
|
|
40
|
+
workspace_store = DirectoryStore[WorkspaceId, Workspace](root_path / "workspaces")
|
|
41
|
+
snapshot_store = DirectoryStore[SnapshotId, Snapshot](root_path / "snapshots")
|
|
42
|
+
manifest_store = DirectoryStore[ManifestId, Manifest](root_path / "manifests")
|
|
43
|
+
reference_store = DirectoryStore[ReferenceId, Reference](root_path / "references")
|
|
44
|
+
content_store = DirectoryStore[ContentId, Content](root_path / "contents")
|
|
45
|
+
yield State(
|
|
46
|
+
user_store=user_store,
|
|
47
|
+
workspace_store=workspace_store,
|
|
48
|
+
snapshot_store=snapshot_store,
|
|
49
|
+
manifest_store=manifest_store,
|
|
50
|
+
reference_store=reference_store,
|
|
51
|
+
content_store=content_store,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
application = FastAPI(lifespan=lifespan)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_user_store(request: Request) -> DirectoryStore[UserId, User]:
|
|
59
|
+
return request.state.user_store
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_workspace_store(request: Request) -> DirectoryStore[WorkspaceId, Workspace]:
|
|
63
|
+
return request.state.workspace_store
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_snapshot_store(request: Request) -> DirectoryStore[SnapshotId, Snapshot]:
|
|
67
|
+
return request.state.snapshot_store
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_manifest_store(request: Request) -> DirectoryStore[ManifestId, Manifest]:
|
|
71
|
+
return request.state.manifest_store
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_reference_store(request: Request) -> DirectoryStore[ReferenceId, Reference]:
|
|
75
|
+
return request.state.reference_store
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_content_store(request: Request) -> DirectoryStore[ContentId, Content]:
|
|
79
|
+
return request.state.content_store
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
UserStoreDependency = Annotated[DirectoryStore[UserId, User], Depends(get_user_store)]
|
|
83
|
+
WorkspaceStoreDependency = Annotated[
|
|
84
|
+
DirectoryStore[WorkspaceId, Workspace], Depends(get_workspace_store)
|
|
85
|
+
]
|
|
86
|
+
SnapshotDependency = Annotated[
|
|
87
|
+
DirectoryStore[SnapshotId, Snapshot], Depends(get_snapshot_store)
|
|
88
|
+
]
|
|
89
|
+
ManifestStoreDependency = Annotated[
|
|
90
|
+
DirectoryStore[ManifestId, Manifest], Depends(get_manifest_store)
|
|
91
|
+
]
|
|
92
|
+
ReferenceStoreDependency = Annotated[
|
|
93
|
+
DirectoryStore[ReferenceId, Reference], Depends(get_reference_store)
|
|
94
|
+
]
|
|
95
|
+
ContentStoreDependency = Annotated[
|
|
96
|
+
DirectoryStore[ContentId, Content], Depends(get_content_store)
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@application.head(
|
|
101
|
+
"/user/{user_id}",
|
|
102
|
+
)
|
|
103
|
+
async def head_user(user_id: UserId, user_store: UserStoreDependency) -> None:
|
|
104
|
+
if not await user_store.contains(user_id):
|
|
105
|
+
raise HTTPException(404)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@application.get(
|
|
109
|
+
"/user/{user_id}",
|
|
110
|
+
)
|
|
111
|
+
async def get_user(user_id: UserId, user_store: UserStoreDependency) -> Response:
|
|
112
|
+
if await user_store.contains(user_id):
|
|
113
|
+
return Response(dill.dumps(await user_store.get(user_id)))
|
|
114
|
+
raise HTTPException(404)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@application.put(
|
|
118
|
+
"/user/{user_id}",
|
|
119
|
+
)
|
|
120
|
+
async def put_user(
|
|
121
|
+
user_id: UserId, request: Request, user_store: UserStoreDependency
|
|
122
|
+
) -> None:
|
|
123
|
+
user: User = dill.loads(await request.body()) # noqa: S301
|
|
124
|
+
await user_store.set(user_id, user)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@application.delete(
|
|
128
|
+
"/user/{user_id}",
|
|
129
|
+
)
|
|
130
|
+
async def delete_user(user_id: UserId, user_store: UserStoreDependency) -> None:
|
|
131
|
+
if user_store.contains(user_id):
|
|
132
|
+
await user_store.delete(user_id)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@application.head(
|
|
136
|
+
"/workspace/{workspace_id}",
|
|
137
|
+
)
|
|
138
|
+
async def head_workspace(
|
|
139
|
+
workspace_id: WorkspaceId, workspace_store: WorkspaceStoreDependency
|
|
140
|
+
) -> None:
|
|
141
|
+
if not await workspace_store.contains(workspace_id):
|
|
142
|
+
raise HTTPException(404)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@application.get(
|
|
146
|
+
"/workspace/{workspace_id}",
|
|
147
|
+
)
|
|
148
|
+
async def get_workspace(
|
|
149
|
+
workspace_id: WorkspaceId, workspace_store: WorkspaceStoreDependency
|
|
150
|
+
) -> Response:
|
|
151
|
+
if await workspace_store.contains(workspace_id):
|
|
152
|
+
return Response(dill.dumps(await workspace_store.get(workspace_id)))
|
|
153
|
+
raise HTTPException(404)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@application.put(
|
|
157
|
+
"/workspace/{workspace_id}",
|
|
158
|
+
)
|
|
159
|
+
async def put_workspace(
|
|
160
|
+
workspace_id: WorkspaceId,
|
|
161
|
+
request: Request,
|
|
162
|
+
workspace_store: WorkspaceStoreDependency,
|
|
163
|
+
) -> None:
|
|
164
|
+
workspace: Workspace = dill.loads(await request.body()) # noqa: S301
|
|
165
|
+
await workspace_store.set(workspace_id, workspace)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@application.delete(
|
|
169
|
+
"/workspace/{workspace_id}",
|
|
170
|
+
)
|
|
171
|
+
async def delete_workspace(
|
|
172
|
+
workspace_id: WorkspaceId, workspace_store: WorkspaceStoreDependency
|
|
173
|
+
) -> None:
|
|
174
|
+
if workspace_store.contains(workspace_id):
|
|
175
|
+
await workspace_store.delete(workspace_id)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@application.head(
|
|
179
|
+
"/snapshot/{snapshot_id}",
|
|
180
|
+
)
|
|
181
|
+
async def head_snapshot(
|
|
182
|
+
snapshot_id: SnapshotId, snapshot_store: SnapshotDependency
|
|
183
|
+
) -> None:
|
|
184
|
+
if not await snapshot_store.contains(snapshot_id):
|
|
185
|
+
raise HTTPException(404)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@application.get(
|
|
189
|
+
"/snapshot/{snapshot_id}",
|
|
190
|
+
)
|
|
191
|
+
async def get_snapshot(
|
|
192
|
+
snapshot_id: SnapshotId, snapshot_store: SnapshotDependency
|
|
193
|
+
) -> Response:
|
|
194
|
+
if await snapshot_store.contains(snapshot_id):
|
|
195
|
+
return Response(dill.dumps(await snapshot_store.get(snapshot_id)))
|
|
196
|
+
raise HTTPException(404)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@application.put(
|
|
200
|
+
"/snapshot/{snapshot_id}",
|
|
201
|
+
)
|
|
202
|
+
async def put_snapshot(
|
|
203
|
+
snapshot_id: SnapshotId,
|
|
204
|
+
request: Request,
|
|
205
|
+
snapshot_store: SnapshotDependency,
|
|
206
|
+
) -> None:
|
|
207
|
+
snapshot: Snapshot = dill.loads(await request.body()) # noqa: S301
|
|
208
|
+
await snapshot_store.set(snapshot_id, snapshot)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@application.delete(
|
|
212
|
+
"/snapshot/{snapshot_id}",
|
|
213
|
+
)
|
|
214
|
+
async def delete_snapshot(
|
|
215
|
+
snapshot_id: SnapshotId, snapshot_store: SnapshotDependency
|
|
216
|
+
) -> None:
|
|
217
|
+
if snapshot_store.contains(snapshot_id):
|
|
218
|
+
await snapshot_store.delete(snapshot_id)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@application.head(
|
|
222
|
+
"/manifest/{manifest_id}",
|
|
223
|
+
)
|
|
224
|
+
async def head_manifest(
|
|
225
|
+
manifest_id: ManifestId, manifest_store: ManifestStoreDependency
|
|
226
|
+
) -> None:
|
|
227
|
+
if not await manifest_store.contains(manifest_id):
|
|
228
|
+
raise HTTPException(404)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@application.get(
|
|
232
|
+
"/manifest/{manifest_id}",
|
|
233
|
+
)
|
|
234
|
+
async def get_manifest(
|
|
235
|
+
manifest_id: ManifestId, manifest_store: ManifestStoreDependency
|
|
236
|
+
) -> Response:
|
|
237
|
+
if await manifest_store.contains(manifest_id):
|
|
238
|
+
return Response(dill.dumps(await manifest_store.get(manifest_id)))
|
|
239
|
+
raise HTTPException(404)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@application.put(
|
|
243
|
+
"/manifest/{manifest_id}",
|
|
244
|
+
)
|
|
245
|
+
async def put_manifest(
|
|
246
|
+
manifest_id: ManifestId,
|
|
247
|
+
request: Request,
|
|
248
|
+
manifest_store: ManifestStoreDependency,
|
|
249
|
+
) -> None:
|
|
250
|
+
manifest: Manifest = dill.loads(await request.body()) # noqa: S301
|
|
251
|
+
await manifest_store.set(manifest_id, manifest)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@application.delete(
|
|
255
|
+
"/manifest/{manifest_id}",
|
|
256
|
+
)
|
|
257
|
+
async def delete_manifest(
|
|
258
|
+
manifest_id: ManifestId, manifest_store: ManifestStoreDependency
|
|
259
|
+
) -> None:
|
|
260
|
+
if manifest_store.contains(manifest_id):
|
|
261
|
+
await manifest_store.delete(manifest_id)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@application.head(
|
|
265
|
+
"/reference/{reference_id}",
|
|
266
|
+
)
|
|
267
|
+
async def head_reference(
|
|
268
|
+
reference_id: ReferenceId, reference_store: ReferenceStoreDependency
|
|
269
|
+
) -> None:
|
|
270
|
+
if not await reference_store.contains(reference_id):
|
|
271
|
+
raise HTTPException(404)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@application.get(
|
|
275
|
+
"/reference/{reference_id}",
|
|
276
|
+
)
|
|
277
|
+
async def get_reference(
|
|
278
|
+
reference_id: ReferenceId, reference_store: ReferenceStoreDependency
|
|
279
|
+
) -> Response:
|
|
280
|
+
if await reference_store.contains(reference_id):
|
|
281
|
+
return Response(dill.dumps(await reference_store.get(reference_id)))
|
|
282
|
+
raise HTTPException(404)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@application.put(
|
|
286
|
+
"/reference/{reference_id}",
|
|
287
|
+
)
|
|
288
|
+
async def put_reference(
|
|
289
|
+
reference_id: ReferenceId,
|
|
290
|
+
request: Request,
|
|
291
|
+
reference_store: ReferenceStoreDependency,
|
|
292
|
+
) -> None:
|
|
293
|
+
reference: Reference = dill.loads(await request.body()) # noqa: S301
|
|
294
|
+
await reference_store.set(reference_id, reference)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@application.delete(
|
|
298
|
+
"/reference/{reference_id}",
|
|
299
|
+
)
|
|
300
|
+
async def delete_reference(
|
|
301
|
+
reference_id: ReferenceId, reference_store: ReferenceStoreDependency
|
|
302
|
+
) -> None:
|
|
303
|
+
if reference_store.contains(reference_id):
|
|
304
|
+
await reference_store.delete(reference_id)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@application.head(
|
|
308
|
+
"/content/{content_id}",
|
|
309
|
+
)
|
|
310
|
+
async def head_content(
|
|
311
|
+
content_id: ContentId, content_store: ContentStoreDependency
|
|
312
|
+
) -> None:
|
|
313
|
+
if not await content_store.contains(content_id):
|
|
314
|
+
raise HTTPException(404)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@application.get(
|
|
318
|
+
"/content/{content_id}",
|
|
319
|
+
)
|
|
320
|
+
async def get_content(
|
|
321
|
+
content_id: ContentId, content_store: ContentStoreDependency
|
|
322
|
+
) -> Response:
|
|
323
|
+
if await content_store.contains(content_id):
|
|
324
|
+
return Response(dill.dumps(await content_store.get(content_id)))
|
|
325
|
+
raise HTTPException(404)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@application.put(
|
|
329
|
+
"/content/{content_id}",
|
|
330
|
+
)
|
|
331
|
+
async def put_content(
|
|
332
|
+
content_id: ContentId,
|
|
333
|
+
request: Request,
|
|
334
|
+
content_store: ContentStoreDependency,
|
|
335
|
+
) -> None:
|
|
336
|
+
content: Content = dill.loads(await request.body()) # noqa: S301
|
|
337
|
+
await content_store.set(content_id, content)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@application.delete(
|
|
341
|
+
"/content/{content_id}",
|
|
342
|
+
)
|
|
343
|
+
async def delete_content(
|
|
344
|
+
content_id: ContentId, content_store: ContentStoreDependency
|
|
345
|
+
) -> None:
|
|
346
|
+
if content_store.contains(content_id):
|
|
347
|
+
await content_store.delete(content_id)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def main() -> None:
|
|
351
|
+
uvicorn.run("evserver:application")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from pathlib import Path # noqa: TC003
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import dill
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from evserver.types import Id
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DirectoryStore[K, V]:
|
|
11
|
+
def __init__(self, root: Path) -> None:
|
|
12
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
self.root = root
|
|
14
|
+
|
|
15
|
+
def _path(self, key: Id) -> Path:
|
|
16
|
+
return self.root / key
|
|
17
|
+
|
|
18
|
+
async def contains(self, key: Id) -> bool:
|
|
19
|
+
return self._path(key).exists()
|
|
20
|
+
|
|
21
|
+
async def set(self, key: Id, value: V) -> None:
|
|
22
|
+
path = self._path(key)
|
|
23
|
+
with path.open("wb") as f:
|
|
24
|
+
dill.dump(value, f)
|
|
25
|
+
|
|
26
|
+
async def get(self, key: Id) -> V:
|
|
27
|
+
path = self._path(key)
|
|
28
|
+
with path.open("rb") as f:
|
|
29
|
+
return dill.load(f) # noqa: S301
|
|
30
|
+
|
|
31
|
+
async def delete(self, key: Id) -> None:
|
|
32
|
+
path = self._path(key)
|
|
33
|
+
path.unlink()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from hashlib import sha3_512
|
|
2
|
+
from pathlib import Path # noqa: TC003
|
|
3
|
+
|
|
4
|
+
from pydantic.dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
type Id = str
|
|
7
|
+
UserId = Id
|
|
8
|
+
WorkspaceId = Id
|
|
9
|
+
SnapshotId = Id
|
|
10
|
+
ManifestId = Id
|
|
11
|
+
ReferenceId = Id
|
|
12
|
+
ContentId = Id
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class User:
|
|
17
|
+
id: UserId
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class Content:
|
|
22
|
+
data: bytes
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def id(self) -> ContentId:
|
|
26
|
+
return sha3_512(self.data).hexdigest()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(slots=True)
|
|
30
|
+
class Reference:
|
|
31
|
+
file_path: Path
|
|
32
|
+
content_id: ContentId
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def id(self) -> ReferenceId:
|
|
36
|
+
return sha3_512(
|
|
37
|
+
str(self.file_path).encode() + self.content_id.encode()
|
|
38
|
+
).hexdigest()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(slots=True)
|
|
42
|
+
class Manifest:
|
|
43
|
+
reference_ids: set[ReferenceId]
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def id(self) -> ManifestId:
|
|
47
|
+
manifest_id = sha3_512()
|
|
48
|
+
for reference_id in self.reference_ids:
|
|
49
|
+
manifest_id.update(reference_id.encode())
|
|
50
|
+
return manifest_id.hexdigest()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(slots=True)
|
|
54
|
+
class Snapshot:
|
|
55
|
+
manifest_id: ManifestId
|
|
56
|
+
note: str | None
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def id(self) -> SnapshotId:
|
|
60
|
+
snapshot_id = sha3_512()
|
|
61
|
+
snapshot_id.update(self.manifest_id.encode())
|
|
62
|
+
if self.note:
|
|
63
|
+
snapshot_id.update(self.note.encode())
|
|
64
|
+
return snapshot_id.hexdigest()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(slots=True)
|
|
68
|
+
class Workspace:
|
|
69
|
+
snapshot_ids: set[SnapshotId]
|
|
70
|
+
id: WorkspaceId
|