sourcecode 1.30.1__py3-none-any.whl → 1.30.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.30.1"
3
+ __version__ = "1.30.2"
sourcecode/cli.py CHANGED
@@ -1866,6 +1866,8 @@ def prepare_context_cmd(
1866
1866
  out["review_hotspots"] = output.review_hotspots
1867
1867
  if output.suggested_review_order:
1868
1868
  out["suggested_review_order"] = output.suggested_review_order
1869
+ if output.execution_paths:
1870
+ out["execution_paths"] = output.execution_paths
1869
1871
  if output.impact_summary:
1870
1872
  out["impact_summary"] = output.impact_summary
1871
1873
  if output.why_these_files:
@@ -0,0 +1,310 @@
1
+ """flow_analyzer.py — Evidence-based execution path extraction for PR context.
2
+
3
+ Builds Entry → Service → Repository → EndState ordered sequences using ONLY
4
+ direct code evidence: field injection, constructor params, type annotations,
5
+ method calls, explicit instantiation.
6
+
7
+ V3: execution_paths with runtime_notes — conditional branches, optional execution,
8
+ and async side-effects are surfaced when explicit code signals exist.
9
+ No inference, no naming, no invented behavior.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from pathlib import Path
15
+ from typing import Callable, Optional
16
+
17
+ _ENTRY_ARTIFACT_TYPES = frozenset({"controller", "entrypoint"})
18
+ _SERVICE_ARTIFACT_TYPES = frozenset({"service"})
19
+ _REPO_ARTIFACT_TYPES = frozenset({"repository", "mapper"})
20
+
21
+ _DB_KEYWORDS = frozenset({"repository", "dao", "mapper", "store", "jpa", "jdbc", "sql"})
22
+ _EVENT_KEYWORDS = frozenset({"event", "publish", "emit", "kafka", "queue", "rabbit", "sns", "bus"})
23
+
24
+ _HTTP_ENTRY_RE = re.compile(
25
+ r'@(?:Get|Post|Put|Delete|Patch|Request)Mapping[^)]*\)'
26
+ r'|@(?:Get|Post|Put|Delete|Patch)\([^)]*\)'
27
+ r'|@\w+\.(?:get|post|put|delete|patch)\([^)]*\)',
28
+ re.IGNORECASE,
29
+ )
30
+ _METHOD_NAME_RE = re.compile(
31
+ r'(?:public\s+|async\s+|def\s+|function\s+)*'
32
+ r'(?:[\w<>\[\]]+\s+)?'
33
+ r'(\w+)\s*\(',
34
+ )
35
+
36
+ # Runtime signal patterns: (compiled_regex, note_text)
37
+ # Only signals with explicit code evidence — no inference.
38
+ # Three categories: condition | branch | async
39
+ _RUNTIME_SIGNALS: list[tuple[re.Pattern, str]] = [
40
+ # ── Conditional / auth guards ─────────────────────────────────────────────
41
+ (re.compile(r'@PreAuthorize|@Secured|@RolesAllowed', re.IGNORECASE),
42
+ "condition: authorization check present (@PreAuthorize / @Secured)"),
43
+ (re.compile(r'isAuthenticated\(\)|hasRole\(|hasAuthority\(|SecurityContextHolder', re.IGNORECASE),
44
+ "condition: reads authentication context"),
45
+ (re.compile(r'featureFlag|FeatureToggle|\.isEnabled\s*\(|\.isActive\s*\(', re.IGNORECASE),
46
+ "condition: feature flag gates execution"),
47
+ # Null/empty guard with early return — matches if (...null/empty...) return/throw on same line
48
+ (re.compile(r'if\s*\([^)]*(?:==\s*null|!=\s*null|isEmpty\s*\(\)|isBlank\s*\(\))[^)]*\)'
49
+ r'\s*(?:\{?\s*)?(?:return|throw)\b', re.IGNORECASE),
50
+ "condition: null/empty guard with early return"),
51
+
52
+ # ── Optional execution / branching ────────────────────────────────────────
53
+ (re.compile(r'@Cacheable|@CacheEvict|@CachePut', re.IGNORECASE),
54
+ "branch: Spring cache may short-circuit downstream call"),
55
+ (re.compile(r'\.getIfPresent\s*\(|cache\.get\s*\(|cacheManager\.', re.IGNORECASE),
56
+ "branch: manual cache lookup may short-circuit"),
57
+ (re.compile(r'Optional\s*<|\.orElseThrow\s*\(|\.orElseGet\s*\(|\.orElse\s*\(', re.IGNORECASE),
58
+ "branch: result may be absent (Optional)"),
59
+
60
+ # ── Async / side effects ──────────────────────────────────────────────────
61
+ (re.compile(r'@Async\b'),
62
+ "async: runs in separate thread (@Async)"),
63
+ (re.compile(r'CompletableFuture|\.supplyAsync\s*\(|\.runAsync\s*\('),
64
+ "async: non-blocking future-based execution"),
65
+ (re.compile(r'\basync\s+def\b|\bawait\b', re.IGNORECASE),
66
+ "async: non-blocking (async/await)"),
67
+ (re.compile(r'publishEvent\s*\(|applicationEventPublisher|eventPublisher\.', re.IGNORECASE),
68
+ "async: Spring application event emitted"),
69
+ (re.compile(r'kafkaTemplate\.|KafkaProducer|@KafkaListener', re.IGNORECASE),
70
+ "async: Kafka message produced"),
71
+ (re.compile(r'rabbitTemplate\.|amqpTemplate\.|@RabbitListener', re.IGNORECASE),
72
+ "async: RabbitMQ message sent"),
73
+ ]
74
+
75
+
76
+ def _detect_lang(path: str) -> str:
77
+ return {
78
+ ".java": "java", ".kt": "kotlin",
79
+ ".py": "python",
80
+ ".ts": "typescript", ".tsx": "typescript",
81
+ ".js": "javascript", ".jsx": "javascript",
82
+ ".go": "go", ".cs": "csharp", ".rb": "ruby", ".php": "php",
83
+ }.get(Path(path).suffix.lower(), "unknown")
84
+
85
+
86
+ def _strip_comments(content: str, lang: str) -> str:
87
+ content = re.sub(r"/\*.*?\*/", " ", content, flags=re.DOTALL)
88
+ content = re.sub(r"//[^\n]*", " ", content)
89
+ if lang in ("python", "ruby", "go"):
90
+ content = re.sub(r"#[^\n]*", " ", content)
91
+ return content
92
+
93
+
94
+ def _read_safe(root: Path, rel_path: str) -> str:
95
+ try:
96
+ return (root / rel_path).read_text(encoding="utf-8", errors="ignore")
97
+ except (OSError, ValueError):
98
+ return ""
99
+
100
+
101
+ def _collect_runtime_notes(content: str, lang: str) -> list[str]:
102
+ """Scan comment-stripped content for explicit runtime behavior signals.
103
+
104
+ Returns only notes backed by a direct code pattern match.
105
+ Returns [] when no signals are found.
106
+ """
107
+ clean = _strip_comments(content, lang)
108
+ notes: list[str] = []
109
+ seen: set[str] = set()
110
+ for pattern, note in _RUNTIME_SIGNALS:
111
+ if note not in seen and pattern.search(clean):
112
+ notes.append(note)
113
+ seen.add(note)
114
+ return notes
115
+
116
+
117
+ def _find_entry_method(clean: str) -> Optional[str]:
118
+ m = _HTTP_ENTRY_RE.search(clean)
119
+ if not m:
120
+ return None
121
+ after = clean[m.end():]
122
+ mn = _METHOD_NAME_RE.match(after.lstrip())
123
+ if mn:
124
+ name = mn.group(1)
125
+ if name.lower() not in ("public", "async", "def", "function", "void", "override"):
126
+ return name
127
+ return None
128
+
129
+
130
+ def _build_field_map(clean: str) -> dict[str, str]:
131
+ """Map field_name_lower → ClassName from injection patterns."""
132
+ fmap: dict[str, str] = {}
133
+ for m in re.finditer(r"private\s+(\w+)(?:<[^>]+>)?\s+(\w+)\s*[;=,)]", clean):
134
+ fmap[m.group(2).lower()] = m.group(1)
135
+ for m in re.finditer(r"(?:private|protected|readonly)\s+(\w+)\s*:\s*(\w+)", clean):
136
+ fmap[m.group(1).lower()] = m.group(2)
137
+ for m in re.finditer(r"self\.(\w+)\s*=\s*(\w+)\s*\(", clean):
138
+ fmap[m.group(1).lower()] = m.group(2)
139
+ return fmap
140
+
141
+
142
+ def _find_called_method(clean: str, class_name: str, fmap: dict[str, str]) -> Optional[str]:
143
+ fields = [f for f, t in fmap.items() if t.lower() == class_name.lower()]
144
+ for field in fields:
145
+ pat = rf"\bthis\.{re.escape(field)}\.(\w+)\s*\(|\b{re.escape(field)}\.(\w+)\s*\("
146
+ for m in re.finditer(pat, clean, re.IGNORECASE):
147
+ name = m.group(1) or m.group(2)
148
+ if name and name.lower() not in ("class", "new", "super", "get", "set"):
149
+ return name
150
+ for m in re.finditer(rf"\b{re.escape(class_name)}\.(\w+)\s*\(", clean, re.IGNORECASE):
151
+ name = m.group(1)
152
+ if name.lower() not in ("class", "new", "super"):
153
+ return name
154
+ return None
155
+
156
+
157
+ def _has_code_evidence(clean: str, class_name: str) -> bool:
158
+ """True only when class_name has direct code evidence in pre-stripped content."""
159
+ esc = re.escape(class_name)
160
+ if re.search(rf"\b(?:private|protected)\s+{esc}\b", clean, re.IGNORECASE):
161
+ return True
162
+ if re.search(rf"[,(]\s*{esc}\s+\w+", clean, re.IGNORECASE):
163
+ return True
164
+ if re.search(rf":\s*{esc}\b", clean, re.IGNORECASE):
165
+ return True
166
+ if re.search(rf"\bnew\s+{esc}\s*\(", clean, re.IGNORECASE):
167
+ return True
168
+ if re.search(rf"\b{esc}\s*\(", clean):
169
+ return True
170
+ if re.search(rf"\b{esc}\b", clean, re.IGNORECASE):
171
+ non_import = re.search(
172
+ rf"^(?!\s*(?:import|require|from|//|#|\*)\b).*\b{esc}\b",
173
+ clean, re.IGNORECASE | re.MULTILINE,
174
+ )
175
+ if non_import:
176
+ return True
177
+ return False
178
+
179
+
180
+ def _find_evidenced_ordered(
181
+ root: Path,
182
+ source_path: str,
183
+ candidates: list[str],
184
+ ) -> list[tuple[str, Optional[str]]]:
185
+ """Return (class_name, method_or_None) for candidates with direct code evidence,
186
+ ordered by their first appearance position in the source file."""
187
+ content = _read_safe(root, source_path)
188
+ if not content:
189
+ return []
190
+ lang = _detect_lang(source_path)
191
+ clean = _strip_comments(content, lang)
192
+ fmap = _build_field_map(clean)
193
+
194
+ positioned: list[tuple[int, str, Optional[str]]] = []
195
+ for cand_path in candidates:
196
+ class_name = Path(cand_path).stem
197
+ if not _has_code_evidence(clean, class_name):
198
+ continue
199
+ method = _find_called_method(clean, class_name, fmap)
200
+ m = re.search(rf"\b{re.escape(class_name)}\b", clean, re.IGNORECASE)
201
+ pos = m.start() if m else len(clean)
202
+ positioned.append((pos, class_name, method))
203
+
204
+ positioned.sort(key=lambda x: x[0])
205
+ return [(cls, meth) for _, cls, meth in positioned]
206
+
207
+
208
+ def _detect_end_state(path: list[str]) -> str:
209
+ for step in path:
210
+ s = step.lower()
211
+ if any(kw in s for kw in _DB_KEYWORDS):
212
+ return "DB write"
213
+ if any(kw in s for kw in _EVENT_KEYWORDS):
214
+ return "event emitted"
215
+ return "HTTP response"
216
+
217
+
218
+ def _step_label(class_name: str, method: Optional[str]) -> str:
219
+ return f"{class_name}.{method}" if method else class_name
220
+
221
+
222
+ def _path_name(entry_class: str) -> str:
223
+ domain = re.sub(
224
+ r"(?:RestController|Controller|Resource|Handler|Api|Endpoint|Router|Servlet)$",
225
+ "", entry_class, flags=re.IGNORECASE,
226
+ )
227
+ return re.sub(r"(?<=[a-z])(?=[A-Z])", " ", domain).strip()
228
+
229
+
230
+ def analyze_execution_paths(
231
+ changed_files: list[str],
232
+ all_paths: list[str],
233
+ root: Path,
234
+ classify_fn: Callable[[str], dict],
235
+ max_paths: int = 3,
236
+ ) -> list[dict]:
237
+ """Build ordered execution paths with runtime behavior signals.
238
+
239
+ Each path:
240
+ - One service per entry point (most evident, earliest-referenced)
241
+ - Each step requires direct code evidence
242
+ - runtime_notes populated from explicit code signals only (never inferred)
243
+ - Forward-only: Controller → Service → Repository
244
+
245
+ Returns list of: {name, entry_point, path, runtime_notes, end_state}
246
+ Returns [] when no verifiable path exists.
247
+ """
248
+ entry_files = [
249
+ f for f in changed_files
250
+ if classify_fn(f)["artifact_type"] in _ENTRY_ARTIFACT_TYPES
251
+ ]
252
+ if not entry_files:
253
+ return []
254
+
255
+ all_services = [p for p in all_paths if classify_fn(p)["artifact_type"] in _SERVICE_ARTIFACT_TYPES]
256
+ all_repos = [p for p in all_paths if classify_fn(p)["artifact_type"] in _REPO_ARTIFACT_TYPES]
257
+
258
+ result: list[dict] = []
259
+
260
+ for entry_path in entry_files[:max_paths]:
261
+ entry_class = Path(entry_path).stem
262
+ lang = _detect_lang(entry_path)
263
+
264
+ entry_content = _read_safe(root, entry_path)
265
+ entry_clean = _strip_comments(entry_content, lang) if entry_content else ""
266
+ entry_method = _find_entry_method(entry_clean) if entry_clean else None
267
+ entry_point_str = _step_label(entry_class, entry_method)
268
+
269
+ evidenced_svcs = _find_evidenced_ordered(root, entry_path, all_services)
270
+ if not evidenced_svcs:
271
+ continue
272
+
273
+ svc_class, svc_method = evidenced_svcs[0]
274
+ svc_label = _step_label(svc_class, svc_method)
275
+
276
+ svc_path = next((p for p in all_services if Path(p).stem == svc_class), None)
277
+ svc_content = _read_safe(root, svc_path) if svc_path else ""
278
+ svc_lang = _detect_lang(svc_path) if svc_path else "unknown"
279
+
280
+ # Service step — notes scoped to service file only
281
+ path_items: list[dict] = [
282
+ {"step": svc_label,
283
+ "notes": _collect_runtime_notes(svc_content, svc_lang) if svc_content else []},
284
+ ]
285
+
286
+ # Repository step — notes scoped to repo file only
287
+ if svc_path:
288
+ evidenced_repos = _find_evidenced_ordered(root, svc_path, all_repos)
289
+ if evidenced_repos:
290
+ repo_class, repo_method = evidenced_repos[0]
291
+ repo_label = _step_label(repo_class, repo_method)
292
+ repo_path = next((p for p in all_repos if Path(p).stem == repo_class), None)
293
+ repo_content = _read_safe(root, repo_path) if repo_path else ""
294
+ repo_lang = _detect_lang(repo_path) if repo_path else "unknown"
295
+ path_items.append(
296
+ {"step": repo_label,
297
+ "notes": _collect_runtime_notes(repo_content, repo_lang) if repo_content else []},
298
+ )
299
+
300
+ # Entry-point notes scoped to controller file
301
+ entry_notes = _collect_runtime_notes(entry_content, lang) if entry_content else []
302
+
303
+ result.append({
304
+ "name": _path_name(entry_class),
305
+ "entry_point": {"step": entry_point_str, "notes": entry_notes},
306
+ "path": path_items,
307
+ "end_state": _detect_end_state([item["step"] for item in path_items]),
308
+ })
309
+
310
+ return result
@@ -351,6 +351,7 @@ class TaskOutput:
351
351
  test_coverage_risk: dict = field(default_factory=dict)
352
352
  review_hotspots: list[str] = field(default_factory=list)
353
353
  suggested_review_order: list[str] = field(default_factory=list)
354
+ execution_paths: list[dict] = field(default_factory=list)
354
355
 
355
356
 
356
357
  # ─────────────────────────────────────────────────────────────────────────────
@@ -874,6 +875,17 @@ class TaskContextBuilder:
874
875
  _pr_suggested_review_order.append(_f)
875
876
  _seen_order.add(_f)
876
877
 
878
+ # ── 6d. review-pr: execution paths ──────────────────────────────────
879
+ _execution_paths: list[dict] = []
880
+ if task_name == "review-pr" and _delta_files:
881
+ from sourcecode.flow_analyzer import analyze_execution_paths
882
+ _execution_paths = analyze_execution_paths(
883
+ changed_files=sorted(_delta_files),
884
+ all_paths=all_paths,
885
+ root=self.root,
886
+ classify_fn=self._classify_changed_file,
887
+ )
888
+
877
889
  # ── 6c. Symptom keyword boost + related notes (fix-bug + --symptom) ──
878
890
  symptom_keywords: list[str] = []
879
891
  related_notes: list[dict] = []
@@ -1104,6 +1116,7 @@ class TaskContextBuilder:
1104
1116
  test_coverage_risk=_pr_test_coverage_risk,
1105
1117
  review_hotspots=_pr_review_hotspots,
1106
1118
  suggested_review_order=_pr_suggested_review_order,
1119
+ execution_paths=_execution_paths,
1107
1120
  )
1108
1121
 
1109
1122
  def render_prompt(self, output: TaskOutput) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.30.1
3
+ Version: 1.30.2
4
4
  Summary: Deterministic codebase context for AI coding agents
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -221,7 +221,7 @@ Description-Content-Type: text/markdown
221
221
 
222
222
  **Compressed AI-ready context for Java/Spring enterprise codebases.**
223
223
 
224
- ![Version](https://img.shields.io/badge/version-1.30.1-blue)
224
+ ![Version](https://img.shields.io/badge/version-1.30.2-blue)
225
225
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
226
226
 
227
227
  ---
@@ -255,7 +255,7 @@ pipx install sourcecode
255
255
 
256
256
  ```bash
257
257
  sourcecode version
258
- # sourcecode 1.30.1
258
+ # sourcecode 1.30.2
259
259
  ```
260
260
 
261
261
  ---
@@ -1,10 +1,10 @@
1
- sourcecode/__init__.py,sha256=gRPMqEDn0fO_SPt0SwGDW592WxUJP93fNtbrm1ay9ms,103
1
+ sourcecode/__init__.py,sha256=ERxetwuKJX_1UzzbbdymfXL8AXwRFp03HJG6sY-iJO4,103
2
2
  sourcecode/adaptive_scanner.py,sha256=RTNExwWPXzjgLaRueT7UuxkPj5ZEToWjGbx1j0LSZ9E,10250
3
3
  sourcecode/architecture_analyzer.py,sha256=MyBa0Hf5HmkudZQDLKrjcWDKETXETXl0mQX1swtTwAA,39091
4
4
  sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
5
5
  sourcecode/ast_extractor.py,sha256=XgrZg2DcWcUm9r87cRG3KGO7IK2TIL_N-CvhSbUmmh4,49901
6
6
  sourcecode/classifier.py,sha256=pYve2J1LqtYssU3lYLMDz18PT-CjN5c18QYE7R_IG1Q,7507
7
- sourcecode/cli.py,sha256=iWzo7u-wmWjj0GYAF54UpbicfpXt2OUxPRy44h2VaCI,80646
7
+ sourcecode/cli.py,sha256=1qVMsC2swT-OtCK6XziIM0J4xKp8kcRhUzfOaHr7vRU,80743
8
8
  sourcecode/code_notes_analyzer.py,sha256=y1MJBnPZHYp4i6cQCXUb9ATIyifS_qMQWjw_8lPkpsU,9215
9
9
  sourcecode/confidence_analyzer.py,sha256=xw_Jv8pAd0wd8t2vvQlorw8Ih0rSF3YCoFS8K-_4aXg,15762
10
10
  sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
@@ -17,10 +17,11 @@ sourcecode/doc_analyzer.py,sha256=afA4uJFwXZ_uR2l4J0pQwbeTkRkGmKdN9KhRVYePBUw,24
17
17
  sourcecode/entrypoint_classifier.py,sha256=gvKgl0f5T8ol1r4JMmkeqGHuZTfZJiOwFOWdc7EYwYw,4061
18
18
  sourcecode/env_analyzer.py,sha256=GxCidahAAIptTdDFIlVB6URd4HBnBlIX_SqUov3MBRQ,22076
19
19
  sourcecode/file_classifier.py,sha256=48ly5Z6exkzBy8lNy1AkdP4-oJqIA1zT3LZfffuTyDo,11572
20
+ sourcecode/flow_analyzer.py,sha256=VQDrItg3NBqOOD8PxHXyntXQnPweUuUn6JtOY8lNWys,12841
20
21
  sourcecode/git_analyzer.py,sha256=_pCg2V4d2aa17k9hayTzpexAj8syvyk4y9NYNvvgOAI,12802
21
22
  sourcecode/graph_analyzer.py,sha256=iUK-7pSV-cvGqqD2hENdYmhnm0wcXFEyK-xnu5ul8OU,62515
22
23
  sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
23
- sourcecode/prepare_context.py,sha256=ODGcRvwcJEQKDVdOVE3tGVYBqjNu0x6NkqBY4rnDAwQ,128946
24
+ sourcecode/prepare_context.py,sha256=ELrCIIcttip4B3y9aQZdMPqIgzaEJR0evDdG8QYTBLc,129623
24
25
  sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
25
26
  sourcecode/ranking_engine.py,sha256=virVglafZufioHpZpwktjMvUiL0TZELWQCQnQNV8dFo,9360
26
27
  sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
@@ -61,8 +62,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
61
62
  sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
62
63
  sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
63
64
  sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
64
- sourcecode-1.30.1.dist-info/METADATA,sha256=bbWfG67q8PN5m5nGQpeGJu04bA2zKFNrUZndmIalCRs,23417
65
- sourcecode-1.30.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
66
- sourcecode-1.30.1.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
67
- sourcecode-1.30.1.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
68
- sourcecode-1.30.1.dist-info/RECORD,,
65
+ sourcecode-1.30.2.dist-info/METADATA,sha256=3bLQsn6BmYa9Rum0jjejw2627bPdOMaYxbqI2XMyOLY,23417
66
+ sourcecode-1.30.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
67
+ sourcecode-1.30.2.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
68
+ sourcecode-1.30.2.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
69
+ sourcecode-1.30.2.dist-info/RECORD,,