party-continuum 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.
- party_continuum-0.1.0/PKG-INFO +27 -0
- party_continuum-0.1.0/README.md +20 -0
- party_continuum-0.1.0/party_continuum/__init__.py +3 -0
- party_continuum-0.1.0/party_continuum/applicability.py +52 -0
- party_continuum-0.1.0/party_continuum/build_metrics.py +72 -0
- party_continuum-0.1.0/party_continuum/calls.py +86 -0
- party_continuum-0.1.0/party_continuum/config.py +194 -0
- party_continuum-0.1.0/party_continuum/file_graph.py +438 -0
- party_continuum-0.1.0/party_continuum/graph.py +492 -0
- party_continuum-0.1.0/party_continuum/graph_buffer.py +192 -0
- party_continuum-0.1.0/party_continuum/import_resolution.py +88 -0
- party_continuum-0.1.0/party_continuum/local.py +444 -0
- party_continuum-0.1.0/party_continuum/local_cli.py +1110 -0
- party_continuum-0.1.0/party_continuum/occurrences.py +301 -0
- party_continuum-0.1.0/party_continuum/pytest_dependencies.py +173 -0
- party_continuum-0.1.0/party_continuum/python_aliases.py +115 -0
- party_continuum-0.1.0/party_continuum/relations.py +969 -0
- party_continuum-0.1.0/party_continuum/resolution.py +85 -0
- party_continuum-0.1.0/party_continuum/semantic.py +436 -0
- party_continuum-0.1.0/party_continuum/semantic_base.py +248 -0
- party_continuum-0.1.0/party_continuum/symbols.py +945 -0
- party_continuum-0.1.0/party_continuum/symbols_typescript.py +93 -0
- party_continuum-0.1.0/party_continuum/ts_extractor/extract.cjs +462 -0
- party_continuum-0.1.0/party_continuum/ts_extractor/package.json +10 -0
- party_continuum-0.1.0/party_continuum.egg-info/PKG-INFO +27 -0
- party_continuum-0.1.0/party_continuum.egg-info/SOURCES.txt +29 -0
- party_continuum-0.1.0/party_continuum.egg-info/dependency_links.txt +1 -0
- party_continuum-0.1.0/party_continuum.egg-info/entry_points.txt +2 -0
- party_continuum-0.1.0/party_continuum.egg-info/top_level.txt +1 -0
- party_continuum-0.1.0/pyproject.toml +21 -0
- party_continuum-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: party-continuum
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Static change impact of a checkout: what depends on a symbol, how far a change reaches, which tests to run
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# continuum
|
|
9
|
+
|
|
10
|
+
Static change impact of the git checkout in the current directory, for Python
|
|
11
|
+
and TypeScript/JavaScript. Continuum never runs tests; it tells you which to run.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install party-continuum # or: brew install maitai-inc/continuum/continuum
|
|
15
|
+
|
|
16
|
+
continuum dependents src/app/billing.py::charge # before editing: what uses it
|
|
17
|
+
continuum impact # after editing: how far it reaches
|
|
18
|
+
continuum tests --explain # before pushing: which tests, and why
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
It compares the working tree with its merge-base with the default branch
|
|
22
|
+
(`--base <ref>` / `--head <commit>` to choose). Every command takes `--json`.
|
|
23
|
+
Suites and test commands can be pinned in a `continuum.toml` at the checkout root.
|
|
24
|
+
|
|
25
|
+
Requirements: Python 3.11+ and git. TypeScript also needs `node` and the
|
|
26
|
+
repository's `typescript` package installed (`npm ci`), or `CONTINUUM_TYPESCRIPT_DIR`.
|
|
27
|
+
No network access or credentials: everything is computed from the local checkout.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# continuum
|
|
2
|
+
|
|
3
|
+
Static change impact of the git checkout in the current directory, for Python
|
|
4
|
+
and TypeScript/JavaScript. Continuum never runs tests; it tells you which to run.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install party-continuum # or: brew install maitai-inc/continuum/continuum
|
|
8
|
+
|
|
9
|
+
continuum dependents src/app/billing.py::charge # before editing: what uses it
|
|
10
|
+
continuum impact # after editing: how far it reaches
|
|
11
|
+
continuum tests --explain # before pushing: which tests, and why
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
It compares the working tree with its merge-base with the default branch
|
|
15
|
+
(`--base <ref>` / `--head <commit>` to choose). Every command takes `--json`.
|
|
16
|
+
Suites and test commands can be pinned in a `continuum.toml` at the checkout root.
|
|
17
|
+
|
|
18
|
+
Requirements: Python 3.11+ and git. TypeScript also needs `node` and the
|
|
19
|
+
repository's `typescript` package installed (`npm ci`), or `CONTINUUM_TYPESCRIPT_DIR`.
|
|
20
|
+
No network access or credentials: everything is computed from the local checkout.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Evidence used to distinguish executable changes from file membership.
|
|
2
|
+
|
|
3
|
+
Source bytes retain their ordinary digest for storage/exploration. Selection has
|
|
4
|
+
an additional module-initialization fingerprint: editing a deferred function
|
|
5
|
+
body changes that callable, not every consumer of its containing module.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
import copy
|
|
10
|
+
from pathlib import PurePosixPath
|
|
11
|
+
|
|
12
|
+
from .config import digest
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def fingerprint(node):
|
|
16
|
+
return node.get("selection_digest", node.get("digest", node))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Initialization(ast.NodeTransformer):
|
|
20
|
+
def generic_visit(self, node):
|
|
21
|
+
node = copy.copy(node)
|
|
22
|
+
for field, value in ast.iter_fields(node):
|
|
23
|
+
if isinstance(value, list):
|
|
24
|
+
setattr(node, field, list(value))
|
|
25
|
+
return super().generic_visit(node)
|
|
26
|
+
|
|
27
|
+
def visit_FunctionDef(self, node):
|
|
28
|
+
result = copy.copy(node)
|
|
29
|
+
# Defaults, annotations and decorators execute at definition time. The
|
|
30
|
+
# body executes on invocation and has its own independently changed root.
|
|
31
|
+
result.body = [ast.Pass()]
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
visit_AsyncFunctionDef = visit_FunctionDef
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def initialization_digest(tree):
|
|
38
|
+
return digest(ast.dump(Initialization().visit(tree), include_attributes=False))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def source_roots(files, config):
|
|
42
|
+
if "source_roots" in config:
|
|
43
|
+
return config["source_roots"]
|
|
44
|
+
roots = {""}
|
|
45
|
+
for path in files:
|
|
46
|
+
if not path.endswith("/__init__.py"):
|
|
47
|
+
continue
|
|
48
|
+
package = PurePosixPath(path).parent
|
|
49
|
+
if str(package.parent / "__init__.py") not in files:
|
|
50
|
+
parent = str(package.parent)
|
|
51
|
+
roots.add("" if parent == "." else parent)
|
|
52
|
+
return sorted(roots)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Coarse build resource counters; no graph contents or validation authority."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import resource
|
|
8
|
+
except ImportError: # Parallel compilation also supports non-POSIX hosts.
|
|
9
|
+
resource = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
FIELDS = (
|
|
13
|
+
"cpu_seconds",
|
|
14
|
+
"user_cpu_seconds",
|
|
15
|
+
"system_cpu_seconds",
|
|
16
|
+
"input_blocks",
|
|
17
|
+
"output_blocks",
|
|
18
|
+
"major_faults",
|
|
19
|
+
"voluntary_switches",
|
|
20
|
+
"involuntary_switches",
|
|
21
|
+
)
|
|
22
|
+
PHASE_FIELDS = tuple(
|
|
23
|
+
phase + "_" + prefix + key
|
|
24
|
+
for phase in ("compilation", "resolution")
|
|
25
|
+
for prefix in (("", "reaped_child_") if phase == "compilation" else ("",))
|
|
26
|
+
for key in FIELDS
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def usage():
|
|
31
|
+
values = {"cpu_seconds": time.process_time()}
|
|
32
|
+
if resource is not None:
|
|
33
|
+
for prefix, who in (
|
|
34
|
+
("", resource.RUSAGE_SELF),
|
|
35
|
+
("reaped_child_", resource.RUSAGE_CHILDREN),
|
|
36
|
+
):
|
|
37
|
+
row = resource.getrusage(who)
|
|
38
|
+
values.update(
|
|
39
|
+
{
|
|
40
|
+
prefix + "cpu_seconds": row.ru_utime + row.ru_stime,
|
|
41
|
+
prefix + "user_cpu_seconds": row.ru_utime,
|
|
42
|
+
prefix + "system_cpu_seconds": row.ru_stime,
|
|
43
|
+
prefix + "input_blocks": row.ru_inblock,
|
|
44
|
+
prefix + "output_blocks": row.ru_oublock,
|
|
45
|
+
prefix + "major_faults": row.ru_majflt,
|
|
46
|
+
prefix + "voluntary_switches": row.ru_nvcsw,
|
|
47
|
+
prefix + "involuntary_switches": row.ru_nivcsw,
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
return values
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def delta(before, after, phase):
|
|
54
|
+
# Compilation's pool has joined before sampling, so RUSAGE_CHILDREN includes
|
|
55
|
+
# those compilers. Resolution excludes children: the independent clean tree
|
|
56
|
+
# may be running concurrently, and must report its own resource counters.
|
|
57
|
+
return {
|
|
58
|
+
phase + "_" + key: after[key] - before[key]
|
|
59
|
+
for key in before.keys() & after.keys()
|
|
60
|
+
if phase == "compilation" or not key.startswith("reaped_child_")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def clean_metrics(stats):
|
|
65
|
+
values = {}
|
|
66
|
+
for key in PHASE_FIELDS:
|
|
67
|
+
if key in stats:
|
|
68
|
+
value = stats[key]
|
|
69
|
+
if type(value) not in (int, float) or not math.isfinite(value) or value < 0:
|
|
70
|
+
raise ValueError("invalid_build_resource_metric")
|
|
71
|
+
values["clean_" + key] = value
|
|
72
|
+
return values
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Conservative argument-to-parameter links for statically named Python callables.
|
|
2
|
+
|
|
3
|
+
Call contexts share parameter bindings: multiple call sites deliberately form a
|
|
4
|
+
union. Star expansion, descriptors and external callables retain uncertainty.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
|
|
9
|
+
from .resolution import TerminalResolver
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def connect_calls(documents, nodes, edges, edge, gap):
|
|
13
|
+
outgoing = defaultdict(list)
|
|
14
|
+
relations = {"binding", "definition", "import_binding", "import_member", "alias_value"}
|
|
15
|
+
pairs = (
|
|
16
|
+
edges.relation_pairs(relations)
|
|
17
|
+
if hasattr(edges, "relation_pairs")
|
|
18
|
+
else (
|
|
19
|
+
(item["source"], item["target"])
|
|
20
|
+
for item in edges.values()
|
|
21
|
+
if item.get("relation") in relations
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
for source, target in pairs:
|
|
25
|
+
outgoing[source].append(target)
|
|
26
|
+
parameters = {}
|
|
27
|
+
calls = []
|
|
28
|
+
for path, (facts, ids, bindings, module, references) in documents.items():
|
|
29
|
+
children = defaultdict(list)
|
|
30
|
+
for item in facts["syntax"]:
|
|
31
|
+
children[item["parent"]].append(item)
|
|
32
|
+
for item in facts["syntax"]:
|
|
33
|
+
if item["type"] in {"FunctionDef", "AsyncFunctionDef", "Lambda"}:
|
|
34
|
+
params = []
|
|
35
|
+
for child in children[item["id"]]:
|
|
36
|
+
if child["type"] == "arg":
|
|
37
|
+
symbol = bindings.get((item["id"], child["arg"]))
|
|
38
|
+
if symbol:
|
|
39
|
+
params.append((child["arg"], symbol, child["field"]))
|
|
40
|
+
positional = [p for p in params if p[2] in {"args", "posonlyargs"}]
|
|
41
|
+
keywords = defaultdict(list)
|
|
42
|
+
for name, symbol, kind in params:
|
|
43
|
+
if kind != "posonlyargs":
|
|
44
|
+
keywords[name].append(symbol)
|
|
45
|
+
parameters[ids[item["id"]]] = positional, keywords
|
|
46
|
+
if item["type"] == "Call":
|
|
47
|
+
calls.append((path, item, children, ids))
|
|
48
|
+
resolver = TerminalResolver(outgoing, parameters)
|
|
49
|
+
for path, call, children, ids in calls:
|
|
50
|
+
callee = next((n for n in children[call["id"]] if n["field"] == "func"), None)
|
|
51
|
+
if not callee:
|
|
52
|
+
continue
|
|
53
|
+
targets = resolver.resolve(ids[callee["id"]])
|
|
54
|
+
arguments = [n for n in children[call["id"]] if n["field"] == "args"]
|
|
55
|
+
keywords = [n for n in children[call["id"]] if n["field"] == "keywords"]
|
|
56
|
+
for target in targets:
|
|
57
|
+
edge(ids[call["id"]], target, "calls", resolution="potential")
|
|
58
|
+
positional, named = parameters[target]
|
|
59
|
+
for parameter, argument in zip(positional, arguments):
|
|
60
|
+
if argument["type"] == "Starred":
|
|
61
|
+
gap(path, ids[argument["id"]], "expanded_argument_binding")
|
|
62
|
+
break
|
|
63
|
+
edge(
|
|
64
|
+
parameter[1],
|
|
65
|
+
ids[argument["id"]],
|
|
66
|
+
"argument_value",
|
|
67
|
+
resolution="potential",
|
|
68
|
+
call=ids[call["id"]],
|
|
69
|
+
)
|
|
70
|
+
for keyword in keywords:
|
|
71
|
+
if not keyword.get("arg"):
|
|
72
|
+
gap(path, ids[keyword["id"]], "expanded_argument_binding")
|
|
73
|
+
continue
|
|
74
|
+
for symbol in named.get(keyword["arg"], ()):
|
|
75
|
+
edge(
|
|
76
|
+
symbol,
|
|
77
|
+
ids[keyword["id"]],
|
|
78
|
+
"argument_value",
|
|
79
|
+
resolution="potential",
|
|
80
|
+
call=ids[call["id"]],
|
|
81
|
+
)
|
|
82
|
+
return {
|
|
83
|
+
"call_resolution_requests": resolver.requests,
|
|
84
|
+
"call_resolution_cache_hits": resolver.hits,
|
|
85
|
+
"call_resolution_nodes": len(resolver.results),
|
|
86
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Explicit operator-provisioned grants; webhook payloads cannot create tenants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
_CANONICAL_ENCODER = json.JSONEncoder(sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
|
12
|
+
# "all" reconciles the default branch, open PR heads and merge-queue refs and
|
|
13
|
+
# schedules PR/merge-group event commits. "default_branch" limits automatic
|
|
14
|
+
# processing to the repository's current default branch: other refs and PR or
|
|
15
|
+
# merge-group events are retained as deliveries but never indexed.
|
|
16
|
+
TRACKED_REFS = frozenset({"all", "default_branch"})
|
|
17
|
+
INVENTORY_KEYS = frozenset(
|
|
18
|
+
{"api_token_env", "webhook_secret_env", "analyzer_config", "tracked_refs"}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def canonical(value):
|
|
23
|
+
return _CANONICAL_ENCODER.encode(value)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def digest(value):
|
|
27
|
+
return hashlib.sha256(canonical(value).encode()).hexdigest()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Repository:
|
|
32
|
+
tenant: str
|
|
33
|
+
installation: int
|
|
34
|
+
repository: int
|
|
35
|
+
name: str
|
|
36
|
+
api_token_env: str
|
|
37
|
+
webhook_secret_env: str
|
|
38
|
+
github_token_env: str = ""
|
|
39
|
+
local_git: str = ""
|
|
40
|
+
default_branch: str = "main"
|
|
41
|
+
analyzer_config: dict = field(default_factory=dict)
|
|
42
|
+
tracked_refs: str = "all"
|
|
43
|
+
|
|
44
|
+
def __post_init__(self):
|
|
45
|
+
if self.tracked_refs not in TRACKED_REFS:
|
|
46
|
+
raise ValueError("invalid_tracked_refs")
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def default_branch_only(self):
|
|
50
|
+
return self.tracked_refs == "default_branch"
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def scope(self):
|
|
54
|
+
return canonical([self.tenant, self.installation, self.repository])
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def config_digest(self):
|
|
58
|
+
return digest(self.analyzer_config)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class Settings:
|
|
63
|
+
database: str
|
|
64
|
+
repositories: list[Repository]
|
|
65
|
+
app_id: str = ""
|
|
66
|
+
private_key_env: str = "CONTINUUM_GITHUB_PRIVATE_KEY"
|
|
67
|
+
reconcile_seconds: int = 60
|
|
68
|
+
lease_seconds: int = 300
|
|
69
|
+
max_attempts: int = 5
|
|
70
|
+
archive_directory: str = ""
|
|
71
|
+
compile_workers: int = 0
|
|
72
|
+
overlap_validation: bool = False
|
|
73
|
+
index_workers: int = 0
|
|
74
|
+
native_publication_budget_bytes: int = 8 * 1024**3
|
|
75
|
+
# Whole-tree source admission limits for a single commit. The defaults match
|
|
76
|
+
# the historical provider constants; large repositories raise them explicitly.
|
|
77
|
+
source_bytes_limit: int = 50 * 1024 * 1024
|
|
78
|
+
source_files_limit: int = 10000
|
|
79
|
+
# Dedicated-App inventory grants (PostgreSQL `continuum_repositories`, kept
|
|
80
|
+
# current by the scheduled inventory sync). Enabled rows become repository
|
|
81
|
+
# grants sharing this credential pair, analyzer configuration and ref policy,
|
|
82
|
+
# so webhook admission, worker scope and dashboard history use one scope key.
|
|
83
|
+
inventory: dict | None = None
|
|
84
|
+
|
|
85
|
+
def grants(self, store=None):
|
|
86
|
+
"""Authoritative repository grants for the current operation.
|
|
87
|
+
|
|
88
|
+
Static `repositories` always apply. With `inventory` configured, enabled
|
|
89
|
+
inventory rows are read on every call, so newly granted repositories are
|
|
90
|
+
admitted and removed or suspended access stops admitting work without a
|
|
91
|
+
restart. Static grants take precedence over an inventory row for the
|
|
92
|
+
same repository ID.
|
|
93
|
+
"""
|
|
94
|
+
result = list(self.repositories)
|
|
95
|
+
if not self.inventory or store is None:
|
|
96
|
+
return result
|
|
97
|
+
with store.connect() as db:
|
|
98
|
+
rows = db.execute(
|
|
99
|
+
"SELECT scope,repository,installation,name FROM continuum_repositories "
|
|
100
|
+
"WHERE enabled ORDER BY repository"
|
|
101
|
+
).fetchall()
|
|
102
|
+
known = {repo.repository for repo in result}
|
|
103
|
+
for row in rows:
|
|
104
|
+
tenant, installation, repository = json.loads(row["scope"])
|
|
105
|
+
if row["repository"] in known or (installation, repository) != (
|
|
106
|
+
row["installation"],
|
|
107
|
+
row["repository"],
|
|
108
|
+
):
|
|
109
|
+
continue # A static grant wins; an inconsistent row never becomes authority.
|
|
110
|
+
result.append(
|
|
111
|
+
Repository(
|
|
112
|
+
tenant,
|
|
113
|
+
installation,
|
|
114
|
+
repository,
|
|
115
|
+
row["name"],
|
|
116
|
+
self.inventory["api_token_env"],
|
|
117
|
+
self.inventory["webhook_secret_env"],
|
|
118
|
+
analyzer_config=dict(self.inventory.get("analyzer_config", {})),
|
|
119
|
+
tracked_refs=self.inventory.get("tracked_refs", "default_branch"),
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def load(cls):
|
|
126
|
+
raw = json.loads(Path(os.environ["CONTINUUM_CONFIG"]).read_text())
|
|
127
|
+
if os.environ.get("CONTINUUM_DATABASE_URL"):
|
|
128
|
+
raw["database"] = os.environ["CONTINUUM_DATABASE_URL"]
|
|
129
|
+
if os.environ.get("CONTINUUM_APP_ID"):
|
|
130
|
+
raw["app_id"] = os.environ[
|
|
131
|
+
"CONTINUUM_APP_ID"
|
|
132
|
+
] # keeps the App identity out of config files
|
|
133
|
+
raw["repositories"] = [Repository(**r) for r in raw["repositories"]]
|
|
134
|
+
settings = cls(**raw)
|
|
135
|
+
if settings.inventory is not None:
|
|
136
|
+
inventory = settings.inventory
|
|
137
|
+
if (
|
|
138
|
+
not isinstance(inventory, dict)
|
|
139
|
+
or set(inventory) - INVENTORY_KEYS
|
|
140
|
+
or not {"api_token_env", "webhook_secret_env"} <= set(inventory)
|
|
141
|
+
or not all(
|
|
142
|
+
isinstance(inventory[key], str) and inventory[key]
|
|
143
|
+
for key in ("api_token_env", "webhook_secret_env")
|
|
144
|
+
)
|
|
145
|
+
or not isinstance(inventory.get("analyzer_config", {}), dict)
|
|
146
|
+
or inventory.get("tracked_refs", "default_branch") not in TRACKED_REFS
|
|
147
|
+
):
|
|
148
|
+
raise ValueError("invalid_inventory_configuration")
|
|
149
|
+
if not settings.database.startswith(("postgresql://", "postgres://")):
|
|
150
|
+
raise ValueError("inventory_grants_require_postgres")
|
|
151
|
+
if not os.environ.get(inventory["api_token_env"]) or not os.environ.get(
|
|
152
|
+
inventory["webhook_secret_env"]
|
|
153
|
+
):
|
|
154
|
+
raise ValueError("configured API and webhook credentials must be present")
|
|
155
|
+
if os.environ.get(inventory["api_token_env"]) in {
|
|
156
|
+
os.environ.get(r.api_token_env) for r in settings.repositories
|
|
157
|
+
}:
|
|
158
|
+
raise ValueError("each repository requires a distinct API credential")
|
|
159
|
+
if settings.archive_directory and not settings.database.startswith(
|
|
160
|
+
("postgresql://", "postgres://")
|
|
161
|
+
):
|
|
162
|
+
raise ValueError("native_archive_requires_postgres")
|
|
163
|
+
if type(settings.compile_workers) is not int or not 0 <= settings.compile_workers <= 8:
|
|
164
|
+
raise ValueError("invalid_compile_workers")
|
|
165
|
+
if settings.compile_workers and not settings.archive_directory:
|
|
166
|
+
raise ValueError("parallel_compile_requires_native_archive")
|
|
167
|
+
if type(settings.index_workers) is not int or not 0 <= settings.index_workers <= 8:
|
|
168
|
+
raise ValueError("invalid_index_workers")
|
|
169
|
+
if settings.index_workers and not settings.archive_directory:
|
|
170
|
+
raise ValueError("parallel_indexes_require_native_archive")
|
|
171
|
+
if type(settings.overlap_validation) is not bool or (
|
|
172
|
+
settings.overlap_validation and not settings.compile_workers
|
|
173
|
+
):
|
|
174
|
+
raise ValueError("overlap_validation_requires_parallel_compile")
|
|
175
|
+
if settings.native_publication_budget_bytes < 1:
|
|
176
|
+
raise ValueError("native_publication_budget_must_be_positive")
|
|
177
|
+
for name in ("source_bytes_limit", "source_files_limit"):
|
|
178
|
+
value = getattr(settings, name)
|
|
179
|
+
if type(value) is not int or value < 1:
|
|
180
|
+
raise ValueError("invalid_" + name)
|
|
181
|
+
ids = [r.repository for r in settings.repositories]
|
|
182
|
+
if len(ids) != len(set(ids)):
|
|
183
|
+
raise ValueError("repository IDs must have one tenant owner")
|
|
184
|
+
if min(settings.reconcile_seconds, settings.lease_seconds, settings.max_attempts) < 1:
|
|
185
|
+
raise ValueError("worker intervals must be positive")
|
|
186
|
+
api_credentials = [os.environ.get(r.api_token_env, "") for r in settings.repositories]
|
|
187
|
+
if len(api_credentials) != len(set(api_credentials)):
|
|
188
|
+
raise ValueError("each repository requires a distinct API credential")
|
|
189
|
+
for repo in settings.repositories:
|
|
190
|
+
if not os.environ.get(repo.api_token_env) or not os.environ.get(
|
|
191
|
+
repo.webhook_secret_env
|
|
192
|
+
):
|
|
193
|
+
raise ValueError("configured API and webhook credentials must be present")
|
|
194
|
+
return settings
|