alissa-tools-github-devloop 0.1.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.
- alissa/tools/github/devloop/__init__.py +11 -0
- alissa/tools/github/devloop/__main__.py +217 -0
- alissa/tools/github/devloop/alissa.py +75 -0
- alissa/tools/github/devloop/config.py +359 -0
- alissa/tools/github/devloop/ghclient.py +406 -0
- alissa/tools/github/devloop/loop.py +620 -0
- alissa/tools/github/devloop/proc.py +56 -0
- alissa/tools/github/devloop/state.py +113 -0
- alissa/tools/github/devloop/version +1 -0
- alissa/tools/github/devloop/version.py +46 -0
- alissa_tools_github_devloop-0.1.0.dist-info/METADATA +61 -0
- alissa_tools_github_devloop-0.1.0.dist-info/RECORD +15 -0
- alissa_tools_github_devloop-0.1.0.dist-info/WHEEL +5 -0
- alissa_tools_github_devloop-0.1.0.dist-info/entry_points.txt +2 -0
- alissa_tools_github_devloop-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Local spawn ledger.
|
|
2
|
+
|
|
3
|
+
Deliberately thin: GitHub is the source of truth for whether an issue is in
|
|
4
|
+
flight (the daemon self-assigns before spawning). This table exists only to
|
|
5
|
+
count developer attempts per issue, to age the newest spawn so a stale one
|
|
6
|
+
can be retried, and to remember which escalations were already raised.
|
|
7
|
+
|
|
8
|
+
Escalations are keyed per KIND ("cap", "assignment", "stalled", ...): each
|
|
9
|
+
condition has its own operator remedy (raise attempt_cap; grant push access;
|
|
10
|
+
check the developer session / close its PR), so each kind's comment must
|
|
11
|
+
never silence another's -- each kind dedupes independently. Kind is
|
|
12
|
+
free-form TEXT: a caller can narrow a kind's dedupe scope by folding more
|
|
13
|
+
key into the string (the stalled ping is keyed per deferral episode,
|
|
14
|
+
"stalled:a<attempt>" -- see loop.stalled_kind).
|
|
15
|
+
|
|
16
|
+
Keying: every `repo_slug` is the full `owner/repo` slug (`Issue.full_name`),
|
|
17
|
+
never the bare repo name. Two owners can host repos with the same bare name,
|
|
18
|
+
and a bare-name key would collide their ledgers into one row.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import sqlite3
|
|
24
|
+
import time
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
SCHEMA = """
|
|
28
|
+
CREATE TABLE IF NOT EXISTS spawns (
|
|
29
|
+
repo_slug TEXT NOT NULL,
|
|
30
|
+
issue INTEGER NOT NULL,
|
|
31
|
+
attempt INTEGER NOT NULL,
|
|
32
|
+
session TEXT NOT NULL,
|
|
33
|
+
spawned_at INTEGER NOT NULL,
|
|
34
|
+
PRIMARY KEY (repo_slug, issue, attempt)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS escalations (
|
|
38
|
+
repo_slug TEXT NOT NULL,
|
|
39
|
+
issue INTEGER NOT NULL,
|
|
40
|
+
kind TEXT NOT NULL,
|
|
41
|
+
escalated_at INTEGER NOT NULL,
|
|
42
|
+
PRIMARY KEY (repo_slug, issue, kind)
|
|
43
|
+
);
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class State:
|
|
48
|
+
def __init__(self, path: Path):
|
|
49
|
+
path = Path(path).expanduser()
|
|
50
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
self._db = sqlite3.connect(str(path))
|
|
52
|
+
self._db.row_factory = sqlite3.Row
|
|
53
|
+
self._db.executescript(SCHEMA)
|
|
54
|
+
self._db.commit()
|
|
55
|
+
|
|
56
|
+
def close(self) -> None:
|
|
57
|
+
self._db.close()
|
|
58
|
+
|
|
59
|
+
def __enter__(self) -> "State":
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def __exit__(self, *exc) -> None:
|
|
63
|
+
self.close()
|
|
64
|
+
|
|
65
|
+
def record_spawn(self, *, repo_slug: str, issue: int, attempt: int, session: str) -> None:
|
|
66
|
+
"""Record a developer spawn. `repo_slug` is `owner/repo`."""
|
|
67
|
+
self._db.execute(
|
|
68
|
+
"INSERT OR REPLACE INTO spawns "
|
|
69
|
+
"(repo_slug, issue, attempt, session, spawned_at) VALUES (?,?,?,?,?)",
|
|
70
|
+
(repo_slug, issue, attempt, session, int(time.time())),
|
|
71
|
+
)
|
|
72
|
+
self._db.commit()
|
|
73
|
+
|
|
74
|
+
def attempt_count(self, repo_slug: str, issue: int) -> int:
|
|
75
|
+
row = self._db.execute(
|
|
76
|
+
"SELECT COUNT(*) AS n FROM spawns WHERE repo_slug=? AND issue=?",
|
|
77
|
+
(repo_slug, issue),
|
|
78
|
+
).fetchone()
|
|
79
|
+
return int(row["n"])
|
|
80
|
+
|
|
81
|
+
def spawn_age(self, repo_slug: str, issue: int) -> float | None:
|
|
82
|
+
"""Seconds since the NEWEST spawn for this issue, or None if never
|
|
83
|
+
spawned. Newest, not first: a retry resets the staleness clock.
|
|
84
|
+
|
|
85
|
+
Clamped at 0.0: `spawned_at` is wall-clock, so an NTP or manual clock
|
|
86
|
+
correction can put it in the future. A negative age is treated as
|
|
87
|
+
"just spawned / unknown", never propagated."""
|
|
88
|
+
row = self._db.execute(
|
|
89
|
+
"SELECT MAX(spawned_at) AS ts FROM spawns WHERE repo_slug=? AND issue=?",
|
|
90
|
+
(repo_slug, issue),
|
|
91
|
+
).fetchone()
|
|
92
|
+
return None if row["ts"] is None else max(0.0, time.time() - row["ts"])
|
|
93
|
+
|
|
94
|
+
def escalated(self, repo_slug: str, issue: int, kind: str) -> bool:
|
|
95
|
+
"""Whether an escalation of this KIND was already raised. Kinds
|
|
96
|
+
dedupe independently -- a cap-out row never silences an
|
|
97
|
+
assignment-rejection comment, and vice versa."""
|
|
98
|
+
row = self._db.execute(
|
|
99
|
+
"SELECT 1 FROM escalations WHERE repo_slug=? AND issue=? AND kind=?",
|
|
100
|
+
(repo_slug, issue, kind),
|
|
101
|
+
).fetchone()
|
|
102
|
+
return row is not None
|
|
103
|
+
|
|
104
|
+
def record_escalation(self, repo_slug: str, issue: int, kind: str) -> None:
|
|
105
|
+
"""Idempotent per kind: OR IGNORE keeps the FIRST escalation's
|
|
106
|
+
timestamp, so `escalated_at` is an audit field for when this kind of
|
|
107
|
+
escalation was first raised, not the most recent re-raise."""
|
|
108
|
+
self._db.execute(
|
|
109
|
+
"INSERT OR IGNORE INTO escalations "
|
|
110
|
+
"(repo_slug, issue, kind, escalated_at) VALUES (?,?,?,?)",
|
|
111
|
+
(repo_slug, issue, kind, int(time.time())),
|
|
112
|
+
)
|
|
113
|
+
self._db.commit()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True, slots=True)
|
|
6
|
+
class Version:
|
|
7
|
+
name: str
|
|
8
|
+
value: str
|
|
9
|
+
|
|
10
|
+
def components(self, as_int: bool = False) -> list:
|
|
11
|
+
return [int(val) if as_int else val for val in self.value.split(".")]
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def major(self) -> int:
|
|
15
|
+
component, *_ = self.components(as_int=True)
|
|
16
|
+
return component
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def minor(self) -> int:
|
|
20
|
+
_, component, *_ = self.components(as_int=True)
|
|
21
|
+
return component
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def patch(self) -> int:
|
|
25
|
+
*_, component = self.components(as_int=True)
|
|
26
|
+
return component
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_path(cls, dirpath: str, name: str):
|
|
30
|
+
for file in os.listdir(dirpath):
|
|
31
|
+
if file.lower().endswith("version"):
|
|
32
|
+
filepath = os.path.join(dirpath, file)
|
|
33
|
+
break
|
|
34
|
+
else:
|
|
35
|
+
raise ValueError("Version file not found for package name: " + name)
|
|
36
|
+
|
|
37
|
+
with open(filepath, "r") as version_file:
|
|
38
|
+
version_value = version_file.readline().strip()
|
|
39
|
+
return cls(name=name, value=version_value)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
version = Version.from_path(name="alissa-tools-github-devloop", dirpath=os.path.dirname(__file__))
|
|
44
|
+
except Exception:
|
|
45
|
+
print("Version file not found for package name: alissa-tools-github-devloop, using 0.0.0")
|
|
46
|
+
version = Version(name="alissa-tools-github-devloop", value="0.0.0")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: alissa-tools-github-devloop
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ALISSA-TOOLS-GITHUB-DEVLOOP
|
|
5
|
+
Home-page: https://alissa.app
|
|
6
|
+
Author: Fahera
|
|
7
|
+
Author-email: support@alissa.app
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Dynamic: author
|
|
11
|
+
Dynamic: author-email
|
|
12
|
+
Dynamic: description
|
|
13
|
+
Dynamic: description-content-type
|
|
14
|
+
Dynamic: home-page
|
|
15
|
+
Dynamic: requires-python
|
|
16
|
+
Dynamic: summary
|
|
17
|
+
|
|
18
|
+
# alissa-tools-github-devloop
|
|
19
|
+
|
|
20
|
+
The `alissa.tools.github.devloop` module: a GitHub watcher that turns
|
|
21
|
+
`alissa:develop`-labeled issues into fresh developer sessions — the
|
|
22
|
+
developer-side counterpart of
|
|
23
|
+
[`alissa-tools-github-reviewloop`](https://github.com/fahera-mx/alissa-github-review-daemon).
|
|
24
|
+
|
|
25
|
+
This distribution ships **only** that module. Everything above it —
|
|
26
|
+
`alissa`, `alissa.tools`, `alissa.tools.github` — is a
|
|
27
|
+
[PEP 420](https://peps.python.org/pep-0420/) namespace package with no
|
|
28
|
+
`__init__.py`, so other distributions in other repositories can contribute
|
|
29
|
+
their own packages under the same namespace:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
alissa/ ← namespace (no __init__.py)
|
|
33
|
+
└── tools/ ← namespace
|
|
34
|
+
└── github/ ← namespace
|
|
35
|
+
├── devloop/ __init__.py ← THIS distribution
|
|
36
|
+
└── reviewloop/ __init__.py ← ships from alissa-github-review-daemon
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The rule: a distribution owns a leaf package and declares only that subtree
|
|
40
|
+
(`find_namespace_packages(include=[PACKAGE, f"{PACKAGE}.*"])`). Adding an
|
|
41
|
+
`__init__.py` at any namespace level would claim it for one distribution and
|
|
42
|
+
shadow the others.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
pip install -e ./alissa-tools-github-devloop
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Console scripts
|
|
51
|
+
|
|
52
|
+
| Command | Entry point |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `alissa-devloop` | `alissa.tools.github.devloop.__main__:main` |
|
|
55
|
+
|
|
56
|
+
## Layout
|
|
57
|
+
|
|
58
|
+
`src/main` holds the package tree, `src/test` mirrors it as `test_*`. The
|
|
59
|
+
distribution version lives in the plain-text `version` file next to the module
|
|
60
|
+
it versions (`src/main/alissa/tools/github/devloop/version`), read by both
|
|
61
|
+
`setup.py` and `version.py`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
alissa/tools/github/devloop/__init__.py,sha256=PQXfVmykwq6E1S3g0c49wW6v8jB3y8svslqXUWd2NRI,506
|
|
2
|
+
alissa/tools/github/devloop/__main__.py,sha256=qXxJxWSRfRNa6EvctidvvdAMy0HuPtZpO8sn-tdzGHo,7772
|
|
3
|
+
alissa/tools/github/devloop/alissa.py,sha256=b2Wbpi7_K-BCOz608UTU98R960BeFRbjLaCpHf5Qh-8,2660
|
|
4
|
+
alissa/tools/github/devloop/config.py,sha256=TMtdq5Zt9nWOdRWuKmZAPqT1nJuzbYqezCfQqQ_DOfY,13678
|
|
5
|
+
alissa/tools/github/devloop/ghclient.py,sha256=nhx4bupNvbyhGrQTUyJ2BA8HCmROa2sYx5YFFtEs1J4,17870
|
|
6
|
+
alissa/tools/github/devloop/loop.py,sha256=6TD3mmmuiHDK6XzNn-3-V3HeuxNLJvoYRkfs1LL1HTQ,27657
|
|
7
|
+
alissa/tools/github/devloop/proc.py,sha256=_dDr3g76WEf2DRyLUz1Dzl4KvLVptJant_UZOrRixNk,1649
|
|
8
|
+
alissa/tools/github/devloop/state.py,sha256=nUR5v5CzM7jnx5g32tLEYgQmLiYEXcIxaRLu2YeUPg0,4443
|
|
9
|
+
alissa/tools/github/devloop/version,sha256=6d2FB_S_DG9CRY5BrqgzrQvT9hJycjNe7pv01YVB7Wc,6
|
|
10
|
+
alissa/tools/github/devloop/version.py,sha256=znuuOaitXGrBpbskNy78_rlVpFdtmKmk_3k1ACxZrl0,1418
|
|
11
|
+
alissa_tools_github_devloop-0.1.0.dist-info/METADATA,sha256=44ZvqeNYyHQre4kHPs6S8x34mahJQcHHuiQALPaEsUs,2093
|
|
12
|
+
alissa_tools_github_devloop-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
13
|
+
alissa_tools_github_devloop-0.1.0.dist-info/entry_points.txt,sha256=yrExoFJZtWoSycWqsEoYYz-lvsCtXj4-TCGaebQSmbY,77
|
|
14
|
+
alissa_tools_github_devloop-0.1.0.dist-info/top_level.txt,sha256=DodjDg-l-TWQnlxG-Vuc3G5sB4O7cWGQwe6XrVx3u-A,7
|
|
15
|
+
alissa_tools_github_devloop-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
alissa
|