mesh-sandbox 0.1.33__py3-none-any.whl → 1.0.1__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.
- mesh_sandbox/__init__.py +1 -1
- mesh_sandbox/common/messaging.py +398 -0
- mesh_sandbox/common/mex_headers.py +1 -1
- mesh_sandbox/conftest.py +2 -1
- mesh_sandbox/dependencies.py +8 -3
- mesh_sandbox/handlers/admin.py +33 -15
- mesh_sandbox/handlers/inbox.py +14 -21
- mesh_sandbox/handlers/lookup.py +6 -8
- mesh_sandbox/handlers/outbox.py +18 -14
- mesh_sandbox/handlers/tracking.py +9 -10
- mesh_sandbox/models/message.py +1 -1
- mesh_sandbox/routers/admin.py +33 -6
- mesh_sandbox/store/base.py +21 -139
- mesh_sandbox/store/canned_store.py +30 -34
- mesh_sandbox/store/file_store.py +22 -22
- mesh_sandbox/store/memory_store.py +22 -148
- mesh_sandbox/test_plugin/example_plugin.py +12 -2
- mesh_sandbox/tests/admin.py +63 -2
- mesh_sandbox/tests/messaging_tests.py +165 -0
- mesh_sandbox/views/admin.py +9 -1
- mesh_sandbox/views/tracking.py +2 -2
- {mesh_sandbox-0.1.33.dist-info → mesh_sandbox-1.0.1.dist-info}/METADATA +1 -1
- {mesh_sandbox-0.1.33.dist-info → mesh_sandbox-1.0.1.dist-info}/RECORD +25 -24
- mesh_sandbox/tests/plugin_manager_tests.py +0 -41
- {mesh_sandbox-0.1.33.dist-info → mesh_sandbox-1.0.1.dist-info}/LICENSE +0 -0
- {mesh_sandbox-0.1.33.dist-info → mesh_sandbox-1.0.1.dist-info}/WHEEL +0 -0
mesh_sandbox/handlers/outbox.py
CHANGED
|
@@ -9,12 +9,13 @@ from fastapi import BackgroundTasks, Depends, HTTPException, Request
|
|
|
9
9
|
from fastapi import status as http_status
|
|
10
10
|
from fastapi.responses import JSONResponse
|
|
11
11
|
|
|
12
|
-
from ..common import
|
|
12
|
+
from ..common import constants, index_of, strtobool
|
|
13
13
|
from ..common.exceptions import MessagingException
|
|
14
14
|
from ..common.fernet import FernetHelper
|
|
15
15
|
from ..common.handler_helpers import get_handler_uri
|
|
16
|
+
from ..common.messaging import Messaging
|
|
16
17
|
from ..common.mex_headers import MexHeaders
|
|
17
|
-
from ..dependencies import
|
|
18
|
+
from ..dependencies import get_fernet, get_logger, get_messaging
|
|
18
19
|
from ..models.mailbox import Mailbox
|
|
19
20
|
from ..models.message import (
|
|
20
21
|
Message,
|
|
@@ -24,7 +25,6 @@ from ..models.message import (
|
|
|
24
25
|
MessageStatus,
|
|
25
26
|
MessageType,
|
|
26
27
|
)
|
|
27
|
-
from ..store.base import Store
|
|
28
28
|
from ..views.outbox import (
|
|
29
29
|
get_rich_outbox_view,
|
|
30
30
|
send_message_response,
|
|
@@ -60,13 +60,11 @@ class OutboxHandler:
|
|
|
60
60
|
# pylint: disable=too-many-arguments
|
|
61
61
|
def __init__(
|
|
62
62
|
self,
|
|
63
|
-
|
|
64
|
-
store: Store = Depends(get_store),
|
|
63
|
+
messaging: Messaging = Depends(get_messaging),
|
|
65
64
|
fernet: FernetHelper = Depends(get_fernet),
|
|
66
65
|
logger: logging.Logger = Depends(get_logger),
|
|
67
66
|
):
|
|
68
|
-
self.
|
|
69
|
-
self.store = store
|
|
67
|
+
self.messaging = messaging
|
|
70
68
|
self.fernet = fernet
|
|
71
69
|
self.logger = logger
|
|
72
70
|
|
|
@@ -96,7 +94,7 @@ class OutboxHandler:
|
|
|
96
94
|
status_code=http_status.HTTP_417_EXPECTATION_FAILED, detail=constants.ERROR_INVALID_HEADER_CHUNKS
|
|
97
95
|
)
|
|
98
96
|
|
|
99
|
-
recipient = await self.
|
|
97
|
+
recipient = await self.messaging.get_mailbox(mex_headers.mex_to)
|
|
100
98
|
if not recipient:
|
|
101
99
|
raise HTTPException(
|
|
102
100
|
status_code=http_status.HTTP_417_EXPECTATION_FAILED, detail=constants.ERROR_UNREGISTERED_RECIPIENT
|
|
@@ -137,7 +135,7 @@ class OutboxHandler:
|
|
|
137
135
|
partner_id=mex_headers.mex_partnerid,
|
|
138
136
|
checksum=mex_headers.mex_content_checksum,
|
|
139
137
|
encrypted=strtobool(mex_headers.mex_content_encrypted),
|
|
140
|
-
|
|
138
|
+
compressed=strtobool(mex_headers.mex_content_compressed),
|
|
141
139
|
),
|
|
142
140
|
)
|
|
143
141
|
|
|
@@ -145,7 +143,7 @@ class OutboxHandler:
|
|
|
145
143
|
if len(body) == 0:
|
|
146
144
|
raise HTTPException(status_code=http_status.HTTP_417_EXPECTATION_FAILED, detail="MissingDataFile")
|
|
147
145
|
|
|
148
|
-
await self.
|
|
146
|
+
await self.messaging.send_message(message=message, body=body, background_tasks=background_tasks)
|
|
149
147
|
|
|
150
148
|
self.logger.info(
|
|
151
149
|
(
|
|
@@ -184,7 +182,7 @@ class OutboxHandler:
|
|
|
184
182
|
detail=constants.ERROR_INVALID_HEADER_CHUNKS,
|
|
185
183
|
message_id=message_id,
|
|
186
184
|
)
|
|
187
|
-
message: Optional[Message] = await self.
|
|
185
|
+
message: Optional[Message] = await self.messaging.get_message(message_id)
|
|
188
186
|
|
|
189
187
|
if not message:
|
|
190
188
|
raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND)
|
|
@@ -205,12 +203,18 @@ class OutboxHandler:
|
|
|
205
203
|
message_id=message_id,
|
|
206
204
|
)
|
|
207
205
|
|
|
208
|
-
|
|
206
|
+
chunk = await request.body()
|
|
207
|
+
|
|
208
|
+
await self.messaging.save_chunk(
|
|
209
|
+
message=message, chunk_number=chunk_number, chunk=chunk, background_tasks=background_tasks
|
|
210
|
+
)
|
|
209
211
|
|
|
210
212
|
if chunk_number < message.total_chunks:
|
|
211
213
|
return upload_chunk_response(message, chunk_number, accepts_api_version)
|
|
212
214
|
|
|
213
|
-
await self.
|
|
215
|
+
file_size = await self.messaging.get_file_size(message)
|
|
216
|
+
|
|
217
|
+
await self.messaging.accept_message(message=message, file_size=file_size, background_tasks=background_tasks)
|
|
214
218
|
|
|
215
219
|
return upload_chunk_response(message, chunk_number, accepts_api_version)
|
|
216
220
|
|
|
@@ -228,7 +232,7 @@ class OutboxHandler:
|
|
|
228
232
|
if continue_from:
|
|
229
233
|
last_key = self.fernet.decode_dict(continue_from)
|
|
230
234
|
|
|
231
|
-
messages: list[Message] = cast(list[Message], await self.
|
|
235
|
+
messages: list[Message] = cast(list[Message], await self.messaging.get_outbox(mailbox.mailbox_id))
|
|
232
236
|
|
|
233
237
|
def message_filter(message: Message) -> bool:
|
|
234
238
|
return message.created_timestamp > from_date
|
|
@@ -4,22 +4,21 @@ from fastapi import Depends, HTTPException
|
|
|
4
4
|
from fastapi import status as http_status
|
|
5
5
|
from starlette.responses import JSONResponse
|
|
6
6
|
|
|
7
|
-
from ..common import MESH_MEDIA_TYPES,
|
|
8
|
-
from ..
|
|
7
|
+
from ..common import MESH_MEDIA_TYPES, exclude_none_json_encoder
|
|
8
|
+
from ..common.messaging import Messaging
|
|
9
|
+
from ..dependencies import get_messaging
|
|
9
10
|
from ..models.mailbox import Mailbox
|
|
10
11
|
from ..models.message import Message
|
|
11
|
-
from ..store.base import Store
|
|
12
12
|
from ..views.tracking import create_tracking_response
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class TrackingHandler:
|
|
16
|
-
def __init__(self,
|
|
17
|
-
self.
|
|
18
|
-
self.store = store
|
|
16
|
+
def __init__(self, messaging: Messaging = Depends(get_messaging)):
|
|
17
|
+
self.messaging = messaging
|
|
19
18
|
|
|
20
19
|
async def tracking_by_message_id(self, sender_mailbox: Mailbox, message_id: str, accepts_api_version: int = 1):
|
|
21
20
|
|
|
22
|
-
message: Optional[Message] = await self.
|
|
21
|
+
message: Optional[Message] = await self.messaging.get_message(message_id)
|
|
23
22
|
|
|
24
23
|
if not message:
|
|
25
24
|
raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND)
|
|
@@ -28,7 +27,7 @@ class TrackingHandler:
|
|
|
28
27
|
# intentionally not a 403 (matching spine)
|
|
29
28
|
raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND)
|
|
30
29
|
|
|
31
|
-
sender_outbox = await self.
|
|
30
|
+
sender_outbox = await self.messaging.get_outbox(sender_mailbox.mailbox_id)
|
|
32
31
|
if message.message_id not in [message.message_id for message in sender_outbox]:
|
|
33
32
|
raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND)
|
|
34
33
|
|
|
@@ -37,7 +36,7 @@ class TrackingHandler:
|
|
|
37
36
|
|
|
38
37
|
async def tracking_by_local_id(self, sender_mailbox: Mailbox, local_id: str):
|
|
39
38
|
|
|
40
|
-
messages: list[Message] = await self.
|
|
39
|
+
messages: list[Message] = await self.messaging.get_by_local_id(sender_mailbox.mailbox_id, local_id)
|
|
41
40
|
|
|
42
41
|
if len(messages) == 0:
|
|
43
42
|
raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND)
|
|
@@ -47,7 +46,7 @@ class TrackingHandler:
|
|
|
47
46
|
|
|
48
47
|
message = messages[0]
|
|
49
48
|
|
|
50
|
-
sender_outbox = await self.
|
|
49
|
+
sender_outbox = await self.messaging.get_outbox(sender_mailbox.mailbox_id)
|
|
51
50
|
if message.message_id not in [message.message_id for message in sender_outbox]:
|
|
52
51
|
raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND)
|
|
53
52
|
|
mesh_sandbox/models/message.py
CHANGED
|
@@ -40,7 +40,7 @@ class MessageMetadata:
|
|
|
40
40
|
partner_id: Optional[str] = field(default=None)
|
|
41
41
|
checksum: Optional[str] = field(default=None)
|
|
42
42
|
encrypted: Optional[bool] = field(default=None)
|
|
43
|
-
|
|
43
|
+
compressed: Optional[bool] = field(default=None)
|
|
44
44
|
etag: Optional[str] = field(default=None)
|
|
45
45
|
last_modified: Optional[str] = field(default=None)
|
|
46
46
|
|
mesh_sandbox/routers/admin.py
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
from fastapi import APIRouter, BackgroundTasks, Depends, status
|
|
1
|
+
from fastapi import APIRouter, BackgroundTasks, Depends, Response, status
|
|
2
2
|
|
|
3
|
-
from ..dependencies import
|
|
3
|
+
from ..dependencies import (
|
|
4
|
+
EnvConfig,
|
|
5
|
+
get_env_config,
|
|
6
|
+
normalise_mailbox_id_path,
|
|
7
|
+
normalise_message_id_path,
|
|
8
|
+
)
|
|
4
9
|
from ..handlers.admin import AdminHandler
|
|
5
|
-
from ..views.admin import
|
|
10
|
+
from ..views.admin import AddMessageEventRequest, CreateReportRequest
|
|
6
11
|
from .request_logging import RequestLoggingRoute
|
|
7
12
|
|
|
8
13
|
router = APIRouter(
|
|
@@ -75,10 +80,32 @@ async def reset_mailbox(
|
|
|
75
80
|
status_code=status.HTTP_200_OK,
|
|
76
81
|
response_model_exclude_none=True,
|
|
77
82
|
)
|
|
78
|
-
async def
|
|
79
|
-
|
|
83
|
+
async def create_report(
|
|
84
|
+
new_report: CreateReportRequest,
|
|
80
85
|
background_tasks: BackgroundTasks,
|
|
81
86
|
handler: AdminHandler = Depends(AdminHandler),
|
|
82
87
|
):
|
|
83
|
-
message = await handler.
|
|
88
|
+
message = await handler.create_report(new_report, background_tasks)
|
|
84
89
|
return {"message_id": message.message_id}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@router.post(
|
|
93
|
+
"/admin/message/{message_id}/event",
|
|
94
|
+
summary=f"appends a status event to a given message, if exists. {TESTING_ONLY}",
|
|
95
|
+
status_code=status.HTTP_200_OK,
|
|
96
|
+
response_model_exclude_none=True,
|
|
97
|
+
)
|
|
98
|
+
@router.post(
|
|
99
|
+
"/messageexchange/message/{message_id}/event",
|
|
100
|
+
summary=f"appends a status event to a given message, if exists. {TESTING_ONLY}",
|
|
101
|
+
status_code=status.HTTP_200_OK,
|
|
102
|
+
response_model_exclude_none=True,
|
|
103
|
+
)
|
|
104
|
+
async def add_message_event(
|
|
105
|
+
new_event: AddMessageEventRequest,
|
|
106
|
+
background_tasks: BackgroundTasks,
|
|
107
|
+
message_id: str = Depends(normalise_message_id_path),
|
|
108
|
+
handler: AdminHandler = Depends(AdminHandler),
|
|
109
|
+
):
|
|
110
|
+
await handler.add_message_event(message_id, new_event, background_tasks)
|
|
111
|
+
return Response()
|
mesh_sandbox/store/base.py
CHANGED
|
@@ -1,173 +1,59 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
|
-
from typing import Callable,
|
|
3
|
+
from typing import Callable, Optional
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
from ..common import EnvConfig, constants, generate_cipher_text
|
|
5
|
+
from ..common import EnvConfig
|
|
8
6
|
from ..models.mailbox import Mailbox
|
|
9
|
-
from ..models.message import Message
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class AuthoriseHeaderParts(NamedTuple):
|
|
13
|
-
scheme: str
|
|
14
|
-
mailbox_id: str
|
|
15
|
-
nonce: str
|
|
16
|
-
nonce_count: str
|
|
17
|
-
timestamp: str
|
|
18
|
-
cipher_text: str
|
|
19
|
-
parts: int
|
|
20
|
-
|
|
21
|
-
def get_reasons_invalid(self) -> list[str]:
|
|
22
|
-
reasons = []
|
|
23
|
-
if self.parts != 5:
|
|
24
|
-
reasons.append(f"invalid num header parts: {self.parts}")
|
|
25
|
-
|
|
26
|
-
if not self.nonce_count.isdigit():
|
|
27
|
-
reasons.append("nonce count is not digits")
|
|
28
|
-
|
|
29
|
-
if self.scheme not in (MESH_AUTH_SCHEME, ""):
|
|
30
|
-
reasons.append("invalid auth scheme or mailbox_id contains a space")
|
|
31
|
-
|
|
32
|
-
if " " in self.mailbox_id:
|
|
33
|
-
reasons.append("mailbox_id contains a space")
|
|
34
|
-
|
|
35
|
-
return reasons
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
_DEFAULT_PARTS_IF_MISSING = ["" for _ in range(5)]
|
|
39
|
-
MESH_AUTH_SCHEME = "NHSMESH"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def try_parse_authorisation_token(auth_token: str) -> Optional[AuthoriseHeaderParts]:
|
|
43
|
-
|
|
44
|
-
if not auth_token:
|
|
45
|
-
return None
|
|
46
|
-
|
|
47
|
-
auth_token = auth_token.strip()
|
|
48
|
-
|
|
49
|
-
scheme = MESH_AUTH_SCHEME if auth_token.upper().startswith(MESH_AUTH_SCHEME) else ""
|
|
50
|
-
|
|
51
|
-
if scheme:
|
|
52
|
-
auth_token = auth_token[len(MESH_AUTH_SCHEME) + 1 :]
|
|
53
|
-
|
|
54
|
-
auth_token_parts = auth_token.split(":")
|
|
55
|
-
|
|
56
|
-
num_parts = len(auth_token_parts)
|
|
57
|
-
auth_token_parts = auth_token_parts + _DEFAULT_PARTS_IF_MISSING
|
|
58
|
-
|
|
59
|
-
header_parts = AuthoriseHeaderParts(
|
|
60
|
-
scheme=scheme,
|
|
61
|
-
mailbox_id=auth_token_parts[0],
|
|
62
|
-
nonce=auth_token_parts[1],
|
|
63
|
-
nonce_count=auth_token_parts[2],
|
|
64
|
-
timestamp=auth_token_parts[3],
|
|
65
|
-
cipher_text=auth_token_parts[4],
|
|
66
|
-
parts=num_parts,
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
return header_parts
|
|
7
|
+
from ..models.message import Message
|
|
70
8
|
|
|
71
9
|
|
|
72
10
|
class Store(ABC):
|
|
73
11
|
|
|
74
|
-
|
|
12
|
+
readonly = True
|
|
75
13
|
|
|
76
14
|
def __init__(self, config: EnvConfig, logger: logging.Logger):
|
|
77
15
|
self.config = config
|
|
78
16
|
self.logger = logger
|
|
79
17
|
|
|
80
|
-
def get_mailboxes_data_dir(self) -> str:
|
|
81
|
-
raise NotImplementedError()
|
|
82
|
-
|
|
83
|
-
async def reset(self):
|
|
84
|
-
raise NotImplementedError()
|
|
85
|
-
|
|
86
|
-
async def reset_mailbox(self, mailbox_id: str):
|
|
87
|
-
raise NotImplementedError()
|
|
88
|
-
|
|
89
18
|
@abstractmethod
|
|
90
19
|
async def get_mailbox(self, mailbox_id: str, accessed: bool = False) -> Optional[Mailbox]:
|
|
91
20
|
pass
|
|
92
21
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return await self.get_mailbox(mailbox_id, accessed=True)
|
|
97
|
-
|
|
98
|
-
authorization = (authorization or "").strip()
|
|
99
|
-
if not authorization:
|
|
100
|
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.ERROR_READING_AUTH_HEADER)
|
|
101
|
-
|
|
102
|
-
header_parts = try_parse_authorisation_token(authorization)
|
|
103
|
-
if not header_parts:
|
|
104
|
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.ERROR_READING_AUTH_HEADER)
|
|
105
|
-
|
|
106
|
-
if header_parts.mailbox_id != mailbox_id:
|
|
107
|
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.ERROR_MAILBOX_TOKEN_MISMATCH)
|
|
108
|
-
|
|
109
|
-
if self.config.auth_mode == "canned":
|
|
110
|
-
|
|
111
|
-
if header_parts.nonce.upper() != "VALID":
|
|
112
|
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.ERROR_INVALID_AUTH_TOKEN)
|
|
113
|
-
return await self.get_mailbox(mailbox_id, accessed=True)
|
|
114
|
-
|
|
115
|
-
if header_parts.get_reasons_invalid():
|
|
116
|
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.ERROR_READING_AUTH_HEADER)
|
|
117
|
-
|
|
118
|
-
mailbox = await self.get_mailbox(mailbox_id, accessed=True)
|
|
119
|
-
|
|
120
|
-
if not mailbox:
|
|
121
|
-
return None
|
|
122
|
-
|
|
123
|
-
cypher_text = generate_cipher_text(
|
|
124
|
-
self.config.shared_key,
|
|
125
|
-
header_parts.mailbox_id,
|
|
126
|
-
mailbox.password,
|
|
127
|
-
header_parts.timestamp,
|
|
128
|
-
header_parts.nonce,
|
|
129
|
-
header_parts.nonce_count,
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
if header_parts.cipher_text != cypher_text:
|
|
133
|
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.ERROR_INVALID_AUTH_TOKEN)
|
|
134
|
-
|
|
135
|
-
return mailbox
|
|
136
|
-
|
|
137
|
-
async def authorise_mailbox(self, mailbox_id: str, authorization: str) -> Optional[Mailbox]:
|
|
138
|
-
|
|
139
|
-
mailbox = await self._validate_auth_token(mailbox_id, authorization)
|
|
140
|
-
|
|
141
|
-
if not mailbox:
|
|
142
|
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.ERROR_NO_MAILBOX_MATCHES)
|
|
22
|
+
@abstractmethod
|
|
23
|
+
async def get_message(self, message_id: str) -> Optional[Message]:
|
|
24
|
+
pass
|
|
143
25
|
|
|
144
|
-
|
|
26
|
+
@abstractmethod
|
|
27
|
+
async def save_message(self, message: Message):
|
|
28
|
+
pass
|
|
145
29
|
|
|
146
30
|
@abstractmethod
|
|
147
|
-
async def
|
|
31
|
+
async def get_chunk(self, message: Message, chunk_number: int) -> Optional[bytes]:
|
|
148
32
|
pass
|
|
149
33
|
|
|
150
34
|
@abstractmethod
|
|
151
|
-
async def
|
|
35
|
+
async def save_chunk(self, message: Message, chunk_number: int, chunk: bytes):
|
|
152
36
|
pass
|
|
153
37
|
|
|
154
38
|
@abstractmethod
|
|
155
|
-
async def
|
|
39
|
+
async def add_to_outbox(self, message: Message):
|
|
156
40
|
pass
|
|
157
41
|
|
|
158
42
|
@abstractmethod
|
|
159
|
-
async def
|
|
43
|
+
async def add_to_inbox(self, message: Message):
|
|
160
44
|
pass
|
|
161
45
|
|
|
162
46
|
@abstractmethod
|
|
163
|
-
async def
|
|
47
|
+
async def get_file_size(self, message: Message) -> int:
|
|
164
48
|
pass
|
|
165
49
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
50
|
+
@abstractmethod
|
|
51
|
+
async def reset(self):
|
|
52
|
+
pass
|
|
169
53
|
|
|
170
|
-
|
|
54
|
+
@abstractmethod
|
|
55
|
+
async def reset_mailbox(self, mailbox_id: str):
|
|
56
|
+
pass
|
|
171
57
|
|
|
172
58
|
@abstractmethod
|
|
173
59
|
async def get_inbox_messages(
|
|
@@ -183,10 +69,6 @@ class Store(ABC):
|
|
|
183
69
|
async def get_by_local_id(self, mailbox_id: str, local_id: str) -> list[Message]:
|
|
184
70
|
pass
|
|
185
71
|
|
|
186
|
-
@abstractmethod
|
|
187
|
-
async def retrieve_chunk(self, message: Message, chunk_number: int) -> Optional[bytes]:
|
|
188
|
-
pass
|
|
189
|
-
|
|
190
72
|
@abstractmethod
|
|
191
73
|
async def lookup_by_ods_code_and_workflow_id(self, ods_code: str, workflow_id: str) -> list[Mailbox]:
|
|
192
74
|
pass
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
import json
|
|
3
2
|
import logging
|
|
4
3
|
import os
|
|
5
|
-
import threading
|
|
6
4
|
from collections import defaultdict
|
|
7
5
|
from datetime import datetime
|
|
8
6
|
from json import JSONDecodeError
|
|
@@ -10,7 +8,6 @@ from typing import Callable, Optional, cast
|
|
|
10
8
|
from weakref import WeakValueDictionary
|
|
11
9
|
|
|
12
10
|
from dateutil.relativedelta import relativedelta
|
|
13
|
-
from fastapi import BackgroundTasks
|
|
14
11
|
|
|
15
12
|
from ..common import EnvConfig
|
|
16
13
|
from ..models.mailbox import Mailbox
|
|
@@ -20,6 +17,10 @@ from .base import Store
|
|
|
20
17
|
from .serialisation import deserialise_model
|
|
21
18
|
|
|
22
19
|
|
|
20
|
+
def _accepted_messages(msg: Message) -> bool:
|
|
21
|
+
return msg.status == MessageStatus.ACCEPTED
|
|
22
|
+
|
|
23
|
+
|
|
23
24
|
class CannedStore(Store):
|
|
24
25
|
"""
|
|
25
26
|
pre canned messages or mailboxes not editable
|
|
@@ -31,8 +32,6 @@ class CannedStore(Store):
|
|
|
31
32
|
self._config = config
|
|
32
33
|
self._canned_data_dir = os.path.join(os.path.dirname(__file__), "data")
|
|
33
34
|
self._mailboxes_data_dir = self.get_mailboxes_data_dir()
|
|
34
|
-
self._sync_lock = threading.Lock()
|
|
35
|
-
self._lock: Optional[asyncio.Lock] = None
|
|
36
35
|
self._filter_expired = filter_expired
|
|
37
36
|
super().__init__(self._config, logger)
|
|
38
37
|
|
|
@@ -54,18 +53,6 @@ class CannedStore(Store):
|
|
|
54
53
|
self._fill_boxes()
|
|
55
54
|
self.messages = cast(dict[str, Message], WeakValueDictionary(self.messages))
|
|
56
55
|
|
|
57
|
-
@property
|
|
58
|
-
def lock(self):
|
|
59
|
-
|
|
60
|
-
if self._lock is not None:
|
|
61
|
-
return self._lock
|
|
62
|
-
|
|
63
|
-
with self._sync_lock:
|
|
64
|
-
if self._lock is not None:
|
|
65
|
-
return self._lock
|
|
66
|
-
self._lock = asyncio.Lock()
|
|
67
|
-
return self._lock
|
|
68
|
-
|
|
69
56
|
def _fill_boxes(self):
|
|
70
57
|
for message in self.messages.values():
|
|
71
58
|
if message.sender.mailbox_id and message.sender.mailbox_id in self.mailboxes:
|
|
@@ -205,25 +192,40 @@ class CannedStore(Store):
|
|
|
205
192
|
if not mailbox:
|
|
206
193
|
return None
|
|
207
194
|
|
|
208
|
-
mailbox.inbox_count = len(await self.
|
|
195
|
+
mailbox.inbox_count = len(await self.get_inbox_messages(mailbox_id, _accepted_messages))
|
|
209
196
|
if accessed:
|
|
210
197
|
mailbox.last_accessed = datetime.utcnow()
|
|
211
198
|
return mailbox
|
|
212
199
|
|
|
213
|
-
async def
|
|
214
|
-
|
|
200
|
+
async def get_message(self, message_id: str) -> Optional[Message]:
|
|
201
|
+
return self.messages.get(message_id)
|
|
215
202
|
|
|
216
|
-
async def
|
|
217
|
-
|
|
203
|
+
async def get_file_size(self, message: Message) -> int:
|
|
204
|
+
return sum(len(chunk or b"") for chunk in self.chunks.get(message.message_id, []))
|
|
218
205
|
|
|
219
|
-
async def
|
|
220
|
-
|
|
206
|
+
async def add_to_outbox(self, message: Message):
|
|
207
|
+
"""does nothing on this readonly store..."""
|
|
221
208
|
|
|
222
|
-
async def
|
|
223
|
-
|
|
209
|
+
async def add_to_inbox(self, message: Message):
|
|
210
|
+
"""does nothing on this readonly store..."""
|
|
224
211
|
|
|
225
|
-
async def
|
|
226
|
-
|
|
212
|
+
async def save_message(self, message: Message):
|
|
213
|
+
raise NotImplementedError()
|
|
214
|
+
|
|
215
|
+
async def get_chunk(self, message: Message, chunk_number: int) -> Optional[bytes]:
|
|
216
|
+
parts = self.chunks.get(message.message_id, [])
|
|
217
|
+
if not parts or len(parts) < chunk_number:
|
|
218
|
+
return None
|
|
219
|
+
return parts[chunk_number - 1]
|
|
220
|
+
|
|
221
|
+
async def save_chunk(self, message: Message, chunk_number: int, chunk: bytes):
|
|
222
|
+
raise NotImplementedError()
|
|
223
|
+
|
|
224
|
+
async def reset(self):
|
|
225
|
+
raise NotImplementedError()
|
|
226
|
+
|
|
227
|
+
async def reset_mailbox(self, mailbox_id: str):
|
|
228
|
+
raise NotImplementedError()
|
|
227
229
|
|
|
228
230
|
async def get_inbox_messages(
|
|
229
231
|
self, mailbox_id: str, predicate: Optional[Callable[[Message], bool]] = None
|
|
@@ -240,12 +242,6 @@ class CannedStore(Store):
|
|
|
240
242
|
async def get_by_local_id(self, mailbox_id: str, local_id: str) -> list[Message]:
|
|
241
243
|
return self.local_ids.get(mailbox_id, {}).get(local_id, [])
|
|
242
244
|
|
|
243
|
-
async def retrieve_chunk(self, message: Message, chunk_number: int) -> Optional[bytes]:
|
|
244
|
-
parts = self.chunks.get(message.message_id, [])
|
|
245
|
-
if not parts or len(parts) < chunk_number:
|
|
246
|
-
return None
|
|
247
|
-
return parts[chunk_number - 1]
|
|
248
|
-
|
|
249
245
|
async def lookup_by_ods_code_and_workflow_id(self, ods_code: str, workflow_id: str) -> list[Mailbox]:
|
|
250
246
|
return self.endpoints.get(f"{ods_code}/{workflow_id}", [])
|
|
251
247
|
|
mesh_sandbox/store/file_store.py
CHANGED
|
@@ -17,16 +17,17 @@ class FileStore(MemoryStore):
|
|
|
17
17
|
def get_mailboxes_data_dir(self) -> str:
|
|
18
18
|
return self._config.mailboxes_dir
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if message.total_chunks < 1:
|
|
23
|
-
return 0
|
|
20
|
+
def message_path(self, message: Message) -> str:
|
|
21
|
+
return os.path.join(self._mailboxes_data_dir, message.recipient.mailbox_id, "in", message.message_id)
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
def chunk_path(self, message: Message, chunk_number: int) -> str:
|
|
24
|
+
return os.path.join(
|
|
25
|
+
self._mailboxes_data_dir, message.recipient.mailbox_id, "in", message.message_id, str(chunk_number)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def _load_chunks(self) -> dict[str, list[Optional[bytes]]]:
|
|
29
|
+
"""overrides canned store default data load"""
|
|
30
|
+
return defaultdict(list)
|
|
30
31
|
|
|
31
32
|
async def save_message(self, message: Message):
|
|
32
33
|
await super().save_message(message)
|
|
@@ -35,18 +36,6 @@ class FileStore(MemoryStore):
|
|
|
35
36
|
with open(message_json_path, "w+", encoding="utf-8") as f:
|
|
36
37
|
json.dump(serialise_model(message), f)
|
|
37
38
|
|
|
38
|
-
def inbox_path(self, mailbox_id: str) -> str:
|
|
39
|
-
return os.path.join(self._mailboxes_data_dir, mailbox_id, "in")
|
|
40
|
-
|
|
41
|
-
def message_path(self, message: Message) -> str:
|
|
42
|
-
return os.path.join(self.inbox_path(message.recipient.mailbox_id), message.message_id)
|
|
43
|
-
|
|
44
|
-
def chunk_path(self, message: Message, chunk_number: int) -> str:
|
|
45
|
-
return os.path.join(self.message_path(message), str(chunk_number))
|
|
46
|
-
|
|
47
|
-
def _load_chunks(self) -> dict[str, list[Optional[bytes]]]:
|
|
48
|
-
return defaultdict(list)
|
|
49
|
-
|
|
50
39
|
async def save_chunk(self, message: Message, chunk_number: int, chunk: Optional[bytes]):
|
|
51
40
|
chunk_path = self.chunk_path(message, chunk_number)
|
|
52
41
|
if chunk is None:
|
|
@@ -59,7 +48,7 @@ class FileStore(MemoryStore):
|
|
|
59
48
|
with open(chunk_path, "wb+") as f:
|
|
60
49
|
f.write(chunk)
|
|
61
50
|
|
|
62
|
-
async def
|
|
51
|
+
async def get_chunk(self, message: Message, chunk_number: int) -> Optional[bytes]:
|
|
63
52
|
|
|
64
53
|
chunk_path = self.chunk_path(message, chunk_number)
|
|
65
54
|
if not os.path.exists(chunk_path):
|
|
@@ -67,3 +56,14 @@ class FileStore(MemoryStore):
|
|
|
67
56
|
|
|
68
57
|
with open(chunk_path, "rb") as f:
|
|
69
58
|
return f.read()
|
|
59
|
+
|
|
60
|
+
async def get_file_size(self, message: Message) -> int:
|
|
61
|
+
size = 0
|
|
62
|
+
if message.total_chunks < 1:
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
message_dir = self.message_path(message)
|
|
66
|
+
for chunk_no in range(message.total_chunks):
|
|
67
|
+
stat = os.stat(f"{message_dir}/{chunk_no+1}")
|
|
68
|
+
size += stat.st_size
|
|
69
|
+
return size
|