plexus-python 0.7.0__py3-none-any.whl → 0.8.0__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.
- plexus/__init__.py +1 -1
- plexus/client.py +11 -15
- plexus/config.py +2 -50
- plexus/ws.py +61 -31
- {plexus_python-0.7.0.dist-info → plexus_python-0.8.0.dist-info}/METADATA +6 -6
- plexus_python-0.8.0.dist-info/RECORD +14 -0
- {plexus_python-0.7.0.dist-info → plexus_python-0.8.0.dist-info}/WHEEL +1 -1
- plexus_python-0.7.0.dist-info/RECORD +0 -14
- {plexus_python-0.7.0.dist-info → plexus_python-0.8.0.dist-info}/entry_points.txt +0 -0
- {plexus_python-0.7.0.dist-info → plexus_python-0.8.0.dist-info}/licenses/LICENSE +0 -0
plexus/__init__.py
CHANGED
|
@@ -10,5 +10,5 @@ Plexus — thin Python SDK for sending telemetry to the Plexus gateway.
|
|
|
10
10
|
from plexus.client import Plexus, PlexusError, AuthenticationError, read_mjpeg_frames
|
|
11
11
|
from plexus.config import RetryConfig
|
|
12
12
|
|
|
13
|
-
__version__ = "0.
|
|
13
|
+
__version__ = "0.8.0"
|
|
14
14
|
__all__ = ["Plexus", "PlexusError", "AuthenticationError", "RetryConfig", "read_mjpeg_frames"]
|
plexus/client.py
CHANGED
|
@@ -55,9 +55,7 @@ from plexus.config import (
|
|
|
55
55
|
get_endpoint,
|
|
56
56
|
get_gateway_url,
|
|
57
57
|
get_gateway_ws_url,
|
|
58
|
-
get_install_id,
|
|
59
58
|
get_source_id,
|
|
60
|
-
set_source_id,
|
|
61
59
|
)
|
|
62
60
|
|
|
63
61
|
logger = logging.getLogger(__name__)
|
|
@@ -414,9 +412,7 @@ class Plexus:
|
|
|
414
412
|
api_key=self.api_key,
|
|
415
413
|
source_id=self.source_id,
|
|
416
414
|
ws_url=self._ws_url,
|
|
417
|
-
install_id=get_install_id(),
|
|
418
415
|
agent_version=__version__,
|
|
419
|
-
on_source_id_assigned=self._on_source_id_assigned,
|
|
420
416
|
on_clock_synced=self._on_clock_synced,
|
|
421
417
|
)
|
|
422
418
|
self._ws.start()
|
|
@@ -428,16 +424,6 @@ class Plexus:
|
|
|
428
424
|
def _on_clock_synced(self, offset_ms: int) -> None:
|
|
429
425
|
self._clock_offset_ms = offset_ms
|
|
430
426
|
|
|
431
|
-
def _on_source_id_assigned(self, assigned: str) -> None:
|
|
432
|
-
"""Callback from WebSocketTransport when the gateway returns an
|
|
433
|
-
auto-suffixed source_id. Persists it so subsequent runs (and the HTTP
|
|
434
|
-
fallback path in this process) use the assigned name directly."""
|
|
435
|
-
self.source_id = assigned
|
|
436
|
-
try:
|
|
437
|
-
set_source_id(assigned)
|
|
438
|
-
except Exception as e: # pragma: no cover - persistence failure is non-fatal
|
|
439
|
-
logger.debug("failed to persist assigned source_id: %s", e)
|
|
440
|
-
|
|
441
427
|
def _encode_frame(self, frame, quality: int) -> Tuple[bytes, int, int]:
|
|
442
428
|
"""Normalize any supported frame type to (jpeg_bytes, width, height).
|
|
443
429
|
|
|
@@ -700,6 +686,7 @@ class Plexus:
|
|
|
700
686
|
*,
|
|
701
687
|
description: Optional[str] = None,
|
|
702
688
|
params: Optional[List[Dict[str, Any]]] = None,
|
|
689
|
+
concurrency: str = "accept",
|
|
703
690
|
) -> None:
|
|
704
691
|
"""Register a command handler (WebSocket transport only).
|
|
705
692
|
|
|
@@ -709,6 +696,12 @@ class Plexus:
|
|
|
709
696
|
|
|
710
697
|
Must be called before the first send() so the command is advertised
|
|
711
698
|
in the auth frame.
|
|
699
|
+
|
|
700
|
+
concurrency: "accept" (default) runs overlapping invocations of the
|
|
701
|
+
same command concurrently; "reject" refuses a new invocation with
|
|
702
|
+
an error result while a previous one is still running. Use
|
|
703
|
+
"reject" for handlers that drive exclusive hardware (e.g. a pump
|
|
704
|
+
init) so a retry or double-click can't start two at once.
|
|
712
705
|
"""
|
|
713
706
|
ws = self._ensure_ws()
|
|
714
707
|
if ws.is_authenticated:
|
|
@@ -718,7 +711,10 @@ class Plexus:
|
|
|
718
711
|
"Call on_command() before the first send().",
|
|
719
712
|
name,
|
|
720
713
|
)
|
|
721
|
-
ws.register_command(
|
|
714
|
+
ws.register_command(
|
|
715
|
+
name, handler, description=description, params=params,
|
|
716
|
+
concurrency=concurrency,
|
|
717
|
+
)
|
|
722
718
|
|
|
723
719
|
def _send_points(self, points: List[Dict[str, Any]]) -> bool:
|
|
724
720
|
"""Send data points to the gateway with retry and buffering.
|
plexus/config.py
CHANGED
|
@@ -49,8 +49,8 @@ CONFIG_DIR = Path.home() / ".plexus"
|
|
|
49
49
|
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
50
50
|
|
|
51
51
|
PLEXUS_ENDPOINT = "https://app.plexus.company"
|
|
52
|
-
PLEXUS_GATEWAY_URL = "https://
|
|
53
|
-
PLEXUS_GATEWAY_WS_URL = "wss://
|
|
52
|
+
PLEXUS_GATEWAY_URL = "https://gateway.plexus.company"
|
|
53
|
+
PLEXUS_GATEWAY_WS_URL = "wss://gateway.plexus.company"
|
|
54
54
|
|
|
55
55
|
DEFAULT_CONFIG = {
|
|
56
56
|
"api_key": None,
|
|
@@ -145,54 +145,6 @@ def get_source_id() -> Optional[str]:
|
|
|
145
145
|
return source_id
|
|
146
146
|
|
|
147
147
|
|
|
148
|
-
def get_install_id() -> str:
|
|
149
|
-
"""Get the device install ID, generating one if not set.
|
|
150
|
-
|
|
151
|
-
The install_id is a stable per-installation UUID. It is generated lazily
|
|
152
|
-
on first run (NOT at image-build time) so that cloned SD-card images
|
|
153
|
-
naturally get distinct install_ids on their first boot. The gateway uses
|
|
154
|
-
it to tell "same device reconnecting" from "different device claiming the
|
|
155
|
-
same name" when resolving source_id collisions.
|
|
156
|
-
|
|
157
|
-
Resolution order:
|
|
158
|
-
1. ``PLEXUS_INSTALL_ID`` env var — lets ephemeral containers (Fly
|
|
159
|
-
machines, CI runners, Kubernetes pods) pin a stable identity
|
|
160
|
-
across restarts when the config filesystem is ephemeral. Without
|
|
161
|
-
this, every redeploy generates a new install_id and the gateway
|
|
162
|
-
auto-suffixes the source_id to avoid a collision with the prior
|
|
163
|
-
install ("gw-001" → "gw-001_2" → "gw-001_3"…).
|
|
164
|
-
2. ``install_id`` in the on-disk config.
|
|
165
|
-
3. Newly-generated UUID, persisted to config.
|
|
166
|
-
"""
|
|
167
|
-
env_id = os.environ.get("PLEXUS_INSTALL_ID", "").strip()
|
|
168
|
-
if env_id:
|
|
169
|
-
return env_id
|
|
170
|
-
|
|
171
|
-
config = load_config()
|
|
172
|
-
install_id = config.get("install_id")
|
|
173
|
-
|
|
174
|
-
if not install_id:
|
|
175
|
-
import uuid
|
|
176
|
-
install_id = uuid.uuid4().hex
|
|
177
|
-
config["install_id"] = install_id
|
|
178
|
-
save_config(config)
|
|
179
|
-
|
|
180
|
-
return install_id
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def set_source_id(source_id: str) -> None:
|
|
184
|
-
"""Persist an updated source_id to the config file.
|
|
185
|
-
|
|
186
|
-
Called by the SDK when the gateway returns an auto-suffixed name so the
|
|
187
|
-
assigned name is stable across reconnects.
|
|
188
|
-
"""
|
|
189
|
-
config = load_config()
|
|
190
|
-
if config.get("source_id") == source_id:
|
|
191
|
-
return
|
|
192
|
-
config["source_id"] = source_id
|
|
193
|
-
save_config(config)
|
|
194
|
-
|
|
195
|
-
|
|
196
148
|
def get_persistent_buffer() -> bool:
|
|
197
149
|
"""Get persistent buffer setting. Default True (store-and-forward enabled)."""
|
|
198
150
|
config = load_config()
|
plexus/ws.py
CHANGED
|
@@ -62,6 +62,11 @@ class _RegisteredCommand:
|
|
|
62
62
|
handler: CommandHandler
|
|
63
63
|
description: Optional[str] = None
|
|
64
64
|
params: List[Dict[str, Any]] = field(default_factory=list)
|
|
65
|
+
# "accept" (default): run overlapping invocations concurrently.
|
|
66
|
+
# "reject": refuse a new invocation with an error result while a
|
|
67
|
+
# previous one of the same command is still running.
|
|
68
|
+
concurrency: str = "accept"
|
|
69
|
+
_lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
|
65
70
|
|
|
66
71
|
def to_manifest(self) -> Dict[str, Any]:
|
|
67
72
|
m: Dict[str, Any] = {"name": self.name}
|
|
@@ -89,11 +94,9 @@ class WebSocketTransport:
|
|
|
89
94
|
source_id: str,
|
|
90
95
|
ws_url: str,
|
|
91
96
|
*,
|
|
92
|
-
install_id: str = "",
|
|
93
97
|
agent_version: str = "0.0.0",
|
|
94
98
|
platform: str = "python-sdk",
|
|
95
99
|
auto_reconnect: bool = True,
|
|
96
|
-
on_source_id_assigned: Optional[Callable[[str], None]] = None,
|
|
97
100
|
on_clock_synced: Optional[Callable[[int], None]] = None,
|
|
98
101
|
):
|
|
99
102
|
if not api_key:
|
|
@@ -103,12 +106,10 @@ class WebSocketTransport:
|
|
|
103
106
|
|
|
104
107
|
self.api_key = api_key
|
|
105
108
|
self.source_id = source_id
|
|
106
|
-
self.install_id = install_id
|
|
107
109
|
self.ws_url = _ensure_device_path(ws_url)
|
|
108
110
|
self.agent_version = agent_version
|
|
109
111
|
self.platform = platform
|
|
110
112
|
self.auto_reconnect = auto_reconnect
|
|
111
|
-
self._on_source_id_assigned = on_source_id_assigned
|
|
112
113
|
self._on_clock_synced = on_clock_synced
|
|
113
114
|
|
|
114
115
|
self._commands: Dict[str, _RegisteredCommand] = {}
|
|
@@ -131,11 +132,24 @@ class WebSocketTransport:
|
|
|
131
132
|
*,
|
|
132
133
|
description: Optional[str] = None,
|
|
133
134
|
params: Optional[List[Dict[str, Any]]] = None,
|
|
135
|
+
concurrency: str = "accept",
|
|
134
136
|
) -> None:
|
|
135
137
|
"""Register a command handler. Must be called before start() to be
|
|
136
|
-
advertised in the auth frame.
|
|
138
|
+
advertised in the auth frame.
|
|
139
|
+
|
|
140
|
+
concurrency controls what happens when a command arrives while a
|
|
141
|
+
previous invocation of the *same* command is still running:
|
|
142
|
+
"accept" (default) — run it concurrently on a new thread.
|
|
143
|
+
"reject" — refuse it with an error result until the
|
|
144
|
+
in-flight invocation finishes.
|
|
145
|
+
"""
|
|
146
|
+
if concurrency not in ("accept", "reject"):
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"concurrency must be 'accept' or 'reject', got {concurrency!r}"
|
|
149
|
+
)
|
|
137
150
|
self._commands[name] = _RegisteredCommand(
|
|
138
|
-
name=name, handler=handler, description=description,
|
|
151
|
+
name=name, handler=handler, description=description,
|
|
152
|
+
params=params or [], concurrency=concurrency,
|
|
139
153
|
)
|
|
140
154
|
|
|
141
155
|
def start(self) -> None:
|
|
@@ -278,16 +292,13 @@ class WebSocketTransport:
|
|
|
278
292
|
self._ws = ws
|
|
279
293
|
|
|
280
294
|
# 1. Send device_auth
|
|
281
|
-
desired_source_id = self.source_id
|
|
282
295
|
auth = {
|
|
283
296
|
"type": "device_auth",
|
|
284
297
|
"api_key": self.api_key,
|
|
285
|
-
"source_id":
|
|
298
|
+
"source_id": self.source_id,
|
|
286
299
|
"platform": self.platform,
|
|
287
300
|
"agent_version": self.agent_version,
|
|
288
301
|
}
|
|
289
|
-
if self.install_id:
|
|
290
|
-
auth["install_id"] = self.install_id
|
|
291
302
|
if self._commands:
|
|
292
303
|
auth["commands"] = [c.to_manifest() for c in self._commands.values()]
|
|
293
304
|
ws.send(json.dumps(auth))
|
|
@@ -312,22 +323,6 @@ class WebSocketTransport:
|
|
|
312
323
|
except Exception as e:
|
|
313
324
|
logger.debug("on_clock_synced callback raised: %s", e)
|
|
314
325
|
|
|
315
|
-
# The gateway may return a different source_id if the desired name
|
|
316
|
-
# was already claimed by another install — adopt the assigned value
|
|
317
|
-
# so all subsequent frames (heartbeats, future reconnects) use it.
|
|
318
|
-
assigned = msg.get("source_id")
|
|
319
|
-
if isinstance(assigned, str) and assigned and assigned != self.source_id:
|
|
320
|
-
logger.info(
|
|
321
|
-
"plexus ws source_id auto-suffixed: requested=%s assigned=%s",
|
|
322
|
-
desired_source_id, assigned,
|
|
323
|
-
)
|
|
324
|
-
self.source_id = assigned
|
|
325
|
-
if self._on_source_id_assigned is not None:
|
|
326
|
-
try:
|
|
327
|
-
self._on_source_id_assigned(assigned)
|
|
328
|
-
except Exception as e: # pragma: no cover - callback errors must not break auth
|
|
329
|
-
logger.debug("on_source_id_assigned callback raised: %s", e)
|
|
330
|
-
|
|
331
326
|
was_reconnect = self._backoff_attempt > 0
|
|
332
327
|
self._authenticated.set()
|
|
333
328
|
self._backoff_attempt = 0
|
|
@@ -394,13 +389,42 @@ class WebSocketTransport:
|
|
|
394
389
|
})
|
|
395
390
|
return
|
|
396
391
|
|
|
392
|
+
# concurrency="reject": if an invocation is already running, refuse
|
|
393
|
+
# this one immediately rather than starting a second in parallel.
|
|
394
|
+
holds_lock = False
|
|
395
|
+
if reg.concurrency == "reject":
|
|
396
|
+
if not reg._lock.acquire(blocking=False):
|
|
397
|
+
self._send_frame({
|
|
398
|
+
"type": "command_result",
|
|
399
|
+
"id": cmd_id,
|
|
400
|
+
"command": command,
|
|
401
|
+
"event": "error",
|
|
402
|
+
"error": f"command already in progress: {command}",
|
|
403
|
+
})
|
|
404
|
+
return
|
|
405
|
+
holds_lock = True
|
|
406
|
+
|
|
397
407
|
# Run the handler off the read-loop thread so a slow handler doesn't
|
|
398
408
|
# block heartbeats or other inbound frames.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
409
|
+
try:
|
|
410
|
+
threading.Thread(
|
|
411
|
+
target=self._run_handler,
|
|
412
|
+
args=(reg, cmd_id, command, params, holds_lock),
|
|
413
|
+
daemon=True,
|
|
414
|
+
).start()
|
|
415
|
+
except Exception as e:
|
|
416
|
+
# Thread creation failed (e.g. resource exhaustion). Release the
|
|
417
|
+
# concurrency lock so the command isn't wedged as "in progress",
|
|
418
|
+
# and report the failure rather than leaving it silently un-acked.
|
|
419
|
+
if holds_lock:
|
|
420
|
+
reg._lock.release()
|
|
421
|
+
self._send_frame({
|
|
422
|
+
"type": "command_result",
|
|
423
|
+
"id": cmd_id,
|
|
424
|
+
"command": command,
|
|
425
|
+
"event": "error",
|
|
426
|
+
"error": f"failed to start command handler: {e}",
|
|
427
|
+
})
|
|
404
428
|
|
|
405
429
|
def _run_handler(
|
|
406
430
|
self,
|
|
@@ -408,6 +432,7 @@ class WebSocketTransport:
|
|
|
408
432
|
cmd_id: str,
|
|
409
433
|
command: str,
|
|
410
434
|
params: Dict[str, Any],
|
|
435
|
+
holds_lock: bool = False,
|
|
411
436
|
) -> None:
|
|
412
437
|
try:
|
|
413
438
|
result = reg.handler(command, params)
|
|
@@ -420,6 +445,11 @@ class WebSocketTransport:
|
|
|
420
445
|
"error": str(e),
|
|
421
446
|
})
|
|
422
447
|
return
|
|
448
|
+
finally:
|
|
449
|
+
# Release the concurrency lock (if held) as soon as the handler
|
|
450
|
+
# returns, regardless of success or failure.
|
|
451
|
+
if holds_lock:
|
|
452
|
+
reg._lock.release()
|
|
423
453
|
self._send_frame({
|
|
424
454
|
"type": "command_result",
|
|
425
455
|
"id": cmd_id,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plexus-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Thin Python SDK for Plexus — send telemetry in one line
|
|
5
|
-
Project-URL: Homepage, https://plexus.
|
|
6
|
-
Project-URL: Documentation, https://docs.plexus.
|
|
5
|
+
Project-URL: Homepage, https://plexus.company
|
|
6
|
+
Project-URL: Documentation, https://docs.plexus.company
|
|
7
7
|
Project-URL: Repository, https://github.com/plexus-oss/plexus-python
|
|
8
8
|
Project-URL: Issues, https://github.com/plexus-oss/plexus-python/issues
|
|
9
|
-
Author-email: Plexus <
|
|
9
|
+
Author-email: Plexus <info@plexus.company>
|
|
10
10
|
License-Expression: Apache-2.0
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Keywords: fleet,hardware,iot,monitoring,observability,telemetry
|
|
@@ -270,8 +270,8 @@ The SDK sends an `ack` frame before invoking the handler, then a `result` frame
|
|
|
270
270
|
| Variable | Description | Default |
|
|
271
271
|
| ----------------------- | ---------------------------- | -------------------------------- |
|
|
272
272
|
| `PLEXUS_API_KEY` | API key (required) | none |
|
|
273
|
-
| `PLEXUS_GATEWAY_URL` | HTTP ingest URL | `https://
|
|
274
|
-
| `PLEXUS_GATEWAY_WS_URL` | WebSocket URL | `wss://
|
|
273
|
+
| `PLEXUS_GATEWAY_URL` | HTTP ingest URL | `https://gateway.plexus.company` |
|
|
274
|
+
| `PLEXUS_GATEWAY_WS_URL` | WebSocket URL | `wss://gateway.plexus.company` |
|
|
275
275
|
|
|
276
276
|
## Architecture
|
|
277
277
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
plexus/__init__.py,sha256=O9GpL-CqlDKRoEfQq5ojYNsIMa7CtXuYvdw3zCrC8pg,447
|
|
2
|
+
plexus/_log.py,sha256=3fjXrHFZghQ_17umMcvDUjjTH6aTQB3J4SpVDBiH03w,335
|
|
3
|
+
plexus/buffer.py,sha256=0i6PLgoj904jFNv9RCrlskvPQsPSu_KNdYWMasFOvsg,9596
|
|
4
|
+
plexus/cli.py,sha256=-2wvHXQzobx3_tDGTXpaE2PlHv884y93Mu29kZE8qZE,14214
|
|
5
|
+
plexus/client.py,sha256=12qnUgmZbTEXuSFtMCAXqosh-V84odSiqagbhtfjAc8,36280
|
|
6
|
+
plexus/config.py,sha256=Y4rPo1zeqAOnz-pQWrDBAXZ1gjfr0UD5Q6HHTMmbKew,4450
|
|
7
|
+
plexus/ws.py,sha256=PSVVrS1vpGi20qGdRfTi4LWCXzt3Dx3hR09FuoT-QIs,18643
|
|
8
|
+
plexus/cameras/__init__.py,sha256=OvnU9KGKxkVtFLlk56H9x-ATa6UvpLI7PANa0HQO2cc,490
|
|
9
|
+
plexus/cameras/thermal.py,sha256=7o33QsF1RiZLManTxZ2E36nO8lRAHppCDkS3zXBHCxs,12047
|
|
10
|
+
plexus_python-0.8.0.dist-info/METADATA,sha256=MfH0vB7R8uBYeJhTTY8Xbd_Qi3qCYZ3jzSIS8cPFNmg,11750
|
|
11
|
+
plexus_python-0.8.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
12
|
+
plexus_python-0.8.0.dist-info/entry_points.txt,sha256=YlkOtTn_7Q_IGuJaKdvpU-90dCeBSPx2p_UTGMAz5Zs,43
|
|
13
|
+
plexus_python-0.8.0.dist-info/licenses/LICENSE,sha256=nm3qP1F-JAGcfLpRVtIX24L20LMnRpxmZ2oKZzFpLVo,10755
|
|
14
|
+
plexus_python-0.8.0.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
plexus/__init__.py,sha256=HNRVhpDrkyyT-SQqfFzNf3E_uwbwQA-XcDKPoBytdEo,447
|
|
2
|
-
plexus/_log.py,sha256=3fjXrHFZghQ_17umMcvDUjjTH6aTQB3J4SpVDBiH03w,335
|
|
3
|
-
plexus/buffer.py,sha256=0i6PLgoj904jFNv9RCrlskvPQsPSu_KNdYWMasFOvsg,9596
|
|
4
|
-
plexus/cli.py,sha256=-2wvHXQzobx3_tDGTXpaE2PlHv884y93Mu29kZE8qZE,14214
|
|
5
|
-
plexus/client.py,sha256=DaCPURIhVswwjf4oGrp381Atiy02gk3viLy6NioBAtQ,36480
|
|
6
|
-
plexus/config.py,sha256=wsG6lhNLmKe3JRlVycyRUKQeywnPUPPfrWkXFxYwELE,6179
|
|
7
|
-
plexus/ws.py,sha256=_c--U-yySZbMXZsaj0fUaYKF_WesolHsrMDx4oWYfFQ,17428
|
|
8
|
-
plexus/cameras/__init__.py,sha256=OvnU9KGKxkVtFLlk56H9x-ATa6UvpLI7PANa0HQO2cc,490
|
|
9
|
-
plexus/cameras/thermal.py,sha256=7o33QsF1RiZLManTxZ2E36nO8lRAHppCDkS3zXBHCxs,12047
|
|
10
|
-
plexus_python-0.7.0.dist-info/METADATA,sha256=tlsktssdsOI7dyA7O2I6azPay2Xj998Z4zNxbFf4H6s,11739
|
|
11
|
-
plexus_python-0.7.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
12
|
-
plexus_python-0.7.0.dist-info/entry_points.txt,sha256=YlkOtTn_7Q_IGuJaKdvpU-90dCeBSPx2p_UTGMAz5Zs,43
|
|
13
|
-
plexus_python-0.7.0.dist-info/licenses/LICENSE,sha256=nm3qP1F-JAGcfLpRVtIX24L20LMnRpxmZ2oKZzFpLVo,10755
|
|
14
|
-
plexus_python-0.7.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|