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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ alissa-devloop = alissa.tools.github.devloop.__main__:main