awindex 0.1.0__tar.gz

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.
awindex-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aitherium
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
awindex-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: awindex
3
+ Version: 0.1.0
4
+ Summary: Resolve .aither keywords and read the AitherNet Index (index.aitherium.com)
5
+ Author: Aitherium
6
+ License: MIT
7
+ Project-URL: Homepage, https://index.aitherium.com
8
+ Project-URL: Source, https://github.com/Aitherium/aithernet-index
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Dynamic: license-file
13
+
14
+ # awindex
15
+
16
+ The AitherNet Index from the shell: a hand-curated map of the internet, kept in git.
17
+ Every entry says who checked it and how.
18
+
19
+ ```bash
20
+ pip install awindex
21
+ awindex resolve neocities # route + trust tier; exit 1 = not indexed
22
+ awindex search "digital garden"
23
+ awindex show neocities.org
24
+ awindex --root records check # validate a checkout of the index repo
25
+ ```
26
+
27
+ It reads a local checkout of
28
+ [Aitherium/aithernet-index](https://github.com/Aitherium/aithernet-index) when one is
29
+ present, and https://index.aitherium.com otherwise. Stdlib only, Python 3.10+.
30
+
31
+ Trust is computed from evidence, never typed in: **submitted**, **reachable** (the bot
32
+ fetched it), **owner-proven** (the site serves `/.well-known/aithernet.json`), and
33
+ **vouched** (two people other than the submitter).
34
+
35
+ MIT. The index data is CC BY 4.0.
@@ -0,0 +1,22 @@
1
+ # awindex
2
+
3
+ The AitherNet Index from the shell: a hand-curated map of the internet, kept in git.
4
+ Every entry says who checked it and how.
5
+
6
+ ```bash
7
+ pip install awindex
8
+ awindex resolve neocities # route + trust tier; exit 1 = not indexed
9
+ awindex search "digital garden"
10
+ awindex show neocities.org
11
+ awindex --root records check # validate a checkout of the index repo
12
+ ```
13
+
14
+ It reads a local checkout of
15
+ [Aitherium/aithernet-index](https://github.com/Aitherium/aithernet-index) when one is
16
+ present, and https://index.aitherium.com otherwise. Stdlib only, Python 3.10+.
17
+
18
+ Trust is computed from evidence, never typed in: **submitted**, **reachable** (the bot
19
+ fetched it), **owner-proven** (the site serves `/.well-known/aithernet.json`), and
20
+ **vouched** (two people other than the submitter).
21
+
22
+ MIT. The index data is CC BY 4.0.
@@ -0,0 +1,7 @@
1
+ """awindex: the AitherNet Index as a library and CLI."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .netindex import TIERS, Index, Record, load, trust, validate # noqa: E402
6
+
7
+ __all__ = ["TIERS", "Index", "Record", "load", "trust", "validate", "__version__"]
@@ -0,0 +1,170 @@
1
+ """awindex: resolve .aither keywords and read the AitherNet Index from the shell.
2
+
3
+ awindex resolve neocities.aither # route + trust tier, exit 1 = not indexed
4
+ awindex search "digital garden" # matching entries
5
+ awindex show neocities.org # one record with its evidence
6
+ awindex check # validate a checkout (exit 0 / 1 / 2)
7
+ awindex --self-test
8
+
9
+ Reads a local checkout when one exists (records/ beside the package, or
10
+ AWINDEX_ROOT, or --root). Otherwise it reads the published index at
11
+ https://index.aitherium.com, so a plain `pip install` works with no clone.
12
+ Stdlib only.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import sys
20
+ import urllib.error
21
+ import urllib.request
22
+ from pathlib import Path
23
+ from typing import Optional
24
+
25
+ from . import __version__, netindex
26
+
27
+ BASE = "https://index.aitherium.com"
28
+ UA = f"awindex/{__version__}"
29
+
30
+
31
+ def _fetch(path: str) -> Optional[dict]:
32
+ req = urllib.request.Request(BASE + path, headers={"User-Agent": UA})
33
+ try:
34
+ with urllib.request.urlopen(req, timeout=15) as resp: # noqa: S310 (fixed https base)
35
+ return json.loads(resp.read().decode("utf-8"))
36
+ except urllib.error.HTTPError as exc:
37
+ if exc.code == 404:
38
+ return None
39
+ raise
40
+
41
+
42
+ def _local(root: Optional[Path]) -> Optional[netindex.Index]:
43
+ root = root or netindex.DEFAULT_ROOT
44
+ if Path(root).is_dir():
45
+ records, _ = netindex.load(Path(root))
46
+ if records:
47
+ return netindex.Index(records)
48
+ return None
49
+
50
+
51
+ def _record_dict(r: netindex.Record) -> dict:
52
+ return {"id": r.id, "kind": r.kind, "title": r.title, "url": r.url, "tags": r.tags,
53
+ "names": r.names, "route": r.route, "edges": r.edges,
54
+ "validated": r.provenance.get("validated", {}),
55
+ "trust": netindex.TIERS[netindex.trust(r)]}
56
+
57
+
58
+ def cmd_resolve(name: str, idx: Optional[netindex.Index]) -> int:
59
+ name = name.strip().lower().rstrip(".")
60
+ if not name.endswith(netindex.NAME_SUFFIX):
61
+ name += netindex.NAME_SUFFIX
62
+ if idx is not None:
63
+ hit = idx.resolve(name)
64
+ if hit:
65
+ hit["trust"] = netindex.TIERS[netindex.trust(idx.records[hit["id"]])]
66
+ else:
67
+ hit = _fetch(f"/n/{name}.json")
68
+ if not hit:
69
+ print(f"NXDOMAIN {name}")
70
+ return 1
71
+ print(json.dumps(hit, indent=2))
72
+ return 0
73
+
74
+
75
+ def _all(idx: Optional[netindex.Index]) -> list:
76
+ if idx is not None:
77
+ return [_record_dict(r) for r in idx.records.values()]
78
+ data = _fetch("/index.json") or {}
79
+ return data.get("records", [])
80
+
81
+
82
+ def cmd_search(query: str, idx: Optional[netindex.Index], limit: int) -> int:
83
+ toks = [t for t in query.lower().split() if t]
84
+ scored = []
85
+ for r in _all(idx):
86
+ hay = " ".join([r["id"], r["title"], *r.get("tags", []), *r.get("names", [])]).lower()
87
+ s = sum(1 for t in toks if t in hay)
88
+ if s:
89
+ scored.append((-s, r["id"], r))
90
+ scored.sort(key=lambda x: (x[0], x[1]))
91
+ for _, _, r in scored[:limit]:
92
+ print(f"{r['trust']:<13} {r['kind']:<8} {r['title']} {r.get('url', '')}")
93
+ return 0 if scored else 1
94
+
95
+
96
+ def cmd_show(rid: str, idx: Optional[netindex.Index]) -> int:
97
+ for r in _all(idx):
98
+ if r["id"] == rid:
99
+ print(json.dumps(r, indent=2))
100
+ return 0
101
+ print(f"not indexed: {rid}")
102
+ return 1
103
+
104
+
105
+ def cmd_check(root: Optional[Path]) -> int:
106
+ root = Path(root or netindex.DEFAULT_ROOT)
107
+ if not root.is_dir():
108
+ print(f"could not judge: {root} is not a directory")
109
+ return 2
110
+ records, findings = netindex.load(root)
111
+ if not records:
112
+ print(f"could not judge: no records under {root}")
113
+ return 2
114
+ findings += netindex.validate(records)
115
+ for f in findings:
116
+ print(f)
117
+ print(f"awindex: {len(records)} records, {len(findings)} findings")
118
+ return 1 if findings else 0
119
+
120
+
121
+ def self_test() -> int:
122
+ good = netindex._parse(None, {
123
+ "id": "a", "kind": "site", "title": "A", "url": "https://a.example",
124
+ "tags": ["alpha"], "names": ["a.aither"],
125
+ "route": {"type": "web", "target": "https://a.example"},
126
+ "provenance": {"added_by": "t", "sources": ["x"],
127
+ "validated": {"at": "2026-01-01", "by": "t", "method": "http-200"}}})
128
+ idx = netindex.Index([good])
129
+ checks = {
130
+ "resolve hit": idx.resolve("a.aither") is not None,
131
+ "resolve miss": idx.resolve("zz.aither") is None,
132
+ "trust reachable": netindex.TIERS[netindex.trust(good)] == "reachable",
133
+ "search": [r.id for r in idx.search("alpha")] == ["a"],
134
+ }
135
+ for k, v in checks.items():
136
+ print(f"{'PASS' if v else 'FAIL'} {k}")
137
+ return 0 if all(checks.values()) else 1
138
+
139
+
140
+ def main(argv: Optional[list] = None) -> int:
141
+ ap = argparse.ArgumentParser(prog="awindex", description=__doc__.splitlines()[0])
142
+ ap.add_argument("--root", type=Path, help="a records/ directory (default: local or web)")
143
+ ap.add_argument("--web", action="store_true", help="ignore any local checkout")
144
+ ap.add_argument("--self-test", action="store_true")
145
+ ap.add_argument("--version", action="version", version=f"awindex {__version__}")
146
+ sub = ap.add_subparsers(dest="cmd")
147
+ sub.add_parser("resolve").add_argument("name")
148
+ s = sub.add_parser("search")
149
+ s.add_argument("query")
150
+ s.add_argument("--limit", type=int, default=20)
151
+ sub.add_parser("show").add_argument("id")
152
+ sub.add_parser("check")
153
+ a = ap.parse_args(argv)
154
+ if a.self_test:
155
+ return self_test()
156
+ if a.cmd == "check":
157
+ return cmd_check(a.root)
158
+ idx = None if a.web else _local(a.root)
159
+ if a.cmd == "resolve":
160
+ return cmd_resolve(a.name, idx)
161
+ if a.cmd == "search":
162
+ return cmd_search(a.query, idx, a.limit)
163
+ if a.cmd == "show":
164
+ return cmd_show(a.id, idx)
165
+ ap.print_help()
166
+ return 2
167
+
168
+
169
+ if __name__ == "__main__":
170
+ sys.exit(main())
@@ -0,0 +1,228 @@
1
+ """AitherNet Index: a curated, validated internet graph that lives in git.
2
+
3
+ One JSON file per entity under ``records/``. The file
4
+ IS the record; git history is the audit log; a pull request is how a
5
+ contribution arrives (from a person, awfind/awresearch harvest, ShadowBroker
6
+ gates or the Tagalong Town residents). Nothing here crawls or fetches: this
7
+ module loads, validates, resolves names and walks edges. Validation that needs
8
+ the network (is the URL alive) is recorded IN the record by whoever checked it,
9
+ under ``provenance.validated``, so a reviewer can re-run it.
10
+
11
+ Name resolution is the AitherNet DNS/router seam: ``resolve("docs.aither")``
12
+ returns the route a client should take. Two records claiming one name is a
13
+ validation error (NI003), never a silent first-wins.
14
+
15
+ Python 3.10 compatible; stdlib only.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import datetime as _dt
21
+ import json
22
+ import os
23
+ from dataclasses import dataclass, field
24
+ from pathlib import Path
25
+ from typing import Iterable, Optional
26
+
27
+ # A checkout of the index repo (records/ beside the package), or AWINDEX_ROOT.
28
+ # A pip install has no records/: the CLI then reads index.aitherium.com instead.
29
+ DEFAULT_ROOT = Path(os.environ.get("AWINDEX_ROOT")
30
+ or Path(__file__).resolve().parents[1] / "records")
31
+
32
+ # garden = a personal digital garden (Neocities-style); room = an AitherRelay channel;
33
+ # space = an AitherSpace. A .aither name on any of them is its AOL-style keyword.
34
+ KINDS = {"site", "service", "org", "gate", "world", "dataset",
35
+ "garden", "room", "space"}
36
+ ROUTE_TYPES = {"web", "aithernet", "relay-gate", "world", "relay-room", "space"}
37
+ EDGE_RELS = {"owned_by", "links_to", "mirrors", "part_of", "bridges", "about", "alternative_to"}
38
+ NAME_SUFFIX = ".aither"
39
+
40
+
41
+ @dataclass
42
+ class Finding:
43
+ code: str
44
+ record: str
45
+ message: str
46
+
47
+ def __str__(self) -> str:
48
+ return f"{self.code} {self.record}: {self.message}"
49
+
50
+
51
+ @dataclass
52
+ class Record:
53
+ id: str
54
+ kind: str
55
+ title: str
56
+ url: str = ""
57
+ tags: list = field(default_factory=list)
58
+ names: list = field(default_factory=list)
59
+ route: dict = field(default_factory=dict)
60
+ edges: list = field(default_factory=list)
61
+ provenance: dict = field(default_factory=dict)
62
+ path: Optional[Path] = None
63
+
64
+
65
+ def _parse(path: Path, raw: dict) -> Record:
66
+ return Record(
67
+ id=raw.get("id", ""),
68
+ kind=raw.get("kind", ""),
69
+ title=raw.get("title", ""),
70
+ url=raw.get("url", ""),
71
+ tags=list(raw.get("tags") or []),
72
+ names=list(raw.get("names") or []),
73
+ route=dict(raw.get("route") or {}),
74
+ edges=list(raw.get("edges") or []),
75
+ provenance=dict(raw.get("provenance") or {}),
76
+ path=path,
77
+ )
78
+
79
+
80
+ def load(root: Path = DEFAULT_ROOT) -> "tuple[list[Record], list[Finding]]":
81
+ """Load every record. Unreadable JSON is a finding, not an exception."""
82
+ records: list = []
83
+ findings: list = []
84
+ for path in sorted(Path(root).rglob("*.json")):
85
+ try:
86
+ raw = json.loads(path.read_text(encoding="utf-8"))
87
+ except (OSError, ValueError) as exc:
88
+ findings.append(Finding("NI000", path.name, f"unreadable JSON: {exc}"))
89
+ continue
90
+ if not isinstance(raw, dict):
91
+ findings.append(Finding("NI000", path.name, "top level is not an object"))
92
+ continue
93
+ records.append(_parse(path, raw))
94
+ return records, findings
95
+
96
+
97
+ def validate(records: Iterable[Record], today: Optional[_dt.date] = None) -> "list[Finding]":
98
+ """Every rule the index must hold. Empty list = clean."""
99
+ today = today or _dt.date.today()
100
+ recs = list(records)
101
+ out: list = []
102
+ ids = {}
103
+ for r in recs:
104
+ if r.id in ids:
105
+ out.append(Finding("NI002", r.id, f"duplicate id (also {ids[r.id]})"))
106
+ ids[r.id] = r.path.name if r.path else r.id
107
+
108
+ names: dict = {}
109
+ for r in recs:
110
+ rid = r.id or (r.path.name if r.path else "?")
111
+ if not r.id or not r.title:
112
+ out.append(Finding("NI001", rid, "id and title are required"))
113
+ if r.kind not in KINDS:
114
+ out.append(Finding("NI001", rid, f"kind {r.kind!r} not in {sorted(KINDS)}"))
115
+ if r.path is not None and r.path.stem != r.id.replace(":", "_"):
116
+ out.append(Finding("NI002", rid, f"file name {r.path.name} != id"))
117
+ if r.url and not r.url.startswith("https://"):
118
+ out.append(Finding("NI007", rid, "url must be https://"))
119
+ for n in r.names:
120
+ if not isinstance(n, str) or not n.endswith(NAME_SUFFIX) or n != n.lower():
121
+ msg = f"name {n!r} must be lowercase and end in {NAME_SUFFIX}"
122
+ out.append(Finding("NI005", rid, msg))
123
+ if n in names:
124
+ out.append(Finding("NI003", rid, f"name {n!r} already claimed by {names[n]}"))
125
+ names.setdefault(n, rid)
126
+ if r.names:
127
+ rtype = r.route.get("type")
128
+ if rtype not in ROUTE_TYPES or not r.route.get("target"):
129
+ msg = f"a named record needs route.type in {sorted(ROUTE_TYPES)} and route.target"
130
+ out.append(Finding("NI006", rid, msg))
131
+ elif rtype == "web" and not str(r.route["target"]).startswith("https://"):
132
+ out.append(Finding("NI007", rid, "web route target must be https://"))
133
+ for e in r.edges:
134
+ rel, to = (e or {}).get("rel"), (e or {}).get("to")
135
+ if rel not in EDGE_RELS:
136
+ out.append(Finding("NI004", rid, f"edge rel {rel!r} not in {sorted(EDGE_RELS)}"))
137
+ if to not in ids:
138
+ out.append(Finding("NI004", rid, f"edge to unknown record {to!r}"))
139
+ prov = r.provenance
140
+ val = prov.get("validated") or {}
141
+ if not prov.get("added_by") or not prov.get("sources"):
142
+ msg = "provenance.added_by and provenance.sources are required"
143
+ out.append(Finding("NI008", rid, msg))
144
+ if not val.get("at") or not val.get("by") or not val.get("method"):
145
+ out.append(Finding("NI008", rid, "provenance.validated needs at, by, method"))
146
+ else:
147
+ try:
148
+ when = _dt.date.fromisoformat(str(val["at"]))
149
+ except ValueError:
150
+ out.append(Finding("NI008", rid, f"validated.at {val['at']!r} is not YYYY-MM-DD"))
151
+ else:
152
+ if when > today:
153
+ out.append(Finding("NI008", rid, f"validated.at {when} is in the future"))
154
+ seen = set()
155
+ for v in prov.get("vouches") or []:
156
+ who = (v or {}).get("by")
157
+ if not who or not (v or {}).get("at"):
158
+ out.append(Finding("NI009", rid, "a vouch needs by and at"))
159
+ elif who == prov.get("added_by"):
160
+ out.append(Finding("NI009", rid, f"{who} cannot vouch for their own submission"))
161
+ elif who in seen:
162
+ out.append(Finding("NI009", rid, f"{who} vouched twice"))
163
+ seen.add(who)
164
+ return out
165
+
166
+
167
+ # Trust is COMPUTED from evidence in the record, never typed in by a contributor.
168
+ # A submitter can claim anything; only these facts move an entry up a tier.
169
+ TIERS = {0: "submitted", 1: "reachable", 2: "owner-proven", 3: "vouched"}
170
+ _REACH_METHODS = {"http-200"}
171
+ _OWNER_METHODS = {"well-known", "owner-asserted"}
172
+
173
+
174
+ def trust(r: Record) -> int:
175
+ """0 submitted · 1 reachable (bot fetched it) · 2 owner-proven (the site serves
176
+ /.well-known/aithernet.json naming this id) · 3 vouched (reachable or better AND
177
+ at least two distinct people other than the submitter vouched)."""
178
+ method = (r.provenance.get("validated") or {}).get("method", "")
179
+ tier = 2 if method in _OWNER_METHODS else 1 if method in _REACH_METHODS else 0
180
+ vouchers = {v.get("by") for v in r.provenance.get("vouches") or [] if v.get("by")}
181
+ vouchers.discard(r.provenance.get("added_by"))
182
+ if tier >= 1 and len(vouchers) >= 2:
183
+ tier = 3
184
+ return tier
185
+
186
+
187
+ class Index:
188
+ """In-memory view: name -> route, id -> record, edge walks."""
189
+
190
+ def __init__(self, records: Iterable[Record]):
191
+ self.records = {r.id: r for r in records}
192
+ self.names = {n: r.id for r in self.records.values() for n in r.names}
193
+
194
+ @classmethod
195
+ def from_root(cls, root: Path = DEFAULT_ROOT) -> "Index":
196
+ recs, _ = load(root)
197
+ return cls(recs)
198
+
199
+ def resolve(self, name: str) -> Optional[dict]:
200
+ """The AitherNet DNS seam. None = NXDOMAIN; never a guess."""
201
+ rid = self.names.get(name.strip().lower().rstrip("."))
202
+ if rid is None:
203
+ return None
204
+ r = self.records[rid]
205
+ return {"name": name, "id": r.id, "kind": r.kind, **r.route}
206
+
207
+ def neighbours(self, rid: str) -> "list[tuple[str, str, str]]":
208
+ """(direction, rel, other_id) for every edge touching ``rid``."""
209
+ out = []
210
+ for r in self.records.values():
211
+ for e in r.edges:
212
+ if r.id == rid:
213
+ out.append(("out", e.get("rel", ""), e.get("to", "")))
214
+ elif e.get("to") == rid:
215
+ out.append(("in", e.get("rel", ""), r.id))
216
+ return out
217
+
218
+ def search(self, query: str, limit: int = 10) -> "list[Record]":
219
+ """Plain token match over id/title/tags; the AitherSearch provider seam."""
220
+ toks = [t for t in query.lower().split() if t]
221
+ scored = []
222
+ for r in self.records.values():
223
+ hay = " ".join([r.id, r.title, *r.tags, *r.names]).lower()
224
+ s = sum(1 for t in toks if t in hay)
225
+ if s:
226
+ scored.append((s, r.id, r))
227
+ scored.sort(key=lambda x: (-x[0], x[1]))
228
+ return [r for _, _, r in scored[:limit]]
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: awindex
3
+ Version: 0.1.0
4
+ Summary: Resolve .aither keywords and read the AitherNet Index (index.aitherium.com)
5
+ Author: Aitherium
6
+ License: MIT
7
+ Project-URL: Homepage, https://index.aitherium.com
8
+ Project-URL: Source, https://github.com/Aitherium/aithernet-index
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Dynamic: license-file
13
+
14
+ # awindex
15
+
16
+ The AitherNet Index from the shell: a hand-curated map of the internet, kept in git.
17
+ Every entry says who checked it and how.
18
+
19
+ ```bash
20
+ pip install awindex
21
+ awindex resolve neocities # route + trust tier; exit 1 = not indexed
22
+ awindex search "digital garden"
23
+ awindex show neocities.org
24
+ awindex --root records check # validate a checkout of the index repo
25
+ ```
26
+
27
+ It reads a local checkout of
28
+ [Aitherium/aithernet-index](https://github.com/Aitherium/aithernet-index) when one is
29
+ present, and https://index.aitherium.com otherwise. Stdlib only, Python 3.10+.
30
+
31
+ Trust is computed from evidence, never typed in: **submitted**, **reachable** (the bot
32
+ fetched it), **owner-proven** (the site serves `/.well-known/aithernet.json`), and
33
+ **vouched** (two people other than the submitter).
34
+
35
+ MIT. The index data is CC BY 4.0.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ awindex/__init__.py
5
+ awindex/cli.py
6
+ awindex/netindex.py
7
+ awindex.egg-info/PKG-INFO
8
+ awindex.egg-info/SOURCES.txt
9
+ awindex.egg-info/dependency_links.txt
10
+ awindex.egg-info/entry_points.txt
11
+ awindex.egg-info/top_level.txt
12
+ tests/test_awindex.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ awindex = awindex.cli:main
@@ -0,0 +1 @@
1
+ awindex
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "awindex"
7
+ version = "0.1.0"
8
+ description = "Resolve .aither keywords and read the AitherNet Index (index.aitherium.com)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "Aitherium"}]
13
+
14
+ [project.urls]
15
+ Homepage = "https://index.aitherium.com"
16
+ Source = "https://github.com/Aitherium/aithernet-index"
17
+
18
+ [project.scripts]
19
+ awindex = "awindex.cli:main"
20
+
21
+ [tool.setuptools]
22
+ packages = ["awindex"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,42 @@
1
+ """awindex contract: offline resolve, search, trust and check over a temp checkout."""
2
+ import json
3
+
4
+ from awindex import cli, netindex
5
+
6
+
7
+ def _write(root, rec):
8
+ (root / f"{rec['id']}.json").write_text(json.dumps(rec), encoding="utf-8")
9
+
10
+
11
+ REC = {"id": "a.example", "kind": "site", "title": "A", "url": "https://a.example",
12
+ "tags": ["alpha"], "names": ["a.aither"],
13
+ "route": {"type": "web", "target": "https://a.example"},
14
+ "provenance": {"added_by": "t", "sources": ["x"],
15
+ "validated": {"at": "2026-01-01", "by": "t", "method": "http-200"}}}
16
+
17
+
18
+ def test_self_test_passes():
19
+ assert cli.main(["--self-test"]) == 0
20
+
21
+
22
+ def test_resolve_and_search_offline(tmp_path, capsys):
23
+ _write(tmp_path, REC)
24
+ assert cli.main(["--root", str(tmp_path), "resolve", "a"]) == 0
25
+ assert '"target": "https://a.example"' in capsys.readouterr().out
26
+ assert cli.main(["--root", str(tmp_path), "resolve", "zz"]) == 1
27
+ assert cli.main(["--root", str(tmp_path), "search", "alpha"]) == 0
28
+
29
+
30
+ def test_check_flags_a_name_collision(tmp_path):
31
+ _write(tmp_path, REC)
32
+ _write(tmp_path, {**REC, "id": "b.example"})
33
+ assert cli.main(["--root", str(tmp_path), "check"]) == 1
34
+
35
+
36
+ def test_check_on_nothing_cannot_judge(tmp_path):
37
+ assert cli.main(["--root", str(tmp_path), "check"]) == 2
38
+
39
+
40
+ def test_trust_is_computed_not_typed():
41
+ r = netindex._parse(None, {**REC, "trust": "vouched"})
42
+ assert netindex.TIERS[netindex.trust(r)] == "reachable"