digitalkin 0.2.21__py3-none-any.whl → 0.2.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.
- digitalkin/__version__.py +1 -1
- digitalkin/grpc_servers/module_server.py +6 -8
- digitalkin/grpc_servers/module_servicer.py +14 -5
- digitalkin/grpc_servers/utils/grpc_client_wrapper.py +4 -4
- digitalkin/grpc_servers/utils/models.py +15 -1
- digitalkin/logger.py +66 -19
- digitalkin/services/filesystem/grpc_filesystem.py +6 -1
- {digitalkin-0.2.21.dist-info → digitalkin-0.2.23.dist-info}/METADATA +1 -1
- {digitalkin-0.2.21.dist-info → digitalkin-0.2.23.dist-info}/RECORD +12 -12
- {digitalkin-0.2.21.dist-info → digitalkin-0.2.23.dist-info}/WHEEL +0 -0
- {digitalkin-0.2.21.dist-info → digitalkin-0.2.23.dist-info}/licenses/LICENSE +0 -0
- {digitalkin-0.2.21.dist-info → digitalkin-0.2.23.dist-info}/top_level.txt +0 -0
digitalkin/__version__.py
CHANGED
|
@@ -79,10 +79,10 @@ class ModuleServer(BaseServer):
|
|
|
79
79
|
|
|
80
80
|
def start(self) -> None:
|
|
81
81
|
"""Start the module server and register with the registry if configured."""
|
|
82
|
-
logger.info(self.server_config)
|
|
82
|
+
logger.info("Starting module server",extra={"server_config": self.server_config})
|
|
83
83
|
super().start()
|
|
84
84
|
|
|
85
|
-
logger.
|
|
85
|
+
logger.debug("Starting module server",extra={"server_config": self.server_config})
|
|
86
86
|
# If a registry address is provided, register the module
|
|
87
87
|
if self.server_config.registry_address:
|
|
88
88
|
try:
|
|
@@ -91,17 +91,15 @@ class ModuleServer(BaseServer):
|
|
|
91
91
|
logger.exception("Failed to register with registry")
|
|
92
92
|
|
|
93
93
|
if self.module_servicer is not None:
|
|
94
|
-
logger.
|
|
95
|
-
"Setup post init started
|
|
94
|
+
logger.debug(
|
|
95
|
+
"Setup post init started",extra={"client_config": self.client_config}
|
|
96
96
|
)
|
|
97
97
|
self.module_servicer.setup.__post_init__(self.client_config)
|
|
98
98
|
|
|
99
99
|
async def start_async(self) -> None:
|
|
100
100
|
"""Start the module server and register with the registry if configured."""
|
|
101
|
-
logger.info(self.server_config)
|
|
101
|
+
logger.info("Starting module server",extra={"server_config": self.server_config})
|
|
102
102
|
await super().start_async()
|
|
103
|
-
|
|
104
|
-
logger.info(self.server_config)
|
|
105
103
|
# If a registry address is provided, register the module
|
|
106
104
|
if self.server_config.registry_address:
|
|
107
105
|
try:
|
|
@@ -111,7 +109,7 @@ class ModuleServer(BaseServer):
|
|
|
111
109
|
|
|
112
110
|
if self.module_servicer is not None:
|
|
113
111
|
logger.info(
|
|
114
|
-
"Setup post init started
|
|
112
|
+
"Setup post init started",extra={"client_config": self.client_config}
|
|
115
113
|
)
|
|
116
114
|
await self.module_servicer.job_manager._start()
|
|
117
115
|
self.module_servicer.setup.__post_init__(self.client_config)
|
|
@@ -99,9 +99,14 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
|
|
|
99
99
|
Raises:
|
|
100
100
|
ServicerError: if the setup data is not returned or job creation fails.
|
|
101
101
|
"""
|
|
102
|
-
logger.info("ConfigSetupVersion called for module: '%s'", self.module_class.__name__)
|
|
103
102
|
logger.info(
|
|
104
|
-
"
|
|
103
|
+
"ConfigSetupVersion called for module: '%s'",
|
|
104
|
+
self.module_class.__name__,
|
|
105
|
+
extra={
|
|
106
|
+
"module_class": self.module_class,
|
|
107
|
+
"setup_version": request.setup_version,
|
|
108
|
+
"mission_id": request.mission_id,
|
|
109
|
+
},
|
|
105
110
|
)
|
|
106
111
|
# Process the module input
|
|
107
112
|
# TODO: Secret should be used here as well
|
|
@@ -134,7 +139,8 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
|
|
|
134
139
|
return lifecycle_pb2.ConfigSetupModuleResponse(success=False)
|
|
135
140
|
|
|
136
141
|
updated_setup_data = await self.job_manager.generate_config_setup_module_response(job_id)
|
|
137
|
-
logger.
|
|
142
|
+
logger.info("Setup updated")
|
|
143
|
+
logger.debug(f"Updated setup data: {updated_setup_data=}")
|
|
138
144
|
setup_version.content = json_format.ParseDict(
|
|
139
145
|
updated_setup_data,
|
|
140
146
|
struct_pb2.Struct(),
|
|
@@ -159,8 +165,11 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
|
|
|
159
165
|
Raises:
|
|
160
166
|
ServicerError: the necessary query didn't work.
|
|
161
167
|
"""
|
|
162
|
-
logger.info(
|
|
163
|
-
|
|
168
|
+
logger.info(
|
|
169
|
+
"StartModule called for module: '%s'",
|
|
170
|
+
self.module_class.__name__,
|
|
171
|
+
extra={"module_class": self.module_class, "setup_id": request.setup_id, "mission_id": request.mission_id},
|
|
172
|
+
)
|
|
164
173
|
# Process the module input
|
|
165
174
|
# TODO: Check failure of input data format
|
|
166
175
|
input_data = self.module_class.create_input_model(dict(request.input.items()))
|
|
@@ -43,9 +43,9 @@ class GrpcClientWrapper:
|
|
|
43
43
|
private_key=private_key,
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
-
return grpc.secure_channel(config.address, channel_credentials)
|
|
46
|
+
return grpc.secure_channel(config.address, channel_credentials, options=config.channel_options)
|
|
47
47
|
# Insecure channel
|
|
48
|
-
return grpc.insecure_channel(config.address)
|
|
48
|
+
return grpc.insecure_channel(config.address, options=config.channel_options)
|
|
49
49
|
|
|
50
50
|
def exec_grpc_query(self, query_endpoint: str, request: Any) -> Any: # noqa: ANN401
|
|
51
51
|
"""Execute a gRPC query with from the query's rpc endpoint name.
|
|
@@ -65,8 +65,8 @@ class GrpcClientWrapper:
|
|
|
65
65
|
logger.debug("send request to %s", query_endpoint)
|
|
66
66
|
response = getattr(self.stub, query_endpoint)(request)
|
|
67
67
|
logger.debug("receive response from request to registry: %s", response)
|
|
68
|
-
except grpc.RpcError:
|
|
69
|
-
logger.exception("RPC error during
|
|
68
|
+
except grpc.RpcError as e:
|
|
69
|
+
logger.exception("RPC error during %s: %s", query_endpoint, e.details())
|
|
70
70
|
raise ServerError
|
|
71
71
|
else:
|
|
72
72
|
return response
|
|
@@ -169,9 +169,17 @@ class ClientConfig(ChannelConfig):
|
|
|
169
169
|
mode: Client operation mode (sync/async)
|
|
170
170
|
security: Security mode (secure/insecure)
|
|
171
171
|
credentials: Client credentials for secure mode
|
|
172
|
+
channel_options: Additional channel options
|
|
172
173
|
"""
|
|
173
174
|
|
|
174
175
|
credentials: ClientCredentials | None = Field(None, description="Client credentials for secure mode")
|
|
176
|
+
channel_options: list[tuple[str, Any]] = Field(
|
|
177
|
+
default_factory=lambda: [
|
|
178
|
+
("grpc.max_receive_message_length", 50 * 1024 * 1024), # 50MB
|
|
179
|
+
("grpc.max_send_message_length", 50 * 1024 * 1024), # 50MB
|
|
180
|
+
],
|
|
181
|
+
description="Additional channel options",
|
|
182
|
+
)
|
|
175
183
|
|
|
176
184
|
@field_validator("credentials")
|
|
177
185
|
@classmethod
|
|
@@ -213,7 +221,13 @@ class ServerConfig(ChannelConfig):
|
|
|
213
221
|
|
|
214
222
|
max_workers: int = Field(10, description="Maximum number of workers for sync mode")
|
|
215
223
|
credentials: ServerCredentials | None = Field(None, description="Server credentials for secure mode")
|
|
216
|
-
server_options: list[tuple[str, Any]] = Field(
|
|
224
|
+
server_options: list[tuple[str, Any]] = Field(
|
|
225
|
+
default_factory=lambda: [
|
|
226
|
+
("grpc.max_receive_message_length", 50 * 1024 * 1024), # 50MB
|
|
227
|
+
("grpc.max_send_message_length", 50 * 1024 * 1024), # 50MB
|
|
228
|
+
],
|
|
229
|
+
description="Additional server options",
|
|
230
|
+
)
|
|
217
231
|
enable_reflection: bool = Field(default=True, description="Enable reflection for the server")
|
|
218
232
|
enable_health_check: bool = Field(default=True, description="Enable health check service")
|
|
219
233
|
|
digitalkin/logger.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"""This module sets up a logger."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import logging
|
|
5
|
+
import os
|
|
4
6
|
import sys
|
|
5
|
-
from
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any, ClassVar
|
|
6
9
|
|
|
7
10
|
|
|
8
|
-
class
|
|
9
|
-
"""Color formatter for
|
|
11
|
+
class ColorJSONFormatter(logging.Formatter):
|
|
12
|
+
"""Color JSON formatter for development (pretty-printed with colors)."""
|
|
10
13
|
|
|
11
14
|
grey = "\x1b[38;20m"
|
|
12
15
|
green = "\x1b[32;20m"
|
|
@@ -15,28 +18,73 @@ class ColorFormatter(logging.Formatter):
|
|
|
15
18
|
red = "\x1b[31;20m"
|
|
16
19
|
bold_red = "\x1b[31;1m"
|
|
17
20
|
reset = "\x1b[0m"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
logging.
|
|
22
|
-
logging.
|
|
23
|
-
logging.
|
|
24
|
-
logging.
|
|
25
|
-
logging.CRITICAL: bold_red + format + reset + "\n", # type: ignore
|
|
21
|
+
|
|
22
|
+
COLORS: ClassVar[dict[int, str]] = {
|
|
23
|
+
logging.DEBUG: grey,
|
|
24
|
+
logging.INFO: green,
|
|
25
|
+
logging.WARNING: yellow,
|
|
26
|
+
logging.ERROR: red,
|
|
27
|
+
logging.CRITICAL: bold_red,
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
def format(self, record: logging.LogRecord) -> str:
|
|
29
|
-
"""Format the log record.
|
|
30
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
31
|
+
"""Format the log record as colored JSON for development.
|
|
30
32
|
|
|
31
33
|
Args:
|
|
32
34
|
record: The log record to format.
|
|
33
35
|
|
|
34
36
|
Returns:
|
|
35
|
-
str: The formatted log record.
|
|
37
|
+
str: The colored JSON formatted log record.
|
|
36
38
|
"""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
log_obj: dict[str, Any] = {
|
|
40
|
+
"timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(),
|
|
41
|
+
"level": record.levelname.lower(),
|
|
42
|
+
"logger": record.name,
|
|
43
|
+
"message": record.getMessage(),
|
|
44
|
+
"location": f"{record.filename}:{record.lineno}",
|
|
45
|
+
"function": record.funcName,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Add exception info if present
|
|
49
|
+
if record.exc_info:
|
|
50
|
+
log_obj["exception"] = self.formatException(record.exc_info)
|
|
51
|
+
|
|
52
|
+
# Add any extra fields
|
|
53
|
+
skip_attrs = {
|
|
54
|
+
"name",
|
|
55
|
+
"msg",
|
|
56
|
+
"args",
|
|
57
|
+
"created",
|
|
58
|
+
"filename",
|
|
59
|
+
"funcName",
|
|
60
|
+
"levelname",
|
|
61
|
+
"levelno",
|
|
62
|
+
"lineno",
|
|
63
|
+
"module",
|
|
64
|
+
"msecs",
|
|
65
|
+
"message",
|
|
66
|
+
"pathname",
|
|
67
|
+
"process",
|
|
68
|
+
"processName",
|
|
69
|
+
"relativeCreated",
|
|
70
|
+
"thread",
|
|
71
|
+
"threadName",
|
|
72
|
+
"exc_info",
|
|
73
|
+
"exc_text",
|
|
74
|
+
"stack_info",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
extras = {key: value for key, value in record.__dict__.items() if key not in skip_attrs}
|
|
78
|
+
|
|
79
|
+
if extras:
|
|
80
|
+
log_obj["extra"] = extras
|
|
81
|
+
|
|
82
|
+
# Pretty print with color
|
|
83
|
+
color = self.COLORS.get(record.levelno, self.grey)
|
|
84
|
+
json_str = json.dumps(log_obj, indent=2, default=str)
|
|
85
|
+
if os.getenv("RAILWAY_SERVICE_NAME"):
|
|
86
|
+
json_str.replace("\\n", "\n")
|
|
87
|
+
return f"{color}{json_str}{self.reset}"
|
|
40
88
|
|
|
41
89
|
|
|
42
90
|
logging.basicConfig(
|
|
@@ -54,8 +102,7 @@ logger = logging.getLogger("digitalkin")
|
|
|
54
102
|
if not logger.handlers:
|
|
55
103
|
ch = logging.StreamHandler()
|
|
56
104
|
ch.setLevel(logging.INFO)
|
|
57
|
-
|
|
58
|
-
ch.setFormatter(ColorFormatter())
|
|
105
|
+
ch.setFormatter(ColorJSONFormatter())
|
|
59
106
|
|
|
60
107
|
logger.addHandler(ch)
|
|
61
108
|
logger.propagate = False
|
|
@@ -327,9 +327,14 @@ class GrpcFilesystem(FilesystemStrategy, GrpcClientWrapper):
|
|
|
327
327
|
Returns:
|
|
328
328
|
tuple[list[FilesystemRecord], int]: List of files and total count
|
|
329
329
|
"""
|
|
330
|
+
match filters.context:
|
|
331
|
+
case "setup":
|
|
332
|
+
context_id = self.setup_id
|
|
333
|
+
case "mission":
|
|
334
|
+
context_id = self.mission_id
|
|
330
335
|
with GrpcFilesystem._handle_grpc_errors("GetFiles"):
|
|
331
336
|
request = filesystem_pb2.GetFilesRequest(
|
|
332
|
-
context=
|
|
337
|
+
context=context_id,
|
|
333
338
|
filters=self._filter_to_proto(filters),
|
|
334
339
|
include_content=include_content,
|
|
335
340
|
list_size=list_size,
|
|
@@ -7,19 +7,19 @@ base_server/mock/__init__.py,sha256=YZFT-F1l_TpvJYuIPX-7kTeE1CfOjhx9YmNRXVoi-jQ,
|
|
|
7
7
|
base_server/mock/mock_pb2.py,sha256=sETakcS3PAAm4E-hTCV1jIVaQTPEAIoVVHupB8Z_k7Y,1843
|
|
8
8
|
base_server/mock/mock_pb2_grpc.py,sha256=BbOT70H6q3laKgkHfOx1QdfmCS_HxCY4wCOX84YAdG4,3180
|
|
9
9
|
digitalkin/__init__.py,sha256=7LLBAba0th-3SGqcpqFO-lopWdUkVLKzLZiMtB-mW3M,162
|
|
10
|
-
digitalkin/__version__.py,sha256=
|
|
11
|
-
digitalkin/logger.py,sha256=
|
|
10
|
+
digitalkin/__version__.py,sha256=ztRCETPoZd6W_J-mnJ5nS74c-ka0qrOuTY9L88iKUGc,191
|
|
11
|
+
digitalkin/logger.py,sha256=I6ARTJ2oWJ97VqMT2oMb6JU2J2GKOZi9LZk7PUplh2E,2889
|
|
12
12
|
digitalkin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
digitalkin/grpc_servers/__init__.py,sha256=0cJBlwipSmFdXkyH3T0i6OJ1WpAtNsZgYX7JaSnkbtg,804
|
|
14
14
|
digitalkin/grpc_servers/_base_server.py,sha256=NXnnZPjJqUDWoumhEbb7EOEWB7d8XYwpQs-l97NTe4k,18647
|
|
15
|
-
digitalkin/grpc_servers/module_server.py,sha256=
|
|
16
|
-
digitalkin/grpc_servers/module_servicer.py,sha256=
|
|
15
|
+
digitalkin/grpc_servers/module_server.py,sha256=81l1i32F7XGQDgBXjQ7Dpukh-KTpmHzM5PKka8LjviE,10386
|
|
16
|
+
digitalkin/grpc_servers/module_servicer.py,sha256=hhUAH6K5OcvkQEbXQUeUTAHJxkus-KlVwJnG1bGHTo4,18963
|
|
17
17
|
digitalkin/grpc_servers/registry_server.py,sha256=StY18DKYoPKQIU1SIzgito6D4_QA1aMVddZ8O2WGlHY,2223
|
|
18
18
|
digitalkin/grpc_servers/registry_servicer.py,sha256=dqsKGHZ0LnaIvGt4ipaAuigd37sbJBndT4MAT029GsY,16471
|
|
19
19
|
digitalkin/grpc_servers/utils/exceptions.py,sha256=SyOgvjggaUECYmSiqy8KJLHwHVt5IClSTxslHM-IZzI,931
|
|
20
20
|
digitalkin/grpc_servers/utils/factory.py,sha256=jm6rFjiqmtSv7BIHNAOxsG9xXtSvWpx9TfzSQiX97MQ,5899
|
|
21
|
-
digitalkin/grpc_servers/utils/grpc_client_wrapper.py,sha256=
|
|
22
|
-
digitalkin/grpc_servers/utils/models.py,sha256=
|
|
21
|
+
digitalkin/grpc_servers/utils/grpc_client_wrapper.py,sha256=iPHwVAaGeuV0ZW7YhTc7E3SWPyWX244Cy0iFf2DSf_Q,2685
|
|
22
|
+
digitalkin/grpc_servers/utils/models.py,sha256=hiwiGHy3sEBBFdOUBCoC1n_7GVeJhjRmi0sl4tCEJy0,8965
|
|
23
23
|
digitalkin/grpc_servers/utils/types.py,sha256=rQ78s4nAet2jy-NIDj_PUWriT0kuGHr_w6ELjmjgBao,539
|
|
24
24
|
digitalkin/models/__init__.py,sha256=hDHtUfswaNh8wo4NZaBItg9JqC0uNSRqXArNWSrGynY,163
|
|
25
25
|
digitalkin/models/module/__init__.py,sha256=fgTVbsNmLZgM43Vy-Ea5-g0EG3pncmAmJl9nk-OQdzo,561
|
|
@@ -53,7 +53,7 @@ digitalkin/services/cost/grpc_cost.py,sha256=cGtb0atPXSEEOrNIWee-o3ScfNRSAFXJGDu
|
|
|
53
53
|
digitalkin/services/filesystem/__init__.py,sha256=BhwMl_BUvM0d65fmglkp0SVwn3RfYiUOKJgIMnOCaGM,381
|
|
54
54
|
digitalkin/services/filesystem/default_filesystem.py,sha256=qfC4jorfo86fBBnth-4ft13VuaGdvIHgWOVurCykXIk,15182
|
|
55
55
|
digitalkin/services/filesystem/filesystem_strategy.py,sha256=zibVLvX_IBQ-kgh-KYzHdszDeiHFPEAZszu_k99x1GQ,9487
|
|
56
|
-
digitalkin/services/filesystem/grpc_filesystem.py,sha256=
|
|
56
|
+
digitalkin/services/filesystem/grpc_filesystem.py,sha256=ilxTFjelmf8_bVSs3xLlsyWFHVlSJf27c4j8H7R1Rkw,12987
|
|
57
57
|
digitalkin/services/identity/__init__.py,sha256=InkeyLgFYYwItx8mePA8HpfacOMWZwwuc0G4pWtKq9s,270
|
|
58
58
|
digitalkin/services/identity/default_identity.py,sha256=Y2auZHrGSZTIN5D8HyjLvLcNbYFM1CNUE23x7p5VIGw,386
|
|
59
59
|
digitalkin/services/identity/identity_strategy.py,sha256=skappBbds1_qa0Gr24FGrNX1N0_OYhYT1Lh7dUaAirE,429
|
|
@@ -76,14 +76,14 @@ digitalkin/utils/arg_parser.py,sha256=nvjI1pKDY1HfS0oGcMQPtdTQcggXLtpxXMbnMxNEKR
|
|
|
76
76
|
digitalkin/utils/development_mode_action.py,sha256=TqRuAF_A7bDD4twRB4PnZcRoNeaiAnEdxM5kvy4aoaA,1511
|
|
77
77
|
digitalkin/utils/llm_ready_schema.py,sha256=JjMug_lrQllqFoanaC091VgOqwAd-_YzcpqFlS7p778,2375
|
|
78
78
|
digitalkin/utils/package_discover.py,sha256=3e9-6Vf3yAAv2VkHHVK4QVqHJBxQqg3d8uuDTsXph24,13471
|
|
79
|
-
digitalkin-0.2.
|
|
79
|
+
digitalkin-0.2.23.dist-info/licenses/LICENSE,sha256=Ies4HFv2r2hzDRakJYxk3Y60uDFLiG-orIgeTpstnIo,20327
|
|
80
80
|
modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
81
81
|
modules/cpu_intensive_module.py,sha256=ejB9XPnFfA0uCuFUQbM3fy5UYfqqAlF36rv_P5Ri8ho,8363
|
|
82
82
|
modules/minimal_llm_module.py,sha256=Ijld__ZnhzfLwpXD1XVkLZ7jyKZKyOFZczOpiPttJZc,11216
|
|
83
83
|
modules/text_transform_module.py,sha256=bwPSnEUthZQyfLwcTLo52iAxItAoknkLh8Y3m5aywaY,7251
|
|
84
84
|
services/filesystem_module.py,sha256=71Mcja8jCQqiqFHPdsIXplFIHTvgkxRhp0TRXuCfgkk,7430
|
|
85
85
|
services/storage_module.py,sha256=ybTMqmvGaTrR8PqJ4FU0cwxaDjT36TskVrGoetTGmno,6955
|
|
86
|
-
digitalkin-0.2.
|
|
87
|
-
digitalkin-0.2.
|
|
88
|
-
digitalkin-0.2.
|
|
89
|
-
digitalkin-0.2.
|
|
86
|
+
digitalkin-0.2.23.dist-info/METADATA,sha256=6xGU63KlOxOodbF57XVFJGRE-2ufQiQISkzXaeuW_bw,30579
|
|
87
|
+
digitalkin-0.2.23.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
88
|
+
digitalkin-0.2.23.dist-info/top_level.txt,sha256=gcjqlyrZuLjIyxrOIavCQM_olpr6ND5kPKkZd2j0xGo,40
|
|
89
|
+
digitalkin-0.2.23.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|