alissa-tools-github-devloop 0.8.58__py3-none-any.whl → 0.8.60__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.
- alissa/tools/github/devloop/loop.py +186 -12
- alissa/tools/github/devloop/loop_events.py +1 -1
- alissa/tools/github/devloop/state.py +17 -0
- alissa/tools/github/devloop/version +1 -1
- {alissa_tools_github_devloop-0.8.58.dist-info → alissa_tools_github_devloop-0.8.60.dist-info}/METADATA +1 -1
- {alissa_tools_github_devloop-0.8.58.dist-info → alissa_tools_github_devloop-0.8.60.dist-info}/RECORD +11 -11
- {alissa_tools_github_devloop-0.8.58.dist-info → alissa_tools_github_devloop-0.8.60.dist-info}/WHEEL +0 -0
- {alissa_tools_github_devloop-0.8.58.dist-info → alissa_tools_github_devloop-0.8.60.dist-info}/entry_points.txt +0 -0
- {alissa_tools_github_devloop-0.8.58.dist-info → alissa_tools_github_devloop-0.8.60.dist-info}/licenses/LICENSE +0 -0
- {alissa_tools_github_devloop-0.8.58.dist-info → alissa_tools_github_devloop-0.8.60.dist-info}/licenses/NOTICE +0 -0
- {alissa_tools_github_devloop-0.8.58.dist-info → alissa_tools_github_devloop-0.8.60.dist-info}/top_level.txt +0 -0
|
@@ -547,11 +547,16 @@ _CRITERIA_RULE = (
|
|
|
547
547
|
)
|
|
548
548
|
|
|
549
549
|
# The adopt half: a resumed or fix-round session inherits a task some other
|
|
550
|
-
# session typed, possibly before this rule existed.
|
|
550
|
+
# session typed, possibly before this rule existed. No worker surface can
|
|
551
|
+
# edit, retype or add a criterion on an existing task (the REST API only
|
|
552
|
+
# creates tasks with criteria and satisfies them; the CLI has neither), so
|
|
553
|
+
# the clause asks for the one thing a worker can do: put each breach in front
|
|
554
|
+
# of the validator, who reads the delivery note.
|
|
551
555
|
_CRITERIA_ADOPT = (
|
|
552
|
-
" On a task you ADOPT,
|
|
553
|
-
"
|
|
554
|
-
"
|
|
556
|
+
" On a task you ADOPT, leave its criteria as they are — no tool you have "
|
|
557
|
+
"can edit, retype or add one — and list every criterion that breaks this "
|
|
558
|
+
"rule, with the type it should carry (a fact typed `human` should be "
|
|
559
|
+
"`evidence`), under the delivery note's Judgment calls."
|
|
555
560
|
)
|
|
556
561
|
|
|
557
562
|
# The origin-task instruction is split in two because the downstream rule is
|
|
@@ -1889,6 +1894,16 @@ LIVE_STALLED_COMMENT = (
|
|
|
1889
1894
|
"`{classified}`."
|
|
1890
1895
|
)
|
|
1891
1896
|
|
|
1897
|
+
ASSIGNED_OUTSIDE_COMMENT = (
|
|
1898
|
+
"**Dev loop skipping this issue** — it has been assigned to `{login}` for "
|
|
1899
|
+
"at least {minutes} min, but this daemon has no spawn for it on its "
|
|
1900
|
+
"ledger and no spawn line of its own on the activity comment, so the "
|
|
1901
|
+
"assignment was made outside the loop (by hand, or under another "
|
|
1902
|
+
"workspace's ledger). The daemon will not spawn over it. Operator "
|
|
1903
|
+
"options: unassign `{login}` to let the loop take the issue, or leave it "
|
|
1904
|
+
"if someone is working it by hand. (One-time notice.)"
|
|
1905
|
+
)
|
|
1906
|
+
|
|
1892
1907
|
# The operator page for an account-level prompt kind (issue #127): an
|
|
1893
1908
|
# expired login, a hit usage limit, an empty credit balance. No keystroke
|
|
1894
1909
|
# answers these, so the page is the act -- one per kind per
|
|
@@ -2253,6 +2268,16 @@ ESCALATION_FIX_HANDOVER = "fix-handover"
|
|
|
2253
2268
|
# neither ping may silence the other.
|
|
2254
2269
|
ESCALATION_LIVE_STALLED = "live-stalled"
|
|
2255
2270
|
|
|
2271
|
+
# The assigned-outside skip's once-per-issue operator page (issue #200): the
|
|
2272
|
+
# issue is assigned to this login, the ledger has no row and its activity
|
|
2273
|
+
# comment no spawn line of ours, so nothing the daemon can see explains the
|
|
2274
|
+
# assignment. Once-ever like ESCALATION_ASSIGNMENT -- the remedy (unassign,
|
|
2275
|
+
# or leave it to whoever is working it by hand) does not change per poll.
|
|
2276
|
+
# ASSIGNED_OUTSIDE_SEEN is the episode's durable first-seen clock, kept in the
|
|
2277
|
+
# activity_log ledger so a daemon restart does not restart the wait.
|
|
2278
|
+
ESCALATION_ASSIGNED_OUTSIDE = "assigned-outside"
|
|
2279
|
+
ASSIGNED_OUTSIDE_SEEN = "assigned-outside-seen"
|
|
2280
|
+
|
|
2256
2281
|
# The wedge kill's once-per-episode marker (see wedge_kind). Not an operator
|
|
2257
2282
|
# page -- the daemon acted instead of asking -- but, like ESCALATION_REREQUEST,
|
|
2258
2283
|
# a once-per-key act the `escalations` table is the ledger of: it dedupes the
|
|
@@ -4832,14 +4857,21 @@ class DevWatcher:
|
|
|
4832
4857
|
return self._claim_deferral(claim, attempts)
|
|
4833
4858
|
|
|
4834
4859
|
if login in issue.assignees and age is None:
|
|
4835
|
-
# Self-assigned but the ledger never spawned:
|
|
4836
|
-
#
|
|
4837
|
-
#
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
)
|
|
4860
|
+
# Self-assigned but the ledger never spawned. Issue #200: when the
|
|
4861
|
+
# issue's own activity comment carries an EXPIRED spawn line by
|
|
4862
|
+
# this login on this lane (the claim above already let it age
|
|
4863
|
+
# out), the assignment is a dead attempt of ours whose ledger row
|
|
4864
|
+
# was lost (an overlapping deploy wrote it) -- adopt it into the
|
|
4865
|
+
# ledger and let the ordinary stale matrix below decide, exactly
|
|
4866
|
+
# as if the row had been there all along.
|
|
4867
|
+
orphans = self._orphaned_spawns(issue, session_name(issue, 1))
|
|
4868
|
+
if orphans:
|
|
4869
|
+
attempts, age = self._adopt_orphaned_spawns(issue, orphans)
|
|
4870
|
+
else:
|
|
4871
|
+
# No spawn line at all: assigned outside this daemon (by
|
|
4872
|
+
# hand, or under another workspace's ledger). Not ours to
|
|
4873
|
+
# double-book -- but not silent forever either.
|
|
4874
|
+
return self._assigned_outside(issue, login)
|
|
4843
4875
|
|
|
4844
4876
|
# Staleness needs THREE signals, not one: the ledger timer says the
|
|
4845
4877
|
# newest spawn is old, but elapsed time alone cannot tell a dead
|
|
@@ -10177,6 +10209,148 @@ class DevWatcher:
|
|
|
10177
10209
|
defer_token=loop_events_mod.DEFER_CLAIM,
|
|
10178
10210
|
)
|
|
10179
10211
|
|
|
10212
|
+
def _orphaned_spawns(
|
|
10213
|
+
self, issue: Issue, session: str
|
|
10214
|
+
) -> "list[ActivityClaim]":
|
|
10215
|
+
"""The EXPIRED spawn lines this login wrote on the issue's activity
|
|
10216
|
+
comment for `session`'s lane, oldest first and one per session name
|
|
10217
|
+
(issue #200). Called only when the issue is self-assigned and the
|
|
10218
|
+
ledger has no row for it, i.e. after `external_claim` has already
|
|
10219
|
+
turned down every line as not fresh: what is left is a dead attempt
|
|
10220
|
+
of ours whose ledger row was lost (written by an overlapping
|
|
10221
|
+
deployment, or before a ledger loss).
|
|
10222
|
+
|
|
10223
|
+
Own authorship comes from `marked_comment`, the lane from
|
|
10224
|
+
`activity_lane`. Same switch and fail-open rule as `external_claim`:
|
|
10225
|
+
with claims disabled, or on any read failure, the answer is "no
|
|
10226
|
+
lines", which keeps today's skip."""
|
|
10227
|
+
if not self.config.activity_claims_enabled:
|
|
10228
|
+
return []
|
|
10229
|
+
try:
|
|
10230
|
+
comment = self.github.marked_comment(
|
|
10231
|
+
issue.owner, issue.repo, issue.number, ACTIVITY_MARKER
|
|
10232
|
+
)
|
|
10233
|
+
except Exception as exc:
|
|
10234
|
+
log.warning(
|
|
10235
|
+
"could not read the activity comment on %s (%s) — no "
|
|
10236
|
+
"orphaned attempt to adopt this poll",
|
|
10237
|
+
issue.issue_slug,
|
|
10238
|
+
exc,
|
|
10239
|
+
)
|
|
10240
|
+
return []
|
|
10241
|
+
if comment is None:
|
|
10242
|
+
return []
|
|
10243
|
+
lane = activity_lane(session)
|
|
10244
|
+
window = self.config.stale_minutes * 60
|
|
10245
|
+
oldest: "dict[str, ActivityClaim]" = {}
|
|
10246
|
+
for claim in activity_claims(comment.body):
|
|
10247
|
+
if activity_lane(claim.session) != lane or claim.age < window:
|
|
10248
|
+
continue
|
|
10249
|
+
known = oldest.get(claim.session)
|
|
10250
|
+
if known is None or claim.at < known.at:
|
|
10251
|
+
oldest[claim.session] = claim
|
|
10252
|
+
return sorted(oldest.values(), key=lambda claim: claim.at)
|
|
10253
|
+
|
|
10254
|
+
def _adopt_orphaned_spawns(
|
|
10255
|
+
self, issue: Issue, orphans: "list[ActivityClaim]"
|
|
10256
|
+
) -> "tuple[int, float]":
|
|
10257
|
+
"""Record each orphaned spawn line as a ledger row stamped with the
|
|
10258
|
+
line's own time, so attempt_cap counts it and the ordinary stale
|
|
10259
|
+
matrix (linked-PR probe, tmux liveness, resume) judges it exactly as
|
|
10260
|
+
a row that was never lost. Returns the (attempts, age) the caller
|
|
10261
|
+
continues with.
|
|
10262
|
+
|
|
10263
|
+
Rows are numbered 1..k in line order (the ledger has none for this
|
|
10264
|
+
issue), keeping the session name the line carries; numbering by the
|
|
10265
|
+
name's own suffix could REPLACE a row when a line is missing. Not
|
|
10266
|
+
written on a dry run (no ledger write is), but the returned values
|
|
10267
|
+
let the dry pass reach the same decision."""
|
|
10268
|
+
for attempt, orphan in enumerate(orphans, start=1):
|
|
10269
|
+
log.warning(
|
|
10270
|
+
"%s: adopted orphaned attempt %d from activity log (`%s`, "
|
|
10271
|
+
"spawned %d min ago by this login, no local ledger row) — "
|
|
10272
|
+
"taking the stale path",
|
|
10273
|
+
issue.issue_slug,
|
|
10274
|
+
attempt,
|
|
10275
|
+
orphan.session,
|
|
10276
|
+
int(orphan.age / 60),
|
|
10277
|
+
)
|
|
10278
|
+
if self.config.dry_run:
|
|
10279
|
+
continue
|
|
10280
|
+
self.state.record_spawn(
|
|
10281
|
+
repo_slug=issue.full_name,
|
|
10282
|
+
issue=issue.number,
|
|
10283
|
+
attempt=attempt,
|
|
10284
|
+
session=orphan.session,
|
|
10285
|
+
)
|
|
10286
|
+
# The row just written is the newest; move its clock back to the
|
|
10287
|
+
# line's, so its age says what the line says.
|
|
10288
|
+
self.state.age_out_spawn(issue.full_name, issue.number, orphan.at)
|
|
10289
|
+
return len(orphans), orphans[-1].age
|
|
10290
|
+
|
|
10291
|
+
def _assigned_outside(self, issue: Issue, login: str) -> Decision:
|
|
10292
|
+
"""The skip for an issue assigned to this login with nothing on the
|
|
10293
|
+
ledger or the activity comment to explain it -- assigned by hand.
|
|
10294
|
+
Not silent forever (issue #200): once the skip has persisted for
|
|
10295
|
+
STALLED_DEFER_MULTIPLE stale windows (clock: the episode's durable
|
|
10296
|
+
first-seen row) it posts ONE operator comment, recorded only after
|
|
10297
|
+
it lands so a failed post retries next poll."""
|
|
10298
|
+
full_name, number = issue.full_name, issue.number
|
|
10299
|
+
reason = (
|
|
10300
|
+
f"assigned to {login} outside this daemon (no spawn on the "
|
|
10301
|
+
f"ledger) — unassign the issue to let the loop take it"
|
|
10302
|
+
)
|
|
10303
|
+
if self.state.escalated(full_name, number, ESCALATION_ASSIGNED_OUTSIDE):
|
|
10304
|
+
return Decision(Action.SKIPPED, reason)
|
|
10305
|
+
if self.config.dry_run:
|
|
10306
|
+
return Decision(Action.SKIPPED, reason)
|
|
10307
|
+
first_seen = self.state.activity_logged_at(
|
|
10308
|
+
full_name, number, ASSIGNED_OUTSIDE_SEEN
|
|
10309
|
+
)
|
|
10310
|
+
if first_seen is None:
|
|
10311
|
+
self.state.record_activity(full_name, number, ASSIGNED_OUTSIDE_SEEN)
|
|
10312
|
+
first_seen = int(time.time())
|
|
10313
|
+
stalled_after = STALLED_DEFER_MULTIPLE * self.config.stale_minutes * 60
|
|
10314
|
+
waited = time.time() - first_seen
|
|
10315
|
+
if waited < stalled_after:
|
|
10316
|
+
# The page's clock is the seat's: no parking across it, or it
|
|
10317
|
+
# would fire on the first pass after the wake (issue #153).
|
|
10318
|
+
return Decision(
|
|
10319
|
+
Action.SKIPPED,
|
|
10320
|
+
reason,
|
|
10321
|
+
owed=True,
|
|
10322
|
+
timer=(
|
|
10323
|
+
parking_mod.TIMER_STALE_WINDOW, first_seen + stalled_after
|
|
10324
|
+
),
|
|
10325
|
+
)
|
|
10326
|
+
body = ASSIGNED_OUTSIDE_COMMENT.format(
|
|
10327
|
+
login=login, minutes=int(waited / 60)
|
|
10328
|
+
)
|
|
10329
|
+
log.warning(
|
|
10330
|
+
"%s has been assigned to %s outside this daemon for %.0f min — "
|
|
10331
|
+
"escalating to operator (once)",
|
|
10332
|
+
issue.issue_slug,
|
|
10333
|
+
login,
|
|
10334
|
+
waited / 60,
|
|
10335
|
+
)
|
|
10336
|
+
try:
|
|
10337
|
+
self.github.comment(issue.owner, issue.repo, number, body)
|
|
10338
|
+
except CommandError as exc:
|
|
10339
|
+
log.error(
|
|
10340
|
+
"could not post the assigned-outside comment on %s: %s — "
|
|
10341
|
+
"the page retries next poll",
|
|
10342
|
+
issue.issue_slug,
|
|
10343
|
+
exc,
|
|
10344
|
+
)
|
|
10345
|
+
return Decision(Action.SKIPPED, reason, owed=True)
|
|
10346
|
+
self.state.record_escalation(
|
|
10347
|
+
full_name, number, ESCALATION_ASSIGNED_OUTSIDE
|
|
10348
|
+
)
|
|
10349
|
+
return Decision(
|
|
10350
|
+
Action.ESCALATED,
|
|
10351
|
+
f"{reason}; operator paged after {int(waited / 60)} min",
|
|
10352
|
+
)
|
|
10353
|
+
|
|
10180
10354
|
def _log_deferral(
|
|
10181
10355
|
self,
|
|
10182
10356
|
owner: str,
|
|
@@ -189,7 +189,7 @@ _LIMIT_LIFTED_RE = re.compile(r"^usage-limit-lifted:(?P<session>[^@]+)@(?P<since
|
|
|
189
189
|
# points the operator at an unrelated PR or at nothing.
|
|
190
190
|
_ISSUE_ANCHORED = (
|
|
191
191
|
"cap", "assignment", "stalled", "live-stalled", "wedged", "first-run-dialog",
|
|
192
|
-
"claude-exited",
|
|
192
|
+
"claude-exited", "assigned-outside",
|
|
193
193
|
)
|
|
194
194
|
|
|
195
195
|
|
|
@@ -1245,6 +1245,23 @@ class State:
|
|
|
1245
1245
|
).fetchone()
|
|
1246
1246
|
return row is not None
|
|
1247
1247
|
|
|
1248
|
+
def activity_logged_at(
|
|
1249
|
+
self, repo_slug: str, number: int, kind: str
|
|
1250
|
+
) -> "int | None":
|
|
1251
|
+
"""When this activity KIND was first recorded (unix seconds), or None.
|
|
1252
|
+
The durable first-seen clock of an episode that must outlive a daemon
|
|
1253
|
+
restart (issue #200: the assigned-outside skip escalates once it has
|
|
1254
|
+
persisted long enough). A row held in memory by a failed write answers
|
|
1255
|
+
with the timestamp it would have carried."""
|
|
1256
|
+
pending = self._pending_gates.get(("activity_log", repo_slug, number, kind))
|
|
1257
|
+
if pending is not None:
|
|
1258
|
+
return int(pending)
|
|
1259
|
+
row = self._db.execute(
|
|
1260
|
+
"SELECT logged_at FROM activity_log WHERE repo_slug=? AND number=? AND kind=?",
|
|
1261
|
+
(repo_slug, number, kind),
|
|
1262
|
+
).fetchone()
|
|
1263
|
+
return None if row is None else int(row["logged_at"])
|
|
1264
|
+
|
|
1248
1265
|
def record_activity(self, repo_slug: str, number: int, kind: str) -> None:
|
|
1249
1266
|
"""Idempotent per kind: OR IGNORE keeps the first line's timestamp.
|
|
1250
1267
|
Recorded by the caller only AFTER the append lands, so a transient
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.8.
|
|
1
|
+
0.8.60
|
{alissa_tools_github_devloop-0.8.58.dist-info → alissa_tools_github_devloop-0.8.60.dist-info}/RECORD
RENAMED
|
@@ -10,13 +10,13 @@ alissa/tools/github/devloop/hubs.py,sha256=yjnsnSHLG7GNNP8X2hILeAMV6P5n4JGc_IaAx
|
|
|
10
10
|
alissa/tools/github/devloop/hungtests.py,sha256=TozIDydUe_Kn1m5EtGeGtYt_BrLqGznrNjikLYhxH-Q,24621
|
|
11
11
|
alissa/tools/github/devloop/integration_check.py,sha256=JkFwz3JqOQBIvzszmiql-IMVGSH3lDers6mJUHh9zMM,17928
|
|
12
12
|
alissa/tools/github/devloop/janitor.py,sha256=R8REoHBeY2cfW7fYJVhKr-F5z9Mjlc-vmv-z4KtvBZc,27360
|
|
13
|
-
alissa/tools/github/devloop/loop.py,sha256=
|
|
14
|
-
alissa/tools/github/devloop/loop_events.py,sha256=
|
|
13
|
+
alissa/tools/github/devloop/loop.py,sha256=RA1gBliGUGsmcUZONWg17clzN70szohgD7dSPqumZQs,704494
|
|
14
|
+
alissa/tools/github/devloop/loop_events.py,sha256=B8wq4_MMXbf1-od3pk9OHVo6F6W2lzzUZmU6GsKcCkM,38113
|
|
15
15
|
alissa/tools/github/devloop/parking.py,sha256=7h3Gg2XQ0eqlvn0M6K6NeEg9xbqnlUPrZNs9gRofzxc,12410
|
|
16
16
|
alissa/tools/github/devloop/proc.py,sha256=nzfsxJSHFWRQcrXmfv588KazFysv8dVQrhMPH1q5eRk,3134
|
|
17
17
|
alissa/tools/github/devloop/prompts.py,sha256=VzN6ec5eF2wwg7QSCRg52WLcxo-tGbGRgQbc4HSahUc,55169
|
|
18
|
-
alissa/tools/github/devloop/state.py,sha256=
|
|
19
|
-
alissa/tools/github/devloop/version,sha256=
|
|
18
|
+
alissa/tools/github/devloop/state.py,sha256=MIlE3siJjUs5YEv1-Xm-Gmur6B-Okq3M_Lu6d_zxtmA,89534
|
|
19
|
+
alissa/tools/github/devloop/version,sha256=MQj5gu3ulgK9CxgShUYHkZ5gnQUvytZl9LkGg_h0KPs,7
|
|
20
20
|
alissa/tools/github/devloop/version.py,sha256=znuuOaitXGrBpbskNy78_rlVpFdtmKmk_3k1ACxZrl0,1418
|
|
21
21
|
alissa/tools/github/devloop/webui/__init__.py,sha256=vdXYoyGYzD7BrJ5lV6XdH8Bpw_pej8-RK0hjfxop7-8,1513
|
|
22
22
|
alissa/tools/github/devloop/webui/__main__.py,sha256=wORyW5AkgsniK0TZcrOrvm_P7Q4Mr-fH-4S6qj9XcgA,6137
|
|
@@ -25,10 +25,10 @@ alissa/tools/github/devloop/webui/page.py,sha256=4OD3e01ByvVM4Wo04AZyFuyOYTFEMT2
|
|
|
25
25
|
alissa/tools/github/devloop/webui/server.py,sha256=3aGYGesjUSrPrgcpfy-oYDefBNKnAj3lGfRzwsRkv_I,27031
|
|
26
26
|
alissa/tools/github/devloop/webui/sources.py,sha256=Uw_2B5QGSmW6f66WfVbQb6yLLYmSv6uuF7HNJ5OSr_I,39314
|
|
27
27
|
alissa/tools/github/devloop/webui/sysinfo.py,sha256=ptlogH9alFv0vZ-pQfyUaA3w-K7LQS3tQHGv0d8Tdlk,9649
|
|
28
|
-
alissa_tools_github_devloop-0.8.
|
|
29
|
-
alissa_tools_github_devloop-0.8.
|
|
30
|
-
alissa_tools_github_devloop-0.8.
|
|
31
|
-
alissa_tools_github_devloop-0.8.
|
|
32
|
-
alissa_tools_github_devloop-0.8.
|
|
33
|
-
alissa_tools_github_devloop-0.8.
|
|
34
|
-
alissa_tools_github_devloop-0.8.
|
|
28
|
+
alissa_tools_github_devloop-0.8.60.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
29
|
+
alissa_tools_github_devloop-0.8.60.dist-info/licenses/NOTICE,sha256=FKstNs3uw_tQod4_1XcFLD1FUTJQXf4FC0oXJAOA_HE,260
|
|
30
|
+
alissa_tools_github_devloop-0.8.60.dist-info/METADATA,sha256=sXX8hAM08-DQtojqLRYh46EqoEPiPnsvfMzoGc6tVfA,15973
|
|
31
|
+
alissa_tools_github_devloop-0.8.60.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
32
|
+
alissa_tools_github_devloop-0.8.60.dist-info/entry_points.txt,sha256=8aC7Ajzx4bu9ZRR37a40IDfuStacvZP5KsAxY1dbFKU,145
|
|
33
|
+
alissa_tools_github_devloop-0.8.60.dist-info/top_level.txt,sha256=DodjDg-l-TWQnlxG-Vuc3G5sB4O7cWGQwe6XrVx3u-A,7
|
|
34
|
+
alissa_tools_github_devloop-0.8.60.dist-info/RECORD,,
|
{alissa_tools_github_devloop-0.8.58.dist-info → alissa_tools_github_devloop-0.8.60.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|