Skip to content

Commit 3091f8b

Browse files
committed
Demonstration finite state machine for PEP 694
1 parent 51ffd8f commit 3091f8b

File tree

1 file changed

+220
-0
lines changed

1 file changed

+220
-0
lines changed

warehouse/forklift/state.py

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
#######################################################################################
14+
# This file demonstrates a Finite State Machine for the concepts of the File Upload
15+
# Session and Upload Session defined in PEP 694.
16+
#######################################################################################
17+
18+
import dataclasses
19+
import datetime
20+
21+
from hashlib import sha256
22+
from typing import Any, Protocol
23+
24+
import automat
25+
26+
27+
@dataclasses.dataclass
28+
class FileUploadMechanism:
29+
name: str
30+
requires_processing: bool
31+
32+
33+
@dataclasses.dataclass
34+
class FileUploadSession:
35+
mechanism: FileUploadMechanism
36+
37+
expiration: datetime.datetime
38+
notices: list[str]
39+
mechanism_details: dict[Any, Any]
40+
41+
42+
class FileUploadSessionController(Protocol):
43+
def action_ready(self) -> None:
44+
"The File Upload Session was marked as ready"
45+
46+
def action_cancel(self) -> None:
47+
"The File Upload Session was marked as canceled"
48+
49+
def action_extend(self, seconds: int) -> None:
50+
"The File Upload Session was requested to be extended"
51+
52+
def _process(self) -> None:
53+
"The File Upload Session is processing a ready file upload"
54+
55+
def _complete(self) -> None:
56+
"The File Upload Session is complete"
57+
58+
def _error(self, notice) -> None:
59+
"The File Upload Session encountered an error"
60+
61+
62+
def build_file_upload_session():
63+
64+
builder = automat.TypeMachineBuilder(FileUploadSessionController, FileUploadSession)
65+
66+
pending = builder.state("pending")
67+
processing = builder.state("processing")
68+
complete = builder.state("complete")
69+
error = builder.state("error")
70+
canceled = builder.state("canceled")
71+
72+
@pending.upon(FileUploadSessionController._process).to(processing)
73+
def _process(
74+
controller: FileUploadSessionController, file_upload_session: FileUploadSession
75+
) -> None:
76+
pass
77+
78+
@pending.upon(FileUploadSessionController._complete).to(complete)
79+
def _complete(
80+
controller: FileUploadSessionController, file_upload_session: FileUploadSession
81+
) -> None:
82+
pass
83+
84+
@pending.upon(FileUploadSessionController.action_cancel).to(canceled)
85+
@processing.upon(FileUploadSessionController.action_cancel).to(canceled)
86+
@complete.upon(FileUploadSessionController.action_cancel).to(canceled)
87+
def action_cancel(
88+
controller: FileUploadSessionController, file_upload_session: FileUploadSession
89+
) -> None:
90+
pass
91+
92+
@pending.upon(FileUploadSessionController._error).to(error)
93+
@processing.upon(FileUploadSessionController._error).to(error)
94+
def _error(
95+
controller: FileUploadSessionController,
96+
file_upload_session: FileUploadSession,
97+
notice: str,
98+
) -> None:
99+
file_upload_session.notices.append(notice)
100+
101+
@pending.upon(FileUploadSessionController.action_ready).loop()
102+
def action_ready(
103+
controller: FileUploadSessionController, file_upload_session: FileUploadSession
104+
) -> None:
105+
if file_upload_session.mechanism.requires_processing:
106+
controller._process()
107+
else:
108+
controller._complete()
109+
110+
@pending.upon(FileUploadSessionController.action_extend).loop()
111+
def action_extend(
112+
controller: FileUploadSessionController,
113+
file_upload_session: FileUploadSession,
114+
seconds: int,
115+
) -> None:
116+
if file_upload_session.expiration >= datetime.datetime.now(datetime.UTC):
117+
controller._error("Expired File Upload Sessions cannot be extended")
118+
else:
119+
file_upload_session.expiration = (
120+
file_upload_session.expiration + datetime.timedelta(seconds=seconds)
121+
)
122+
123+
return builder.build()
124+
125+
126+
FileUploadSessionFactory = build_file_upload_session()
127+
128+
129+
@dataclasses.dataclass
130+
class UploadSession:
131+
project: str
132+
version: str
133+
file_upload_sessions: list[FileUploadSession]
134+
135+
expiration: datetime.datetime
136+
notices: list[str]
137+
138+
nonce: str = ""
139+
140+
@property
141+
def can_publish(self):
142+
return True
143+
144+
@property
145+
def session_token(self):
146+
h = sha256()
147+
h.update(self.name.encode())
148+
h.update(self.version.encode())
149+
h.update(self.nonce.encode())
150+
return h.hexdigest()
151+
152+
153+
class UploadSessionController(Protocol):
154+
def action_publish(self) -> None:
155+
"The Upload Session was marked as published"
156+
157+
def action_cancel(self) -> None:
158+
"The Upload Session was marked as canceled"
159+
160+
def action_extend(self, seconds: int) -> None:
161+
"The Upload Session was requested to be extended"
162+
163+
def _publish(self) -> None:
164+
"The Upload Session was published"
165+
166+
def _error(self, notice) -> None:
167+
"The Upload Session encountered an error"
168+
169+
170+
def build_upload_session():
171+
builder = automat.TypeMachineBuilder(UploadSessionController, UploadSession)
172+
173+
pending = builder.state("pending")
174+
published = builder.state("published")
175+
error = builder.state("error")
176+
canceled = builder.state("canceled")
177+
178+
@pending.upon(UploadSessionController.action_publish).loop()
179+
def action_publish(
180+
controller: UploadSessionController, upload_session: UploadSession
181+
):
182+
if upload_session.can_publish:
183+
controller._publish()
184+
else:
185+
controller._error("Upload Session could not be published")
186+
187+
@pending.upon(UploadSessionController.action_cancel).to(canceled)
188+
@error.upon(UploadSessionController.action_cancel).to(canceled)
189+
def action_cancel(
190+
controller: UploadSessionController, upload_session: UploadSession
191+
):
192+
pass
193+
194+
@pending.upon(UploadSessionController._error).to(error)
195+
def _error(
196+
controller: UploadSessionController, upload_session: UploadSession, notice: str
197+
):
198+
upload_session.notices.append(notice)
199+
200+
@pending.upon(UploadSessionController._publish).to(published)
201+
def _publish(controller: UploadSessionController, upload_session: UploadSession):
202+
pass
203+
204+
@pending.upon(UploadSessionController.action_extend).loop()
205+
def action_extend(
206+
controller: UploadSessionController,
207+
upload_session: UploadSession,
208+
seconds: int,
209+
) -> None:
210+
if upload_session.expiration >= datetime.datetime.now(datetime.UTC):
211+
controller._error("Expired Upload Sessions cannot be extended")
212+
else:
213+
upload_session.expiration = upload_session.expiration + datetime.timedelta(
214+
seconds=seconds
215+
)
216+
217+
return builder.build()
218+
219+
220+
UploadSessionFactory = build_upload_session()

0 commit comments

Comments
 (0)