sourcecode 1.36.1__py3-none-any.whl → 1.36.2__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.
sourcecode/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.36.1"
3
+ __version__ = "1.36.2"
sourcecode/cli.py CHANGED
@@ -6212,4 +6212,13 @@ def main_entry() -> None:
6212
6212
  except Exception:
6213
6213
  pass
6214
6214
  _preprocess_argv()
6215
- app()
6215
+ try:
6216
+ app()
6217
+ finally:
6218
+ # Best-effort "new version available" nudge. Only speaks on an
6219
+ # interactive terminal; never blocks, raises, or affects exit status.
6220
+ try:
6221
+ from sourcecode.version_check import maybe_notify_update
6222
+ maybe_notify_update(__version__)
6223
+ except Exception:
6224
+ pass
@@ -0,0 +1,149 @@
1
+ """Best-effort "new version available" nudge.
2
+
3
+ Prints a single stderr line when a newer release exists on PyPI. Designed to be
4
+ invisible unless it has something useful to say:
5
+
6
+ * Only runs in an interactive terminal (stderr.isatty()) — never pollutes
7
+ piped output, MCP stdio, CI logs, or test runners.
8
+ * Hits the network at most once per 24h (cached in
9
+ ~/.sourcecode/version_check.json); warm runs read the cache and are instant.
10
+ * Re-shows the same nudge at most ~once per 20h so it informs without nagging.
11
+ * Swallows every error and never blocks meaningfully (1.5s network timeout).
12
+
13
+ Disable entirely with SOURCECODE_NO_UPDATE_CHECK=1 (also off under SOURCECODE_CI).
14
+ The check reads PyPI only; it never touches the license in ~/.sourcecode/license.json.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import sys
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path
23
+ from typing import Optional
24
+
25
+ _CACHE_DIR = Path.home() / ".sourcecode"
26
+ _CACHE_FILE = _CACHE_DIR / "version_check.json"
27
+ _PYPI_URL = "https://pypi.org/pypi/sourcecode/json"
28
+ _CHECK_TTL_SECONDS = 86_400 # refresh the PyPI lookup at most once per 24h
29
+ _NOTIFY_TTL_SECONDS = 72_000 # re-show the nudge at most every ~20h
30
+ _FETCH_TIMEOUT = 1.5
31
+
32
+
33
+ def _disabled() -> bool:
34
+ """True when the nudge must stay silent (opt-out, CI, or non-interactive)."""
35
+ if os.environ.get("SOURCECODE_NO_UPDATE_CHECK"):
36
+ return True
37
+ if os.environ.get("SOURCECODE_CI"):
38
+ return True
39
+ try:
40
+ return not sys.stderr.isatty()
41
+ except Exception:
42
+ return True # no usable stderr -> stay silent
43
+
44
+
45
+ def _read_cache() -> dict:
46
+ try:
47
+ if _CACHE_FILE.exists():
48
+ return json.loads(_CACHE_FILE.read_text(encoding="utf-8"))
49
+ except Exception:
50
+ pass
51
+ return {}
52
+
53
+
54
+ def _write_cache(data: dict) -> None:
55
+ try:
56
+ _CACHE_DIR.mkdir(parents=True, exist_ok=True)
57
+ tmp = _CACHE_FILE.with_suffix(".tmp")
58
+ tmp.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
59
+ tmp.replace(_CACHE_FILE)
60
+ except Exception:
61
+ pass
62
+
63
+
64
+ def _age_seconds(iso: Optional[str]) -> float:
65
+ if not iso:
66
+ return float("inf")
67
+ try:
68
+ ts = datetime.fromisoformat(iso)
69
+ if ts.tzinfo is None:
70
+ ts = ts.replace(tzinfo=timezone.utc)
71
+ return (datetime.now(timezone.utc) - ts).total_seconds()
72
+ except Exception:
73
+ return float("inf")
74
+
75
+
76
+ def _fetch_latest() -> Optional[str]:
77
+ import urllib.request
78
+ try:
79
+ req = urllib.request.Request(_PYPI_URL, headers={"Accept": "application/json"})
80
+ with urllib.request.urlopen(req, timeout=_FETCH_TIMEOUT) as resp:
81
+ data = json.loads(resp.read().decode("utf-8"))
82
+ return ((data.get("info") or {}).get("version")) or None
83
+ except Exception:
84
+ return None
85
+
86
+
87
+ def _parse(v: str) -> tuple:
88
+ """Lenient dotted-numeric parse for the fallback path (no packaging dep)."""
89
+ parts = []
90
+ for chunk in str(v).split("."):
91
+ num = ""
92
+ for ch in chunk:
93
+ if ch.isdigit():
94
+ num += ch
95
+ else:
96
+ break
97
+ parts.append(int(num) if num else 0)
98
+ return tuple(parts)
99
+
100
+
101
+ def _is_newer(latest: str, current: str) -> bool:
102
+ try:
103
+ from packaging.version import parse as _vparse # type: ignore
104
+ return _vparse(latest) > _vparse(current)
105
+ except Exception:
106
+ return _parse(latest) > _parse(current)
107
+
108
+
109
+ def maybe_notify_update(current_version: str) -> None:
110
+ """Print an upgrade nudge to stderr if PyPI has a newer release.
111
+
112
+ Best-effort and fully guarded: any failure is silently ignored. Safe to call
113
+ unconditionally from the CLI entry point.
114
+ """
115
+ if _disabled():
116
+ return
117
+ try:
118
+ cache = _read_cache()
119
+
120
+ # Refresh the cached "latest" at most once per TTL (the only network hit).
121
+ if _age_seconds(cache.get("checked_at")) >= _CHECK_TTL_SECONDS:
122
+ latest = _fetch_latest()
123
+ if latest:
124
+ cache["latest"] = latest
125
+ cache["checked_at"] = datetime.now(timezone.utc).isoformat()
126
+ _write_cache(cache)
127
+
128
+ latest = cache.get("latest")
129
+ if not latest or not _is_newer(latest, current_version):
130
+ return
131
+
132
+ # Throttle: don't nag for the same version more than once per ~20h.
133
+ if (
134
+ cache.get("notified_for") == latest
135
+ and _age_seconds(cache.get("notified_at")) < _NOTIFY_TTL_SECONDS
136
+ ):
137
+ return
138
+
139
+ sys.stderr.write(
140
+ f"\n[sourcecode] v{latest} is available (you have {current_version}). "
141
+ "Upgrade: pipx upgrade sourcecode (pip: pip install -U sourcecode)\n"
142
+ )
143
+ sys.stderr.flush()
144
+
145
+ cache["notified_for"] = latest
146
+ cache["notified_at"] = datetime.now(timezone.utc).isoformat()
147
+ _write_cache(cache)
148
+ except Exception:
149
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.36.1
3
+ Version: 1.36.2
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=mpBzwyDeOFDWE6zNN6TkjBgPX2taBQzVcufgFBu_TN8,103
1
+ sourcecode/__init__.py,sha256=OwsI0itjMvrYqTYz2jWyNvxs5j-e5JKVwJ4h7_kCDEQ,103
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/architecture_analyzer.py,sha256=liCwQmLgb5vplohy8arjYxs_HOIv5C9MjLh_gY6bc5Q,44115
4
4
  sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
@@ -7,7 +7,7 @@ sourcecode/cache.py,sha256=1V3vsaODAa2UBJAC0xpvxpmRdriCezQx5Q8JCcfgziE,31892
7
7
  sourcecode/canonical_ir.py,sha256=c_lYTVoegg-1W2dZ34_2s3tN8L0GVx7eiDRh9ghdSD8,24178
8
8
  sourcecode/cir_graphs.py,sha256=rZi8JV4ZrAa2WSCeyNa4JIEKQ_yZzDZTsrvVz2KfuKA,8919
9
9
  sourcecode/classifier.py,sha256=hKzg-nQ47htqqIUzSGvYxv15cXrA3KgICTwJmdqal0o,8095
10
- sourcecode/cli.py,sha256=76_rdkUKpm9fGP-97rx5Q25q2KXzW030fDpDZiwCnIA,252320
10
+ sourcecode/cli.py,sha256=oNcQu-l2maZ9Pb2fZiY4JTCojFi0C156TLGTrJBKT34,252665
11
11
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
12
12
  sourcecode/confidence_analyzer.py,sha256=_jckZSxksV-OU38vbkxfVNBnWCtlCq8Vwfg23x1uspA,19054
13
13
  sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
@@ -58,6 +58,7 @@ sourcecode/spring_semantic.py,sha256=O1nKSGVzlukuxLHQVuCPxc-XrcrMFxwlHA20_dmEGgM
58
58
  sourcecode/spring_tx_analyzer.py,sha256=FdFcyqPp3aT9oJ-PKrnXcTA6s69wdvzG-NBm0GMGPTU,30717
59
59
  sourcecode/summarizer.py,sha256=zgdps7yS2IktAbWe7IWz0oUcr3QIuNPRGrsScbZ4R1g,21797
60
60
  sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
61
+ sourcecode/version_check.py,sha256=CHp6ZxTIfo8kyHPCBgJA1uFC0xQCoXMuuOfrW8QTL8o,4942
61
62
  sourcecode/workspace.py,sha256=X_6NmNnitvT3_38V-JDChydo_sR68s249hLFlrQskU0,8271
62
63
  sourcecode/detectors/__init__.py,sha256=A0AACJFF6HWf_RgatNtWu3PUzstcKtIGM9f1PoFcJug,1987
63
64
  sourcecode/detectors/base.py,sha256=C2EqfZudQ1ITK4DID4M70nPxqoh9bl1zn_ta6XRaGWs,1168
@@ -96,8 +97,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
96
97
  sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
97
98
  sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
98
99
  sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
99
- sourcecode-1.36.1.dist-info/METADATA,sha256=3B_cemxNmCw3HpB0xSWmblfvftNCGh45Wi5mQvvrdeg,30313
100
- sourcecode-1.36.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
101
- sourcecode-1.36.1.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
102
- sourcecode-1.36.1.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
103
- sourcecode-1.36.1.dist-info/RECORD,,
100
+ sourcecode-1.36.2.dist-info/METADATA,sha256=JWwQcBBUaBi8XxPm9ztTXwbj-lqk1CebR-LnQPQ731A,30313
101
+ sourcecode-1.36.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
102
+ sourcecode-1.36.2.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
103
+ sourcecode-1.36.2.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
104
+ sourcecode-1.36.2.dist-info/RECORD,,