flashnode 0.2.1__py3-none-any.whl → 0.3.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.
- flashnode/config/local_data.py +111 -0
- flashnode/executor/argv_runner.py +7 -1
- flashnode/executor/docker_runner.py +6 -1
- flashnode/executor/hardening.py +84 -1
- flashnode/inventory/capabilities.py +11 -0
- {flashnode-0.2.1.dist-info → flashnode-0.3.0.dist-info}/METADATA +11 -2
- {flashnode-0.2.1.dist-info → flashnode-0.3.0.dist-info}/RECORD +11 -10
- {flashnode-0.2.1.dist-info → flashnode-0.3.0.dist-info}/WHEEL +0 -0
- {flashnode-0.2.1.dist-info → flashnode-0.3.0.dist-info}/entry_points.txt +0 -0
- {flashnode-0.2.1.dist-info → flashnode-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {flashnode-0.2.1.dist-info → flashnode-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Local datasets: data the host owner lends to tasks without uploading it.
|
|
2
|
+
|
|
3
|
+
FLASHNODE_LOCAL_DATA="patients=/srv/data/patients-2026,labs=/srv/labs"
|
|
4
|
+
|
|
5
|
+
The owner names a directory on their machine and gives it a LABEL. The label
|
|
6
|
+
is what the agent advertises to the coordinator (`NodeRegistration.
|
|
7
|
+
local_datasets`, so the placement gate can route a job that needs `patients`
|
|
8
|
+
to a machine that has it); the PATH never leaves this machine. That asymmetry
|
|
9
|
+
is the feature — a path leaks the owner's directory layout, their username,
|
|
10
|
+
and frequently the dataset's identity, and none of that is needed to schedule
|
|
11
|
+
work.
|
|
12
|
+
|
|
13
|
+
Two rules make the rest of the system safe to write:
|
|
14
|
+
|
|
15
|
+
- **Fail closed on garbage** (same judgement as `HostPolicy` above): a
|
|
16
|
+
malformed value raises rather than yielding the entries that happened to
|
|
17
|
+
parse. Half-applying the owner's intent is the worst outcome — they believe
|
|
18
|
+
two directories are exposed, one is, and the disagreement surfaces hours
|
|
19
|
+
later as a job that never places.
|
|
20
|
+
- **A label is a name, not a path fragment.** It is restricted to
|
|
21
|
+
``[A-Za-z0-9._-]`` here, once, so that no consumer downstream has to defend
|
|
22
|
+
itself: it is a map key, and a single container-side directory segment. If
|
|
23
|
+
someone later joins it to a host path, the charset already forbids the
|
|
24
|
+
escape.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
|
|
32
|
+
__all__ = ["LocalDataError", "LABEL_RE", "check_label", "parse_local_data",
|
|
33
|
+
"load_local_data", "LOCAL_DATA_ENV"]
|
|
34
|
+
|
|
35
|
+
LOCAL_DATA_ENV = "FLASHNODE_LOCAL_DATA"
|
|
36
|
+
|
|
37
|
+
#: The whole alphabet a dataset label may use. Deliberately narrower than
|
|
38
|
+
#: anything a filesystem accepts: '/', '..' and whitespace are the characters
|
|
39
|
+
#: that turn a name into a traversal, and they are simply not expressible.
|
|
40
|
+
LABEL_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class LocalDataError(ValueError):
|
|
44
|
+
"""The owner's `FLASHNODE_LOCAL_DATA` cannot be honoured as written."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def check_label(label: str) -> str:
|
|
48
|
+
"""Return `label` if it is a legal dataset label; raise otherwise.
|
|
49
|
+
|
|
50
|
+
Shared by the parser (owner-supplied labels) and the mount builder
|
|
51
|
+
(payload-supplied labels) so both ends of the wire agree on what a label
|
|
52
|
+
is — one definition, not two that can drift.
|
|
53
|
+
"""
|
|
54
|
+
if not LABEL_RE.match(label) or label in (".", ".."):
|
|
55
|
+
raise LocalDataError(
|
|
56
|
+
f"illegal local dataset label {label!r}: labels are names, not "
|
|
57
|
+
"paths — use only [A-Za-z0-9._-]"
|
|
58
|
+
)
|
|
59
|
+
return label
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def parse_local_data(raw: str | None) -> dict[str, str]:
|
|
63
|
+
"""Parse ``"label=/path,other=/path2"`` into ``{label: path}``.
|
|
64
|
+
|
|
65
|
+
Unset, empty, or all-separators means the owner did not opt in, and that
|
|
66
|
+
is the normal case: an empty map, no mounts, nothing advertised.
|
|
67
|
+
"""
|
|
68
|
+
result: dict[str, str] = {}
|
|
69
|
+
for entry in (raw or "").split(","):
|
|
70
|
+
entry = entry.strip()
|
|
71
|
+
if not entry:
|
|
72
|
+
continue
|
|
73
|
+
label, sep, path = entry.partition("=")
|
|
74
|
+
label, path = label.strip(), path.strip()
|
|
75
|
+
if not sep or not label or not path:
|
|
76
|
+
raise LocalDataError(
|
|
77
|
+
f"malformed {LOCAL_DATA_ENV} entry {entry!r}: expected "
|
|
78
|
+
"label=/absolute/path"
|
|
79
|
+
)
|
|
80
|
+
check_label(label)
|
|
81
|
+
if not path.startswith("/") and ":" not in path:
|
|
82
|
+
# A relative path would resolve against whatever directory the
|
|
83
|
+
# agent happens to have been started in — never what the owner
|
|
84
|
+
# meant, and a moving target between a shell run and a systemd
|
|
85
|
+
# unit. (The ':' escape hatch is a Windows drive letter, which
|
|
86
|
+
# the next check then judges on its own terms.)
|
|
87
|
+
raise LocalDataError(
|
|
88
|
+
f"local dataset {label!r} path {path!r} is not absolute — "
|
|
89
|
+
"refusing to resolve it against the agent's working directory"
|
|
90
|
+
)
|
|
91
|
+
if ":" in path and not re.match(r"^[A-Za-z]:[\\/]", path):
|
|
92
|
+
# `docker -v` splits its argument on ':'. A source containing one
|
|
93
|
+
# silently re-reads as src:dst:opts — i.e. as a mount the owner
|
|
94
|
+
# did not write. A Windows drive letter is the one form that is
|
|
95
|
+
# both legitimate and rewritable (hardening._bind_mount_source).
|
|
96
|
+
raise LocalDataError(
|
|
97
|
+
f"local dataset {label!r} path {path!r} contains ':', which "
|
|
98
|
+
"cannot be used as a bind-mount source"
|
|
99
|
+
)
|
|
100
|
+
if label in result:
|
|
101
|
+
raise LocalDataError(
|
|
102
|
+
f"local dataset label {label!r} is mapped twice — refusing to "
|
|
103
|
+
"guess which directory the owner meant"
|
|
104
|
+
)
|
|
105
|
+
result[label] = path
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def load_local_data(env: dict[str, str] | None = None) -> dict[str, str]:
|
|
110
|
+
"""The host owner's dataset map, from the environment."""
|
|
111
|
+
return parse_local_data((env if env is not None else os.environ).get(LOCAL_DATA_ENV))
|
|
@@ -70,7 +70,13 @@ class ArgvDockerRunner:
|
|
|
70
70
|
name = container_name(payload.get("task_id"))
|
|
71
71
|
command = [
|
|
72
72
|
"docker", "run", "--rm", "--name", name,
|
|
73
|
-
*harden_args(
|
|
73
|
+
*harden_args(
|
|
74
|
+
workdir, cpus=self.cpus, memory_gb=self.memory_gb,
|
|
75
|
+
# The host owner's local datasets this task asked for. Refused
|
|
76
|
+
# here (before any subprocess) if this host does not lend them
|
|
77
|
+
# — see hardening.local_data_mounts.
|
|
78
|
+
local_inputs=payload.get("local_inputs"),
|
|
79
|
+
),
|
|
74
80
|
*env_args,
|
|
75
81
|
image, # argv follows the image, where docker treats it
|
|
76
82
|
*argv, # as the container command: leading '-' is inert
|
|
@@ -75,7 +75,12 @@ class DockerRunner:
|
|
|
75
75
|
name = container_name(payload.get("task_id"))
|
|
76
76
|
argv = [
|
|
77
77
|
"docker", "run", "--rm", "--name", name,
|
|
78
|
-
*harden_args(
|
|
78
|
+
*harden_args(
|
|
79
|
+
workdir, cpus=self.cpus, memory_gb=self.memory_gb,
|
|
80
|
+
# See hardening.local_data_mounts: read-only, only what the
|
|
81
|
+
# payload named, and a refusal if this host does not lend it.
|
|
82
|
+
local_inputs=payload.get("local_inputs"),
|
|
83
|
+
),
|
|
79
84
|
image,
|
|
80
85
|
"python", "-m", module,
|
|
81
86
|
"--spec", f"{CONTAINER_WORKDIR}/spec.json",
|
flashnode/executor/hardening.py
CHANGED
|
@@ -16,6 +16,9 @@ import sys
|
|
|
16
16
|
import uuid
|
|
17
17
|
from pathlib import Path, PureWindowsPath
|
|
18
18
|
|
|
19
|
+
from flashnode.config.local_data import LocalDataError, check_label, load_local_data
|
|
20
|
+
from flashnode.executor.runner import TaskExecutionError
|
|
21
|
+
|
|
19
22
|
CONTAINER_WORKDIR = "/work"
|
|
20
23
|
|
|
21
24
|
# Docker container names must match [a-zA-Z0-9][a-zA-Z0-9_.-]*. We prefix
|
|
@@ -112,14 +115,91 @@ def _bind_mount_source(workdir: Path) -> str:
|
|
|
112
115
|
return str(workdir)
|
|
113
116
|
|
|
114
117
|
|
|
118
|
+
def local_data_mounts(
|
|
119
|
+
local_inputs: object,
|
|
120
|
+
available: dict[str, str] | None = None,
|
|
121
|
+
) -> list[str]:
|
|
122
|
+
"""`-v` flags binding the host owner's local datasets READ-ONLY under
|
|
123
|
+
`/work/inputs/<label>`, for the labels this payload asked for.
|
|
124
|
+
|
|
125
|
+
This is the half of the local-data feature that never uploads anything:
|
|
126
|
+
the bytes stay on the owner's disk and the task reads them in place. Three
|
|
127
|
+
properties are load-bearing.
|
|
128
|
+
|
|
129
|
+
- **`:ro`, always.** The owner lends the data; they do not hand a
|
|
130
|
+
stranger's code write access to it. There is no flag to turn this off.
|
|
131
|
+
- **Only what was requested.** A payload that names `patients` gets
|
|
132
|
+
`patients`. A host that also lends `labs` does not silently expose it to
|
|
133
|
+
a job that never asked — least privilege, per task.
|
|
134
|
+
- **Refuse rather than run half-fed** (AGENTS.md rule 3, fail closed). A
|
|
135
|
+
label this host has not mapped is a `TaskExecutionError` naming the
|
|
136
|
+
label — the *task* fails and requeues elsewhere; the agent lives. It
|
|
137
|
+
should not be reachable at all, because the coordinator's placement gate
|
|
138
|
+
reads the same advertisement this host published, so arriving here means
|
|
139
|
+
gate and host disagree — precisely the moment to stop, not to improvise
|
|
140
|
+
an empty directory the workload would read as "no patients".
|
|
141
|
+
|
|
142
|
+
`local_inputs` comes from an untrusted payload, so it is type-checked and
|
|
143
|
+
charset-checked here rather than trusted to be the list the compiler
|
|
144
|
+
produced. A label is never joined to a host path: it is a map key, and one
|
|
145
|
+
container-side directory segment.
|
|
146
|
+
|
|
147
|
+
`available` defaults to the host owner's `FLASHNODE_LOCAL_DATA` — read
|
|
148
|
+
only when a task actually asks for something, so a host whose value is
|
|
149
|
+
malformed fails the tasks that need it rather than every task on the
|
|
150
|
+
machine.
|
|
151
|
+
"""
|
|
152
|
+
if local_inputs is None:
|
|
153
|
+
return []
|
|
154
|
+
if not isinstance(local_inputs, list) or not all(
|
|
155
|
+
isinstance(name, str) for name in local_inputs
|
|
156
|
+
):
|
|
157
|
+
raise TaskExecutionError(
|
|
158
|
+
"payload 'local_inputs' must be a list of local dataset labels"
|
|
159
|
+
)
|
|
160
|
+
if not local_inputs:
|
|
161
|
+
return []
|
|
162
|
+
if available is None:
|
|
163
|
+
try:
|
|
164
|
+
available = load_local_data()
|
|
165
|
+
except LocalDataError as exc:
|
|
166
|
+
# Barely reachable — `discover()` parses the same value at startup
|
|
167
|
+
# and the agent refuses to register on a malformed one. If it ever
|
|
168
|
+
# is reached, a misconfigured host fails a task; it does not die.
|
|
169
|
+
raise TaskExecutionError(f"host local-data config is unusable: {exc}") from None
|
|
170
|
+
args: list[str] = []
|
|
171
|
+
for label in local_inputs:
|
|
172
|
+
try:
|
|
173
|
+
check_label(label)
|
|
174
|
+
except LocalDataError as exc:
|
|
175
|
+
raise TaskExecutionError(str(exc)) from None
|
|
176
|
+
if label not in available:
|
|
177
|
+
raise TaskExecutionError(
|
|
178
|
+
f"task requires local dataset {label!r}, which this host does "
|
|
179
|
+
"not provide — refusing to run"
|
|
180
|
+
)
|
|
181
|
+
source = _bind_mount_source(Path(available[label]))
|
|
182
|
+
args += ["-v", f"{source}:{CONTAINER_WORKDIR}/inputs/{label}:ro"]
|
|
183
|
+
return args
|
|
184
|
+
|
|
185
|
+
|
|
115
186
|
def harden_args(
|
|
116
187
|
workdir: Path,
|
|
117
188
|
*,
|
|
118
189
|
cpus: float,
|
|
119
190
|
memory_gb: float,
|
|
120
191
|
pids_limit: int = 512,
|
|
192
|
+
local_inputs: object = None,
|
|
193
|
+
local_data: dict[str, str] | None = None,
|
|
121
194
|
) -> list[str]:
|
|
122
|
-
"""Docker flags common to every sandboxed task.
|
|
195
|
+
"""Docker flags common to every sandboxed task.
|
|
196
|
+
|
|
197
|
+
`local_inputs` is the payload's list of local dataset labels (None or []
|
|
198
|
+
for the overwhelmingly common task that needs none, and then the flags are
|
|
199
|
+
byte-for-byte what they were before the feature existed). `local_data` is
|
|
200
|
+
the host owner's label→path map; it defaults to their environment, so a
|
|
201
|
+
runner never has to know where the map comes from.
|
|
202
|
+
"""
|
|
123
203
|
return [
|
|
124
204
|
# the job never reaches the volunteer's LAN or the internet; the
|
|
125
205
|
# agent is the courier for inputs, outputs, and checkpoints
|
|
@@ -137,5 +217,8 @@ def harden_args(
|
|
|
137
217
|
"--memory-swap", f"{memory_gb}g",
|
|
138
218
|
"--ulimit", "nofile=1024:1024",
|
|
139
219
|
"-v", f"{_bind_mount_source(workdir)}:{CONTAINER_WORKDIR}",
|
|
220
|
+
# After the workdir mount, never before: these land *inside* it, at
|
|
221
|
+
# /work/inputs/<label>, and the outer mount has to exist first.
|
|
222
|
+
*local_data_mounts(local_inputs, local_data),
|
|
140
223
|
"-w", CONTAINER_WORKDIR,
|
|
141
224
|
]
|
|
@@ -13,6 +13,7 @@ import socket
|
|
|
13
13
|
|
|
14
14
|
import psutil
|
|
15
15
|
|
|
16
|
+
from flashnode.config.local_data import load_local_data
|
|
16
17
|
from flashruntime.protocol.v1alpha1 import (
|
|
17
18
|
NodeCapabilities,
|
|
18
19
|
NodeEnvironment,
|
|
@@ -110,6 +111,16 @@ def discover(node_id: str, kubernetes_node: str,
|
|
|
110
111
|
# coordinator's module gate is fail-open (unlike argv_capable), so
|
|
111
112
|
# the default here matches every caller that doesn't pass it.
|
|
112
113
|
module_capable=module_capable,
|
|
114
|
+
# The LABELS of the datasets this host owner lends to tasks
|
|
115
|
+
# (FLASHNODE_LOCAL_DATA) — never the paths. The coordinator needs the
|
|
116
|
+
# names to place a job that requires `patients` on a machine that has
|
|
117
|
+
# it; it has no use for `/srv/data/patients-2026`, which would leak
|
|
118
|
+
# the owner's directory layout, their username, and often the
|
|
119
|
+
# dataset's identity off the machine the whole feature exists to keep
|
|
120
|
+
# the data on. Sorted so the wire form is stable across restarts.
|
|
121
|
+
# A malformed value raises out of here rather than advertising a
|
|
122
|
+
# subset: an owner who typed it wrong must find out at startup.
|
|
123
|
+
local_datasets=sorted(load_local_data()),
|
|
113
124
|
pool=labels.get("flashml.dev/pool", os.environ.get("FLASHNODE_POOL", "local")),
|
|
114
125
|
runtime_profile=os.environ.get("FLASHNODE_RUNTIME_PROFILE", "kubernetes"),
|
|
115
126
|
labels=labels,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flashnode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Open host agent for the FlashML fragmented-compute network: join, benchmark, execute sandboxed ML tasks, earn contribution credits.
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/Zolli-Labs/flashnode
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
|
-
Requires-Dist: flashruntime<0.
|
|
10
|
+
Requires-Dist: flashruntime<0.5,>=0.4
|
|
11
11
|
Requires-Dist: psutil>=5.9
|
|
12
12
|
Requires-Dist: websockets>=12
|
|
13
13
|
Requires-Dist: cryptography>=42
|
|
@@ -60,8 +60,17 @@ flashnode work --coordinator http://<coordinator>:8100
|
|
|
60
60
|
# FLASHNODE_WORKDIR=$HOME/.cache/flashnode (macOS + colima: VM-visible workdirs)
|
|
61
61
|
# FLASHNODE_WORKDIR=C:\Users\<you>\.flashnode (Windows: must be under a
|
|
62
62
|
# directory Docker Desktop shares)
|
|
63
|
+
# FLASHNODE_LOCAL_DATA=patients=/srv/data/patients-2026,labs=/srv/labs
|
|
64
|
+
# lend local directories to tasks by LABEL
|
|
63
65
|
```
|
|
64
66
|
|
|
67
|
+
`FLASHNODE_LOCAL_DATA` lets you offer data **without uploading it**. Only the
|
|
68
|
+
label names (`patients`, `labs`) are advertised to the coordinator — never the
|
|
69
|
+
paths — and a task that names a label in its `local_inputs` gets that directory
|
|
70
|
+
bind-mounted **read-only** at `/work/inputs/<label>`. A task asking for a label
|
|
71
|
+
this machine does not lend is refused, not run half-fed; a task that asks for
|
|
72
|
+
nothing sees nothing, exactly as before.
|
|
73
|
+
|
|
65
74
|
If the coordinator enforces per-machine authentication
|
|
66
75
|
(`FLASHML_NODE_TOKENS` set server-side), save the bearer token you were
|
|
67
76
|
given before running `work`:
|
|
@@ -6,12 +6,13 @@ flashnode/agent/kube.py,sha256=3olz_oVv0RdDNogFwakiXyUD-q8J7PKVZ5_pABWiQew,1400
|
|
|
6
6
|
flashnode/artifacts/__init__.py,sha256=J3VdNm08o0AL1kLD_cjsxmGgm8e_uhwnljvYMI5nmek,177
|
|
7
7
|
flashnode/benchmark/__init__.py,sha256=YC66KYR-W7NANMtV0Y8kpixdzBxslgsaK9mDYaDxbBI,4168
|
|
8
8
|
flashnode/config/__init__.py,sha256=t6C8iJkWyIGJmdv9kqh30iFkcw6vkdy73UkPnvDbZhs,4404
|
|
9
|
+
flashnode/config/local_data.py,sha256=Q-eFDd9BM7UkHkA9SYN4Mcv3xBFsTeSuZMjQIKqPxFc,4919
|
|
9
10
|
flashnode/executor/__init__.py,sha256=6wQKrF-0zl5jAU21_xkeNA3P3iQ5jhbx0IFUlXdjkNU,1035
|
|
10
11
|
flashnode/executor/archives.py,sha256=HsFpwR_MyICSp7wkw4Wr_4rmobvnaT_d2l2I6LSNJkI,12472
|
|
11
|
-
flashnode/executor/argv_runner.py,sha256=
|
|
12
|
+
flashnode/executor/argv_runner.py,sha256=fKc9Fk0RdJdFVAWAzKEvqLp8cLtvxQALaTBj0QeWszk,5586
|
|
12
13
|
flashnode/executor/client.py,sha256=0mSkjIrb1M66Jz9ofL-OqM7fu13arrMjEqTfKgYl2a8,7473
|
|
13
|
-
flashnode/executor/docker_runner.py,sha256=
|
|
14
|
-
flashnode/executor/hardening.py,sha256=
|
|
14
|
+
flashnode/executor/docker_runner.py,sha256=6o6rB7RarSoAd3I1UGB6le92d6J5flNJ7IArI6G4Grw,5315
|
|
15
|
+
flashnode/executor/hardening.py,sha256=irdvMGMbupbBJdzsCfLWN6PHmSDlXRGMSqMCIcMaWtg,9969
|
|
15
16
|
flashnode/executor/images.py,sha256=OGJt36m1HKwUOErN_MR_sixG3gxIFKH8htfTVK7Opeg,7563
|
|
16
17
|
flashnode/executor/loop.py,sha256=E_gBh6A-T3oEQmR-lvYDR2k5NXrYSySX61RIiOgy4To,16847
|
|
17
18
|
flashnode/executor/runner.py,sha256=AT6M80QDB9p8GFIpT7rgym2Tus-IUNnh5eBpIPavyx0,4218
|
|
@@ -20,11 +21,11 @@ flashnode/identity/credentials.py,sha256=y-OGUYEyv5J5z_FUT01tSYWGdpPfC9z0F6sX5Ay
|
|
|
20
21
|
flashnode/identity/enrol.py,sha256=GHvBLQDWdCTfDR45CQ1C70Jd00wFINfcxzvcDRC4ofg,6622
|
|
21
22
|
flashnode/identity/store.py,sha256=ZGCPhSiH63PAE3GRPGrlhlc2nNPgstDSMtNPu4N-OCE,1734
|
|
22
23
|
flashnode/inventory/__init__.py,sha256=epNMD283icBq-jjg5rN0u51tpSWm1RTw8PhkAXr88fM,193
|
|
23
|
-
flashnode/inventory/capabilities.py,sha256=
|
|
24
|
+
flashnode/inventory/capabilities.py,sha256=3jMrE7Ua41O5FqU_keoNz8t4ExagDZgqYFGxVHmEfa4,5377
|
|
24
25
|
flashnode/telemetry/__init__.py,sha256=MRJ99bQykL1do3dQYMNiQABmYhTuEkgZdfsm5Np0KD4,3363
|
|
25
|
-
flashnode-0.
|
|
26
|
-
flashnode-0.
|
|
27
|
-
flashnode-0.
|
|
28
|
-
flashnode-0.
|
|
29
|
-
flashnode-0.
|
|
30
|
-
flashnode-0.
|
|
26
|
+
flashnode-0.3.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
27
|
+
flashnode-0.3.0.dist-info/METADATA,sha256=4LPXSLr0DK4mv7R9Z1amKxAb_FRe5qrs58-MCNJ0Yho,8114
|
|
28
|
+
flashnode-0.3.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
29
|
+
flashnode-0.3.0.dist-info/entry_points.txt,sha256=rn0W3v0MU8yhzp6b3j9frGA92h5bN3vgqMQRuYAoBkM,55
|
|
30
|
+
flashnode-0.3.0.dist-info/top_level.txt,sha256=BErbFoSvml51DxXayRtL5l8_1p59Z4XnC2bmKeaWdBo,10
|
|
31
|
+
flashnode-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|