forgexa-cli 1.78.1__tar.gz → 1.78.2__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.
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/PKG-INFO +3 -1
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli/daemon.py +1 -1
- forgexa_cli-1.78.2/forgexa_cli/observability_contract.py +833 -0
- forgexa_cli-1.78.2/forgexa_cli/observability_evidence.py +132 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli.egg-info/PKG-INFO +3 -1
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli.egg-info/SOURCES.txt +2 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli.egg-info/requires.txt +2 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/pyproject.toml +2 -2
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/README.md +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli/agent_core.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/setup.cfg +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.78.1 → forgexa_cli-1.78.2}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: forgexa-cli
|
|
3
|
-
Version: 1.78.
|
|
3
|
+
Version: 1.78.2
|
|
4
4
|
Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
|
|
5
5
|
Author-email: Jason Sun <dev.winds@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,6 +26,8 @@ Requires-Python: >=3.9
|
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
Requires-Dist: httpx>=0.24
|
|
28
28
|
Requires-Dist: socksio>=1.0.0
|
|
29
|
+
Requires-Dist: pydantic<3,>=2.10
|
|
30
|
+
Requires-Dist: pycountry>=24.6
|
|
29
31
|
Provides-Extra: daemon
|
|
30
32
|
Requires-Dist: httpx>=0.24; extra == "daemon"
|
|
31
33
|
Requires-Dist: socksio>=1.0.0; extra == "daemon"
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.78.
|
|
2
|
+
__version__ = "1.78.2"
|
|
@@ -1234,7 +1234,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
1234
1234
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
1235
1235
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
1236
1236
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
1237
|
-
DAEMON_VERSION = "1.78.
|
|
1237
|
+
DAEMON_VERSION = "1.78.2"
|
|
1238
1238
|
|
|
1239
1239
|
|
|
1240
1240
|
def _detect_client_type() -> str:
|
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
"""Generated by backend/scripts/export_observability_runtime.py; do not edit."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Union
|
|
4
|
+
from itertools import zip_longest
|
|
5
|
+
|
|
6
|
+
def _strict_zip(*iterables):
|
|
7
|
+
missing = object()
|
|
8
|
+
for items in zip_longest(*iterables, fillvalue=missing):
|
|
9
|
+
if any((item is missing for item in items)):
|
|
10
|
+
raise ValueError('zip() arguments have different lengths')
|
|
11
|
+
yield items
|
|
12
|
+
import pycountry
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from pydantic import ConfigDict
|
|
15
|
+
import math
|
|
16
|
+
from typing import Annotated
|
|
17
|
+
from typing import Literal
|
|
18
|
+
from urllib.parse import urlsplit
|
|
19
|
+
from uuid import UUID
|
|
20
|
+
from pydantic import AwareDatetime
|
|
21
|
+
from pydantic import Field
|
|
22
|
+
from pydantic import field_validator
|
|
23
|
+
from pydantic import model_serializer
|
|
24
|
+
from pydantic import model_validator
|
|
25
|
+
import json
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
Provider = Literal['mpulse', 'newrelic']
|
|
28
|
+
ResultType = Literal['summary', 'table', 'timeseries', 'histogram', 'distribution', 'records', 'span', 'trace', 'metadata']
|
|
29
|
+
Identifier = Annotated[str, Field(min_length=1, max_length=256, pattern='^[A-Za-z0-9_.:/-]+$')]
|
|
30
|
+
Scalar = Union[Union[Union[Annotated[str, Field(max_length=2048)], bool], int], float]
|
|
31
|
+
|
|
32
|
+
class ObservabilityModel(BaseModel):
|
|
33
|
+
model_config = ConfigDict(extra='forbid', allow_inf_nan=False, hide_input_in_errors=True)
|
|
34
|
+
|
|
35
|
+
def unique_values(values: list) -> list:
|
|
36
|
+
if len(values) != len(set(values)):
|
|
37
|
+
raise ValueError('Values must be unique')
|
|
38
|
+
return values
|
|
39
|
+
|
|
40
|
+
def validate_country_code(country: str) -> str:
|
|
41
|
+
if len(country) != 2 or not country.isupper() or pycountry.countries.get(alpha_2=country) is None:
|
|
42
|
+
raise ValueError('Invalid ISO country code')
|
|
43
|
+
return country
|
|
44
|
+
|
|
45
|
+
class ObservabilityWindow(ObservabilityModel):
|
|
46
|
+
start_time: AwareDatetime
|
|
47
|
+
end_time: AwareDatetime
|
|
48
|
+
|
|
49
|
+
@model_validator(mode='after')
|
|
50
|
+
def validate_window(self) -> ObservabilityWindow:
|
|
51
|
+
if self.start_time >= self.end_time:
|
|
52
|
+
raise ValueError('Window must have a positive duration')
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
class ObservabilitySelection(ObservabilityModel):
|
|
56
|
+
field_id: Union[Identifier, Literal['*']]
|
|
57
|
+
aggregate: Literal['native', 'count', 'sum', 'average', 'mean', 'min', 'max', 'percentile', 'rate', 'uniqueCount', 'apdex', 'histogram']
|
|
58
|
+
percentile: Union[float, None] = Field(default=None, ge=1, le=99, strict=True)
|
|
59
|
+
rate_interval_seconds: Union[int, None] = Field(default=None, ge=1, le=86400, strict=True)
|
|
60
|
+
threshold: Union[float, None] = Field(default=None, gt=0, strict=True)
|
|
61
|
+
|
|
62
|
+
@model_validator(mode='after')
|
|
63
|
+
def validate_arguments(self) -> ObservabilitySelection:
|
|
64
|
+
arguments = {'percentile': 'percentile', 'rate_interval_seconds': 'rate', 'threshold': 'apdex'}
|
|
65
|
+
for name, aggregate in arguments.items():
|
|
66
|
+
required = self.aggregate == aggregate
|
|
67
|
+
if required != (getattr(self, name) is not None):
|
|
68
|
+
raise ValueError('Aggregate arguments do not match the selected function')
|
|
69
|
+
if not required and name in self.model_fields_set:
|
|
70
|
+
raise ValueError('Aggregate does not accept this argument')
|
|
71
|
+
if self.field_id == '*' and self.aggregate != 'count':
|
|
72
|
+
raise ValueError('Wildcard fields are allowed only for count')
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
class ObservabilityCondition(ObservabilityModel):
|
|
76
|
+
field_id: Identifier
|
|
77
|
+
operator: Literal['eq', 'neq', 'in', 'not_in', 'gt', 'gte', 'lt', 'lte', 'exists', 'not_exists', 'prefix', 'contains']
|
|
78
|
+
value: Union[Scalar, None] = Field(default=None, repr=False)
|
|
79
|
+
values: Union[list[Scalar], None] = Field(default=None, min_length=1, max_length=100, repr=False)
|
|
80
|
+
|
|
81
|
+
@model_serializer(mode='wrap')
|
|
82
|
+
def serialize_operands(self, handler):
|
|
83
|
+
result = handler(self)
|
|
84
|
+
for name in ('value', 'values'):
|
|
85
|
+
if getattr(self, name) is None:
|
|
86
|
+
result.pop(name, None)
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
@model_validator(mode='after')
|
|
90
|
+
def validate_operands(self) -> ObservabilityCondition:
|
|
91
|
+
supplied = self.model_fields_set & {'value', 'values'}
|
|
92
|
+
if self.operator in {'exists', 'not_exists'}:
|
|
93
|
+
if supplied:
|
|
94
|
+
raise ValueError('Existence conditions do not accept operands')
|
|
95
|
+
elif self.operator in {'in', 'not_in'}:
|
|
96
|
+
if supplied != {'values'} or self.values is None:
|
|
97
|
+
raise ValueError('Set conditions require non-empty values')
|
|
98
|
+
elif supplied != {'value'} or self.value is None:
|
|
99
|
+
raise ValueError('Comparison conditions require one non-null value')
|
|
100
|
+
if self.operator in {'prefix', 'contains'} and (not isinstance(self.value, str)):
|
|
101
|
+
raise ValueError('Text conditions require a string')
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
class ObservabilityFilterGroup(ObservabilityModel):
|
|
105
|
+
operator: Literal['and', 'or']
|
|
106
|
+
conditions: list[Annotated[Union[ObservabilityCondition, ObservabilityFilterGroup], Field(discriminator='operator')]] = Field(min_length=1, max_length=100)
|
|
107
|
+
FilterTree = Annotated[Union[ObservabilityCondition, ObservabilityFilterGroup], Field(discriminator='operator')]
|
|
108
|
+
|
|
109
|
+
class ObservabilityOrder(ObservabilityModel):
|
|
110
|
+
field_id: Identifier
|
|
111
|
+
direction: Literal['asc', 'desc']
|
|
112
|
+
|
|
113
|
+
class ObservabilityQuery(ObservabilityModel):
|
|
114
|
+
dataset_id: Identifier
|
|
115
|
+
result_type: ResultType
|
|
116
|
+
select: Union[list[ObservabilitySelection], None] = Field(default=None, min_length=1, max_length=50)
|
|
117
|
+
fields: Union[list[Identifier], None] = Field(default=None, min_length=1, max_length=50)
|
|
118
|
+
filters: Union[FilterTree, None] = None
|
|
119
|
+
group_by: list[Identifier] = Field(default_factory=list, max_length=10)
|
|
120
|
+
order_by: list[ObservabilityOrder] = Field(default_factory=list, max_length=5)
|
|
121
|
+
interval_seconds: Union[int, None] = Field(default=None, ge=1, le=604800, strict=True)
|
|
122
|
+
limit: int = Field(default=100, ge=1, le=5000, strict=True)
|
|
123
|
+
|
|
124
|
+
@field_validator('filters', mode='before')
|
|
125
|
+
@classmethod
|
|
126
|
+
def validate_filter_size(cls, value):
|
|
127
|
+
pending = [(value, 1)] if value is not None else []
|
|
128
|
+
count = 0
|
|
129
|
+
while pending:
|
|
130
|
+
node, depth = pending.pop()
|
|
131
|
+
count += 1
|
|
132
|
+
if depth > 8 or count > 100:
|
|
133
|
+
raise ValueError('Filter tree exceeds its depth or node limit')
|
|
134
|
+
if isinstance(node, dict) and isinstance(node.get('conditions'), list):
|
|
135
|
+
if len(node['conditions']) > 100:
|
|
136
|
+
raise ValueError('Filter tree exceeds its node limit')
|
|
137
|
+
pending.extend(((child, depth + 1) for child in node['conditions']))
|
|
138
|
+
elif isinstance(node, ObservabilityFilterGroup):
|
|
139
|
+
pending.extend(((child, depth + 1) for child in node.conditions))
|
|
140
|
+
return value
|
|
141
|
+
|
|
142
|
+
@model_validator(mode='after')
|
|
143
|
+
def validate_shape(self) -> ObservabilityQuery:
|
|
144
|
+
projected = self.result_type in {'records', 'span', 'trace', 'metadata'}
|
|
145
|
+
if projected:
|
|
146
|
+
if not self.fields or 'select' in self.model_fields_set or self.group_by:
|
|
147
|
+
raise ValueError('Projected results require fields and reject aggregates or grouping')
|
|
148
|
+
unique_values(self.fields)
|
|
149
|
+
elif not self.select or 'fields' in self.model_fields_set:
|
|
150
|
+
raise ValueError('Aggregate results require selections and reject record projections')
|
|
151
|
+
unique_values(self.group_by)
|
|
152
|
+
unique_values([item.field_id for item in self.order_by])
|
|
153
|
+
if self.interval_seconds is not None and self.result_type != 'timeseries':
|
|
154
|
+
raise ValueError('Intervals are allowed only for time series')
|
|
155
|
+
return self
|
|
156
|
+
Sha256 = Annotated[str, Field(pattern='^[a-f0-9]{64}$')]
|
|
157
|
+
CellValue = Union[Union[Union[Union[str, bool], int], float], None]
|
|
158
|
+
CountKind = Literal['measurements', 'aggregate_requests', 'reported_events', 'estimated_events', 'sessions', 'unknown']
|
|
159
|
+
Statistic = Literal['native', 'count', 'sum', 'mean', 'average', 'min', 'max', 'percentile', 'rate', 'uniqueCount', 'apdex']
|
|
160
|
+
|
|
161
|
+
def validate_issuer(value: str) -> str:
|
|
162
|
+
try:
|
|
163
|
+
if any((character.isspace() or ord(character) < 32 for character in value)):
|
|
164
|
+
raise ValueError
|
|
165
|
+
parsed = urlsplit(value)
|
|
166
|
+
parsed.port
|
|
167
|
+
if not parsed.hostname or parsed.username or parsed.password:
|
|
168
|
+
raise ValueError
|
|
169
|
+
if parsed.path not in {'', '/'} or parsed.query or parsed.fragment:
|
|
170
|
+
raise ValueError
|
|
171
|
+
if parsed.scheme != 'https' and (not (parsed.scheme == 'http' and parsed.hostname in {'localhost', '127.0.0.1', '::1'})):
|
|
172
|
+
raise ValueError
|
|
173
|
+
except ValueError:
|
|
174
|
+
raise ValueError('observability_invalid_issuer') from None
|
|
175
|
+
return value
|
|
176
|
+
|
|
177
|
+
def validate_unit_values(native_value, value, native_unit, display_unit):
|
|
178
|
+
if native_value is None or value is None:
|
|
179
|
+
if native_value is not None or value is not None:
|
|
180
|
+
raise ValueError('observability_inconsistent_missing_value')
|
|
181
|
+
return
|
|
182
|
+
conversions = {('seconds', 'milliseconds'): 1000, ('milliseconds', 'seconds'): 0.001, ('ratio', 'percent'): 100, ('percent', 'ratio'): 0.01}
|
|
183
|
+
scale = 1 if native_unit == display_unit else conversions.get((native_unit, display_unit))
|
|
184
|
+
if scale is None or not math.isclose(native_value * scale, value, rel_tol=1e-12, abs_tol=1e-12):
|
|
185
|
+
raise ValueError('observability_unit_conversion_invalid')
|
|
186
|
+
|
|
187
|
+
class ObservabilityOrigin(ObservabilityModel):
|
|
188
|
+
issuer: str = Field(min_length=1, max_length=2048)
|
|
189
|
+
workspace_id: UUID
|
|
190
|
+
project_id: UUID
|
|
191
|
+
created_by_user_id: UUID
|
|
192
|
+
_validate_issuer = field_validator('issuer')(validate_issuer)
|
|
193
|
+
|
|
194
|
+
class ObservabilityCoverageWindow(ObservabilityWindow):
|
|
195
|
+
window_index: int = Field(default=0, ge=0, le=4, strict=True)
|
|
196
|
+
|
|
197
|
+
class ObservabilitySource(ObservabilityModel):
|
|
198
|
+
source_id: Identifier
|
|
199
|
+
target_ref: Identifier
|
|
200
|
+
connection_id: Identifier
|
|
201
|
+
environment: Identifier
|
|
202
|
+
endpoint_region: Identifier
|
|
203
|
+
scope_precision: Literal['dedicated', 'filtered']
|
|
204
|
+
origin_scope_ids: list[Identifier] = Field(min_length=1, max_length=100)
|
|
205
|
+
market_codes: list[str] = Field(default_factory=list, max_length=20)
|
|
206
|
+
application_id: Union[Identifier, None] = None
|
|
207
|
+
account_id: Union[int, None] = Field(default=None, ge=1, strict=True)
|
|
208
|
+
entity_guid: Union[str, None] = Field(default=None, min_length=1, max_length=256)
|
|
209
|
+
material_version: int = Field(ge=1, strict=True)
|
|
210
|
+
status: Literal['available', 'zero_matches', 'partial', 'unavailable']
|
|
211
|
+
completed: bool
|
|
212
|
+
actual_windows: list[ObservabilityCoverageWindow] = Field(default_factory=list, max_length=500)
|
|
213
|
+
|
|
214
|
+
class ObservabilitySafeCondition(ObservabilityModel):
|
|
215
|
+
field_id: Identifier
|
|
216
|
+
operator: Literal['eq', 'neq', 'in', 'not_in', 'gt', 'gte', 'lt', 'lte', 'exists', 'not_exists', 'prefix', 'contains']
|
|
217
|
+
values_redacted: Literal[True] = True
|
|
218
|
+
|
|
219
|
+
class ObservabilitySafeFilterGroup(ObservabilityModel):
|
|
220
|
+
operator: Literal['and', 'or']
|
|
221
|
+
conditions: list[Annotated[Union[ObservabilitySafeCondition, ObservabilitySafeFilterGroup], Field(discriminator='operator')]] = Field(min_length=1, max_length=100)
|
|
222
|
+
|
|
223
|
+
class ObservabilityFrozenScope(ObservabilityModel):
|
|
224
|
+
scope_mode: Literal['common', 'market']
|
|
225
|
+
scope_ids: list[Identifier] = Field(default_factory=list, max_length=20)
|
|
226
|
+
country_codes: list[str] = Field(default_factory=list, max_length=20)
|
|
227
|
+
environment: Union[Identifier, None] = None
|
|
228
|
+
target_refs: list[Identifier] = Field(min_length=1, max_length=100)
|
|
229
|
+
|
|
230
|
+
@model_validator(mode='after')
|
|
231
|
+
def validate_scope(self) -> ObservabilityFrozenScope:
|
|
232
|
+
for values in (self.scope_ids, self.country_codes, self.target_refs):
|
|
233
|
+
unique_values(values)
|
|
234
|
+
if self.scope_mode == 'common':
|
|
235
|
+
if not self.scope_ids or self.country_codes or self.environment is not None:
|
|
236
|
+
raise ValueError('observability_scope_mode_mismatch')
|
|
237
|
+
elif not self.country_codes or not self.environment or self.scope_ids:
|
|
238
|
+
raise ValueError('observability_scope_mode_mismatch')
|
|
239
|
+
for country in self.country_codes:
|
|
240
|
+
validate_country_code(country)
|
|
241
|
+
return self
|
|
242
|
+
|
|
243
|
+
class ObservabilitySafeParameters(ObservabilityModel):
|
|
244
|
+
dataset_id: Identifier
|
|
245
|
+
request_digest: Sha256
|
|
246
|
+
field_ids: list[Identifier] = Field(min_length=1, max_length=50)
|
|
247
|
+
filter_fields: list[Identifier] = Field(default_factory=list, max_length=100)
|
|
248
|
+
filter_values_redacted: Literal[True] = True
|
|
249
|
+
filters: Union[Annotated[Union[ObservabilitySafeCondition, ObservabilitySafeFilterGroup], Field(discriminator='operator')], None] = None
|
|
250
|
+
query_ref: Identifier
|
|
251
|
+
limit: Union[int, None] = Field(default=None, ge=1, le=5000, strict=True)
|
|
252
|
+
interval_seconds: Union[int, None] = Field(default=None, ge=1, strict=True)
|
|
253
|
+
workload_ids: Union[list[Identifier], None] = Field(default=None, min_length=1, max_length=1)
|
|
254
|
+
|
|
255
|
+
@model_serializer(mode='wrap')
|
|
256
|
+
def serialize_optional_workloads(self, handler):
|
|
257
|
+
value = handler(self)
|
|
258
|
+
if self.workload_ids is None:
|
|
259
|
+
value.pop('workload_ids', None)
|
|
260
|
+
return value
|
|
261
|
+
|
|
262
|
+
@field_validator('filters', mode='before')
|
|
263
|
+
@classmethod
|
|
264
|
+
def validate_filter_size(cls, value):
|
|
265
|
+
if isinstance(value, ObservabilityModel):
|
|
266
|
+
value = value.model_dump()
|
|
267
|
+
return ObservabilityQuery.validate_filter_size(value)
|
|
268
|
+
|
|
269
|
+
def validate_redacted_value(value, mode):
|
|
270
|
+
if value is None or mode == 'none':
|
|
271
|
+
return
|
|
272
|
+
valid = value == '[REDACTED]' if mode == 'mask' else isinstance(value, str) and value.startswith('sha256:') and (len(value) == 71) and all((character in '0123456789abcdef' for character in value[7:]))
|
|
273
|
+
if not valid:
|
|
274
|
+
raise ValueError('observability_redacted_value_invalid')
|
|
275
|
+
|
|
276
|
+
class ObservabilityColumn(ObservabilityModel):
|
|
277
|
+
field_id: Identifier
|
|
278
|
+
value_type: Literal['string', 'number', 'integer', 'boolean', 'timestamp']
|
|
279
|
+
classification: Literal['aggregate', 'identifier', 'text']
|
|
280
|
+
native_unit: Union[str, None] = Field(default=None, min_length=1, max_length=128)
|
|
281
|
+
display_unit: Union[str, None] = Field(default=None, min_length=1, max_length=128)
|
|
282
|
+
definition_version: int = Field(default=1, ge=1, strict=True)
|
|
283
|
+
statistic: Union[Statistic, None] = None
|
|
284
|
+
percentile: Union[float, None] = Field(default=None, ge=1, le=99, strict=True)
|
|
285
|
+
redaction: Literal['none', 'mask', 'sha256'] = 'none'
|
|
286
|
+
native_value_type: Union[Literal['string', 'number', 'integer', 'boolean', 'timestamp'], None] = None
|
|
287
|
+
|
|
288
|
+
@model_validator(mode='after')
|
|
289
|
+
def validate_statistic(self) -> ObservabilityColumn:
|
|
290
|
+
if (self.statistic == 'percentile') != (self.percentile is not None):
|
|
291
|
+
raise ValueError('observability_percentile_required')
|
|
292
|
+
if (self.redaction == 'none') != (self.native_value_type is None) or (self.redaction != 'none' and self.value_type != 'string'):
|
|
293
|
+
raise ValueError('observability_redacted_column_invalid')
|
|
294
|
+
return self
|
|
295
|
+
|
|
296
|
+
@model_serializer(mode='wrap')
|
|
297
|
+
def serialize_column(self, handler):
|
|
298
|
+
value = handler(self)
|
|
299
|
+
if self.redaction == 'none':
|
|
300
|
+
value.pop('redaction', None)
|
|
301
|
+
value.pop('native_value_type', None)
|
|
302
|
+
return value
|
|
303
|
+
|
|
304
|
+
class ObservabilityMetricQuality(ObservabilityModel):
|
|
305
|
+
status: Literal['available', 'missing', 'unsupported', 'unavailable']
|
|
306
|
+
sampling: Identifier
|
|
307
|
+
actual_interval_seconds: Union[int, None] = Field(default=None, ge=1, strict=True)
|
|
308
|
+
provisional: bool = False
|
|
309
|
+
truncated: bool = False
|
|
310
|
+
|
|
311
|
+
class ObservabilityMetricValue(ObservabilityWindow):
|
|
312
|
+
kind: Literal['metric'] = 'metric'
|
|
313
|
+
evidence_id: Identifier
|
|
314
|
+
integration_type: Provider
|
|
315
|
+
evidence_type: Literal['mpulse_data', 'newrelic_data']
|
|
316
|
+
data_family: Identifier
|
|
317
|
+
result_type: ResultType
|
|
318
|
+
source_id: Identifier
|
|
319
|
+
target_ref: Union[Identifier, None] = None
|
|
320
|
+
scope_id: Identifier
|
|
321
|
+
workload_id: Union[Identifier, None] = None
|
|
322
|
+
window_index: int = Field(default=0, ge=0, le=4, strict=True)
|
|
323
|
+
metric_id: Identifier
|
|
324
|
+
definition_version: int = Field(ge=1, strict=True)
|
|
325
|
+
data_type: Identifier
|
|
326
|
+
native_metric: str = Field(min_length=1, max_length=256)
|
|
327
|
+
statistic: Statistic
|
|
328
|
+
percentile: Union[float, None] = Field(default=None, ge=1, le=99, strict=True)
|
|
329
|
+
native_unit: str = Field(min_length=1, max_length=128)
|
|
330
|
+
native_value: Union[float, None] = Field(default=None, strict=True)
|
|
331
|
+
display_unit: str = Field(min_length=1, max_length=128)
|
|
332
|
+
value: Union[float, None] = Field(default=None, strict=True)
|
|
333
|
+
sample_count: Union[int, None] = Field(default=None, ge=0, strict=True)
|
|
334
|
+
count_kind: CountKind
|
|
335
|
+
query_ref: Identifier
|
|
336
|
+
quality: ObservabilityMetricQuality
|
|
337
|
+
|
|
338
|
+
@model_validator(mode='after')
|
|
339
|
+
def validate_measurement(self) -> ObservabilityMetricValue:
|
|
340
|
+
if self.evidence_type != f'{self.integration_type}_data':
|
|
341
|
+
raise ValueError('observability_provider_mismatch')
|
|
342
|
+
if (self.statistic == 'percentile') != (self.percentile is not None):
|
|
343
|
+
raise ValueError('observability_percentile_required')
|
|
344
|
+
if self.quality.status == 'available' and self.value is None:
|
|
345
|
+
raise ValueError('observability_available_value_required')
|
|
346
|
+
if self.quality.status != 'available' and self.value is not None:
|
|
347
|
+
raise ValueError('observability_missing_value_must_be_null')
|
|
348
|
+
if self.statistic == 'percentile' and self.value is not None and (self.sample_count == 0):
|
|
349
|
+
raise ValueError('observability_empty_percentile_population')
|
|
350
|
+
validate_unit_values(self.native_value, self.value, self.native_unit, self.display_unit)
|
|
351
|
+
return self
|
|
352
|
+
|
|
353
|
+
class ObservabilityRow(ObservabilityModel):
|
|
354
|
+
kind: Literal['row', 'metadata']
|
|
355
|
+
item_id: Identifier
|
|
356
|
+
target_ref: Identifier
|
|
357
|
+
window_index: int = Field(default=0, ge=0, le=4, strict=True)
|
|
358
|
+
values: dict[Identifier, CellValue] = Field(min_length=1, max_length=50)
|
|
359
|
+
|
|
360
|
+
class ObservabilityTimeBucket(ObservabilityWindow):
|
|
361
|
+
item_id: Identifier
|
|
362
|
+
native_value: Union[float, None] = Field(default=None, strict=True)
|
|
363
|
+
value: Union[float, None] = Field(default=None, strict=True)
|
|
364
|
+
sample_count: Union[int, None] = Field(default=None, ge=0, strict=True)
|
|
365
|
+
|
|
366
|
+
class ObservabilitySeries(ObservabilityModel):
|
|
367
|
+
kind: Literal['series']
|
|
368
|
+
item_id: Identifier
|
|
369
|
+
target_ref: Identifier
|
|
370
|
+
window_index: int = Field(default=0, ge=0, le=4, strict=True)
|
|
371
|
+
field_id: Identifier
|
|
372
|
+
dimensions: dict[Identifier, CellValue] = Field(default_factory=dict, max_length=10)
|
|
373
|
+
native_unit: str = Field(min_length=1, max_length=128)
|
|
374
|
+
display_unit: str = Field(min_length=1, max_length=128)
|
|
375
|
+
count_kind: CountKind
|
|
376
|
+
statistic: Statistic
|
|
377
|
+
percentile: Union[float, None] = Field(default=None, ge=1, le=99, strict=True)
|
|
378
|
+
buckets: list[ObservabilityTimeBucket] = Field(min_length=1, max_length=50000)
|
|
379
|
+
|
|
380
|
+
@model_serializer(mode='wrap')
|
|
381
|
+
def preserve_legacy_series(self, handler):
|
|
382
|
+
result = handler(self)
|
|
383
|
+
if not self.dimensions:
|
|
384
|
+
result.pop('dimensions', None)
|
|
385
|
+
return result
|
|
386
|
+
|
|
387
|
+
@model_validator(mode='after')
|
|
388
|
+
def validate_buckets(self) -> ObservabilitySeries:
|
|
389
|
+
if (self.statistic == 'percentile') != (self.percentile is not None):
|
|
390
|
+
raise ValueError('observability_percentile_required')
|
|
391
|
+
for bucket in self.buckets:
|
|
392
|
+
validate_unit_values(bucket.native_value, bucket.value, self.native_unit, self.display_unit)
|
|
393
|
+
for previous, current in zip(self.buckets, self.buckets[1:]):
|
|
394
|
+
if previous.end_time > current.start_time:
|
|
395
|
+
raise ValueError('observability_time_buckets_overlap')
|
|
396
|
+
return self
|
|
397
|
+
|
|
398
|
+
class ObservabilityHistory(ObservabilityModel):
|
|
399
|
+
kind: Literal['history'] = 'history'
|
|
400
|
+
item_id: Identifier
|
|
401
|
+
target_ref: Identifier
|
|
402
|
+
window_index: int = Field(default=0, ge=0, le=4, strict=True)
|
|
403
|
+
field_id: Identifier
|
|
404
|
+
axis: Literal['sample_index'] = 'sample_index'
|
|
405
|
+
data_timezone: Union[str, None] = Field(default=None, max_length=128)
|
|
406
|
+
native_unit: str = Field(min_length=1, max_length=128)
|
|
407
|
+
display_unit: str = Field(min_length=1, max_length=128)
|
|
408
|
+
statistic: Statistic
|
|
409
|
+
percentile: Union[float, None] = Field(default=None, ge=1, le=99, strict=True)
|
|
410
|
+
count_kind: CountKind
|
|
411
|
+
native_values: list[Union[float, None]] = Field(max_length=50000)
|
|
412
|
+
values: list[Union[float, None]] = Field(max_length=50000)
|
|
413
|
+
latest_native_value: Union[float, None] = Field(default=None, strict=True)
|
|
414
|
+
latest_value: Union[float, None] = Field(default=None, strict=True)
|
|
415
|
+
|
|
416
|
+
@field_validator('native_values', 'values', mode='before')
|
|
417
|
+
@classmethod
|
|
418
|
+
def numeric_history(cls, values):
|
|
419
|
+
if not isinstance(values, list) or any((value is not None and type(value) not in {int, float} for value in values)):
|
|
420
|
+
raise ValueError('observability_history_value_invalid')
|
|
421
|
+
return values
|
|
422
|
+
|
|
423
|
+
@model_validator(mode='after')
|
|
424
|
+
def validate_history(self):
|
|
425
|
+
if not self.native_values and self.latest_native_value is None:
|
|
426
|
+
raise ValueError('observability_history_empty')
|
|
427
|
+
if len(self.native_values) != len(self.values) or (self.statistic == 'percentile') != (self.percentile is not None):
|
|
428
|
+
raise ValueError('observability_history_semantics_invalid')
|
|
429
|
+
for native_value, value in _strict_zip(self.native_values, self.values):
|
|
430
|
+
validate_unit_values(native_value, value, self.native_unit, self.display_unit)
|
|
431
|
+
validate_unit_values(self.latest_native_value, self.latest_value, self.native_unit, self.display_unit)
|
|
432
|
+
return self
|
|
433
|
+
|
|
434
|
+
class ObservabilityDistributionBucket(ObservabilityModel):
|
|
435
|
+
item_id: Identifier
|
|
436
|
+
lower: Union[float, None] = Field(default=None, strict=True)
|
|
437
|
+
upper: Union[float, None] = Field(default=None, strict=True)
|
|
438
|
+
count: Union[int, None] = Field(default=None, ge=0, strict=True)
|
|
439
|
+
native_value: Union[float, None] = Field(default=None, strict=True)
|
|
440
|
+
value: Union[float, None] = Field(default=None, strict=True)
|
|
441
|
+
|
|
442
|
+
@model_validator(mode='after')
|
|
443
|
+
def validate_edges(self) -> ObservabilityDistributionBucket:
|
|
444
|
+
if self.lower is not None and self.upper is not None and (self.lower >= self.upper):
|
|
445
|
+
raise ValueError('observability_distribution_edges_invalid')
|
|
446
|
+
return self
|
|
447
|
+
|
|
448
|
+
class ObservabilityDistribution(ObservabilityModel):
|
|
449
|
+
kind: Literal['distribution']
|
|
450
|
+
item_id: Identifier
|
|
451
|
+
target_ref: Identifier
|
|
452
|
+
window_index: int = Field(default=0, ge=0, le=4, strict=True)
|
|
453
|
+
field_id: Identifier
|
|
454
|
+
measurement_unit: str = Field(min_length=1, max_length=128)
|
|
455
|
+
native_unit: Union[str, None] = Field(default=None, min_length=1, max_length=128)
|
|
456
|
+
display_unit: Union[str, None] = Field(default=None, min_length=1, max_length=128)
|
|
457
|
+
count_kind: CountKind
|
|
458
|
+
buckets: list[ObservabilityDistributionBucket] = Field(min_length=1, max_length=50000)
|
|
459
|
+
|
|
460
|
+
@model_validator(mode='after')
|
|
461
|
+
def validate_values(self) -> ObservabilityDistribution:
|
|
462
|
+
for bucket in self.buckets:
|
|
463
|
+
if bucket.value is not None or bucket.native_value is not None:
|
|
464
|
+
if self.native_unit is None or self.display_unit is None:
|
|
465
|
+
raise ValueError('observability_distribution_value_units_required')
|
|
466
|
+
validate_unit_values(bucket.native_value, bucket.value, self.native_unit, self.display_unit)
|
|
467
|
+
return self
|
|
468
|
+
|
|
469
|
+
class ObservabilitySpan(ObservabilityModel):
|
|
470
|
+
kind: Literal['span']
|
|
471
|
+
item_id: Identifier
|
|
472
|
+
target_ref: Identifier
|
|
473
|
+
window_index: int = Field(default=0, ge=0, le=4, strict=True)
|
|
474
|
+
trace_id: Identifier
|
|
475
|
+
span_id: Identifier
|
|
476
|
+
parent_span_id: Union[Identifier, None] = None
|
|
477
|
+
parent_missing: bool
|
|
478
|
+
values: dict[Identifier, CellValue] = Field(default_factory=dict, max_length=50)
|
|
479
|
+
|
|
480
|
+
class ObservabilityRedactedItem(ObservabilityModel):
|
|
481
|
+
kind: Literal['redacted']
|
|
482
|
+
item_id: Identifier
|
|
483
|
+
target_ref: Identifier
|
|
484
|
+
window_index: int = Field(default=0, ge=0, le=4, strict=True)
|
|
485
|
+
original_kind: Literal['metric', 'series', 'history', 'distribution', 'span']
|
|
486
|
+
value_scope: Literal['whole_field_structure']
|
|
487
|
+
quality: Union[ObservabilityMetricQuality, None] = None
|
|
488
|
+
primary_field_id: Union[Identifier, None] = None
|
|
489
|
+
bucket_refs: list[Identifier] = Field(default_factory=list, max_length=1000)
|
|
490
|
+
values: dict[Identifier, CellValue] = Field(min_length=1, max_length=50)
|
|
491
|
+
|
|
492
|
+
@model_validator(mode='after')
|
|
493
|
+
def validate_representation(self):
|
|
494
|
+
unique_values(self.bucket_refs)
|
|
495
|
+
if self.original_kind == 'span':
|
|
496
|
+
if self.primary_field_id is not None or self.bucket_refs:
|
|
497
|
+
raise ValueError('observability_redacted_structure_invalid')
|
|
498
|
+
elif self.primary_field_id not in self.values or (self.bucket_refs and self.original_kind not in {'series', 'distribution'}):
|
|
499
|
+
raise ValueError('observability_redacted_structure_invalid')
|
|
500
|
+
return self
|
|
501
|
+
DataItem = Annotated[Union[Union[Union[Union[Union[Union[ObservabilityMetricValue, ObservabilityRow], ObservabilitySeries], ObservabilityHistory], ObservabilityDistribution], ObservabilitySpan], ObservabilityRedactedItem], Field(discriminator='kind')]
|
|
502
|
+
|
|
503
|
+
class ObservabilitySnapshotQuality(ObservabilityModel):
|
|
504
|
+
status: Literal['available', 'zero_matches', 'partial', 'unavailable']
|
|
505
|
+
completed: bool
|
|
506
|
+
sampling: Identifier
|
|
507
|
+
truncated: bool = False
|
|
508
|
+
limitations: list[Identifier] = Field(default_factory=list, max_length=32)
|
|
509
|
+
|
|
510
|
+
class ObservabilitySnapshotPayload(ObservabilityModel):
|
|
511
|
+
schema_version: Literal[1] = 1
|
|
512
|
+
snapshot_id: UUID
|
|
513
|
+
integration_type: Provider
|
|
514
|
+
evidence_type: Literal['mpulse_data', 'newrelic_data']
|
|
515
|
+
origin: ObservabilityOrigin
|
|
516
|
+
data_family: Identifier
|
|
517
|
+
result_type: ResultType
|
|
518
|
+
config_version: int = Field(ge=1, strict=True)
|
|
519
|
+
catalog_version: int = Field(ge=1, strict=True)
|
|
520
|
+
policy_version: int = Field(ge=1, strict=True)
|
|
521
|
+
source_signature: Sha256
|
|
522
|
+
recipe_dependencies: dict[Identifier, Sha256] = Field(min_length=1, max_length=50)
|
|
523
|
+
scope: ObservabilityFrozenScope
|
|
524
|
+
sources: list[ObservabilitySource] = Field(min_length=1, max_length=100)
|
|
525
|
+
parameters: ObservabilitySafeParameters
|
|
526
|
+
requested_windows: list[ObservabilityWindow] = Field(min_length=1, max_length=5)
|
|
527
|
+
actual_windows: list[ObservabilityCoverageWindow] = Field(default_factory=list, max_length=500)
|
|
528
|
+
columns: list[ObservabilityColumn] = Field(min_length=1, max_length=50)
|
|
529
|
+
data: list[DataItem] = Field(max_length=50000)
|
|
530
|
+
quality: ObservabilitySnapshotQuality
|
|
531
|
+
collected_at: AwareDatetime
|
|
532
|
+
expires_at: AwareDatetime
|
|
533
|
+
|
|
534
|
+
@model_validator(mode='after')
|
|
535
|
+
def validate_dataset(self) -> ObservabilitySnapshotPayload:
|
|
536
|
+
if self.evidence_type != f'{self.integration_type}_data':
|
|
537
|
+
raise ValueError('observability_provider_mismatch')
|
|
538
|
+
if not self.parameters.dataset_id.startswith(f'{self.integration_type}.'):
|
|
539
|
+
raise ValueError('observability_dataset_provider_mismatch')
|
|
540
|
+
if self.expires_at <= self.collected_at:
|
|
541
|
+
raise ValueError('observability_expiry_invalid')
|
|
542
|
+
source_refs = [source.target_ref for source in self.sources]
|
|
543
|
+
unique_values(source_refs)
|
|
544
|
+
unique_values([source.source_id for source in self.sources])
|
|
545
|
+
if set(source_refs) != set(self.scope.target_refs):
|
|
546
|
+
raise ValueError('observability_target_set_mismatch')
|
|
547
|
+
for source in self.sources:
|
|
548
|
+
unique_values(source.origin_scope_ids)
|
|
549
|
+
unique_values(source.market_codes)
|
|
550
|
+
for country in source.market_codes:
|
|
551
|
+
validate_country_code(country)
|
|
552
|
+
if self.integration_type == 'mpulse':
|
|
553
|
+
if not source.application_id or source.account_id is not None or source.entity_guid:
|
|
554
|
+
raise ValueError('observability_source_identity_invalid')
|
|
555
|
+
if source.endpoint_region != 'mpulse.soasta.com':
|
|
556
|
+
raise ValueError('observability_source_region_invalid')
|
|
557
|
+
elif source.application_id or source.account_id is None or (not source.entity_guid):
|
|
558
|
+
raise ValueError('observability_source_identity_invalid')
|
|
559
|
+
elif source.endpoint_region not in {'US', 'EU', 'JP'}:
|
|
560
|
+
raise ValueError('observability_source_region_invalid')
|
|
561
|
+
if source.completed and (not source.actual_windows):
|
|
562
|
+
raise ValueError('observability_completed_coverage_required')
|
|
563
|
+
if any((window.window_index >= len(self.requested_windows) for window in source.actual_windows)):
|
|
564
|
+
raise ValueError('observability_coverage_window_invalid')
|
|
565
|
+
if self.scope.scope_mode == 'common':
|
|
566
|
+
origins = {value for source in self.sources for value in source.origin_scope_ids}
|
|
567
|
+
if origins != set(self.scope.scope_ids) or any((source.market_codes for source in self.sources)):
|
|
568
|
+
raise ValueError('observability_scope_origins_mismatch')
|
|
569
|
+
else:
|
|
570
|
+
markets = {value for source in self.sources for value in source.market_codes}
|
|
571
|
+
if markets != set(self.scope.country_codes) or any((source.environment != self.scope.environment for source in self.sources)):
|
|
572
|
+
raise ValueError('observability_scope_origins_mismatch')
|
|
573
|
+
column_map = {column.field_id: column for column in self.columns}
|
|
574
|
+
unique_values([column.field_id for column in self.columns])
|
|
575
|
+
unique_values(self.parameters.field_ids)
|
|
576
|
+
if set(self.parameters.field_ids) != set(column_map):
|
|
577
|
+
raise ValueError('observability_projection_not_declared')
|
|
578
|
+
identifiers = []
|
|
579
|
+
point_count = 0
|
|
580
|
+
shape_kinds = {'summary': {'metric', 'row'}, 'table': {'row'}, 'records': {'row'}, 'timeseries': {'series', 'history'}, 'histogram': {'distribution'}, 'distribution': {'distribution'}, 'span': {'span'}, 'trace': {'span'}, 'metadata': {'metadata'}}
|
|
581
|
+
for item in self.data:
|
|
582
|
+
native_kind = item.original_kind if item.kind == 'redacted' else item.kind
|
|
583
|
+
if native_kind not in shape_kinds[self.result_type] or item.target_ref not in source_refs:
|
|
584
|
+
raise ValueError('observability_result_shape_or_target_invalid')
|
|
585
|
+
if item.window_index >= len(self.requested_windows):
|
|
586
|
+
raise ValueError('observability_data_window_invalid')
|
|
587
|
+
identifiers.append(item.evidence_id if item.kind == 'metric' else item.item_id)
|
|
588
|
+
if item.kind == 'redacted':
|
|
589
|
+
if not any((name in column_map and column_map[name].redaction != 'none' for name in item.values)):
|
|
590
|
+
raise ValueError('observability_redacted_representation_required')
|
|
591
|
+
if item.primary_field_id and (item.primary_field_id not in column_map or column_map[item.primary_field_id].redaction == 'none'):
|
|
592
|
+
raise ValueError('observability_redacted_representation_required')
|
|
593
|
+
identifiers.extend(item.bucket_refs)
|
|
594
|
+
point_count += len(item.bucket_refs)
|
|
595
|
+
if item.kind == 'history':
|
|
596
|
+
if item.field_id not in column_map:
|
|
597
|
+
raise ValueError('observability_projection_not_declared')
|
|
598
|
+
column = column_map[item.field_id]
|
|
599
|
+
if column.redaction != 'none':
|
|
600
|
+
raise ValueError('observability_redacted_representation_required')
|
|
601
|
+
if column.statistic != item.statistic or column.percentile != item.percentile:
|
|
602
|
+
raise ValueError('observability_statistic_mismatch')
|
|
603
|
+
point_count += max(1, len(item.values))
|
|
604
|
+
elif item.kind in {'series', 'distribution'}:
|
|
605
|
+
if item.field_id not in column_map:
|
|
606
|
+
raise ValueError('observability_projection_not_declared')
|
|
607
|
+
if column_map[item.field_id].redaction != 'none':
|
|
608
|
+
raise ValueError('observability_redacted_representation_required')
|
|
609
|
+
if item.kind == 'series':
|
|
610
|
+
column = column_map[item.field_id]
|
|
611
|
+
if column.statistic != item.statistic or column.percentile != item.percentile:
|
|
612
|
+
raise ValueError('observability_statistic_mismatch')
|
|
613
|
+
for field_id, value in item.dimensions.items():
|
|
614
|
+
if field_id not in column_map:
|
|
615
|
+
raise ValueError('observability_projection_not_declared')
|
|
616
|
+
validate_redacted_value(value, column_map[field_id].redaction)
|
|
617
|
+
expected = column_map[field_id].value_type
|
|
618
|
+
if value is not None and (not (expected == 'number' and type(value) in {int, float} or (expected == 'integer' and type(value) is int) or (expected == 'boolean' and type(value) is bool) or (expected in {'string', 'timestamp'} and isinstance(value, str)))):
|
|
619
|
+
raise ValueError('observability_column_type_invalid')
|
|
620
|
+
identifiers.extend((bucket.item_id for bucket in item.buckets))
|
|
621
|
+
point_count += len(item.buckets)
|
|
622
|
+
if self.result_type == 'histogram' and any((bucket.count is None or bucket.value is not None or bucket.native_value is not None for bucket in item.buckets)):
|
|
623
|
+
raise ValueError('observability_histogram_requires_native_counts')
|
|
624
|
+
elif item.kind == 'metric':
|
|
625
|
+
if item.metric_id not in column_map or item.integration_type != self.integration_type:
|
|
626
|
+
raise ValueError('observability_metric_not_declared')
|
|
627
|
+
if column_map[item.metric_id].redaction != 'none':
|
|
628
|
+
raise ValueError('observability_redacted_representation_required')
|
|
629
|
+
source = next((source for source in self.sources if source.target_ref == item.target_ref))
|
|
630
|
+
if item.source_id != source.source_id or item.data_family != self.data_family:
|
|
631
|
+
raise ValueError('observability_metric_source_mismatch')
|
|
632
|
+
if item.definition_version != column_map[item.metric_id].definition_version:
|
|
633
|
+
raise ValueError('observability_definition_version_mismatch')
|
|
634
|
+
if column_map[item.metric_id].statistic != item.statistic:
|
|
635
|
+
raise ValueError('observability_statistic_mismatch')
|
|
636
|
+
else:
|
|
637
|
+
if item.kind == 'span' and any((column.redaction != 'none' for column in self.columns)):
|
|
638
|
+
raise ValueError('observability_redacted_representation_required')
|
|
639
|
+
for field_id, value in item.values.items():
|
|
640
|
+
if field_id not in column_map:
|
|
641
|
+
raise ValueError('observability_projection_not_declared')
|
|
642
|
+
mode = column_map[field_id].redaction
|
|
643
|
+
validate_redacted_value(value, mode)
|
|
644
|
+
expected = column_map[field_id].value_type
|
|
645
|
+
valid = value is None or (expected == 'number' and type(value) in {int, float} or (expected == 'integer' and type(value) is int) or (expected == 'boolean' and type(value) is bool) or (expected in {'string', 'timestamp'} and isinstance(value, str)))
|
|
646
|
+
if not valid:
|
|
647
|
+
raise ValueError('observability_column_type_invalid')
|
|
648
|
+
if value is not None and expected == 'timestamp':
|
|
649
|
+
try:
|
|
650
|
+
if datetime.fromisoformat(value.replace('Z', '+00:00')).tzinfo is None:
|
|
651
|
+
raise ValueError
|
|
652
|
+
except ValueError:
|
|
653
|
+
raise ValueError('observability_timestamp_invalid') from None
|
|
654
|
+
unique_values(identifiers)
|
|
655
|
+
if point_count > 50000 or len(self.data) > 1000:
|
|
656
|
+
raise ValueError('observability_result_budget_exceeded')
|
|
657
|
+
if self.result_type == 'timeseries' and len(self.data) > 100:
|
|
658
|
+
raise ValueError('observability_series_budget_exceeded')
|
|
659
|
+
if self.result_type == 'trace' and len({item.trace_id for item in self.data if item.kind == 'span'}) > 5:
|
|
660
|
+
raise ValueError('observability_trace_budget_exceeded')
|
|
661
|
+
if self.result_type in {'span', 'trace'}:
|
|
662
|
+
if not {'span.id', 'trace.id', 'parent.id'} <= column_map.keys():
|
|
663
|
+
raise ValueError('observability_span_identifiers_required')
|
|
664
|
+
identities = [(item.target_ref, item.window_index, item.trace_id, item.span_id) for item in self.data if item.kind == 'span']
|
|
665
|
+
unique_values(identities)
|
|
666
|
+
for item in self.data:
|
|
667
|
+
if item.kind != 'span':
|
|
668
|
+
continue
|
|
669
|
+
missing = item.parent_span_id is not None and (item.target_ref, item.window_index, item.trace_id, item.parent_span_id) not in identities
|
|
670
|
+
if item.parent_missing != missing:
|
|
671
|
+
raise ValueError('observability_span_parent_coverage_invalid')
|
|
672
|
+
if self.quality.status == 'zero_matches' and (self.data or not self.quality.completed):
|
|
673
|
+
raise ValueError('observability_zero_matches_not_proven')
|
|
674
|
+
if self.quality.status == 'zero_matches' and any((source.status != 'zero_matches' or not source.completed for source in self.sources)):
|
|
675
|
+
raise ValueError('observability_zero_matches_not_proven')
|
|
676
|
+
if self.quality.status == 'available' and (not self.quality.completed or any((not source.completed or source.status not in {'available', 'zero_matches'} for source in self.sources))):
|
|
677
|
+
raise ValueError('observability_complete_target_coverage_required')
|
|
678
|
+
if self.quality.status == 'available' and (not self.data):
|
|
679
|
+
raise ValueError('observability_empty_result_not_explained')
|
|
680
|
+
return self
|
|
681
|
+
|
|
682
|
+
class ObservabilityAttachmentSelection(ObservabilityModel):
|
|
683
|
+
request_id: UUID
|
|
684
|
+
item_refs: list[Identifier] = Field(default_factory=list, max_length=1000)
|
|
685
|
+
field_ids: list[Identifier] = Field(default_factory=list, max_length=50)
|
|
686
|
+
representation_version: Literal[1, 2] = 1
|
|
687
|
+
include_zero_matches: bool = False
|
|
688
|
+
|
|
689
|
+
@model_validator(mode='after')
|
|
690
|
+
def validate_selection(self) -> ObservabilityAttachmentSelection:
|
|
691
|
+
unique_values(self.item_refs)
|
|
692
|
+
unique_values(self.field_ids)
|
|
693
|
+
if self.include_zero_matches:
|
|
694
|
+
if self.item_refs:
|
|
695
|
+
raise ValueError('observability_zero_matches_selection_invalid')
|
|
696
|
+
elif not self.item_refs or not self.field_ids:
|
|
697
|
+
raise ValueError('observability_attachment_selection_empty')
|
|
698
|
+
return self
|
|
699
|
+
|
|
700
|
+
class ObservabilityAttachmentPayload(ObservabilitySnapshotPayload):
|
|
701
|
+
attachment_id: UUID
|
|
702
|
+
parent_payload_sha256: Sha256
|
|
703
|
+
selection: ObservabilityAttachmentSelection
|
|
704
|
+
redaction_version: int = Field(ge=1, strict=True)
|
|
705
|
+
|
|
706
|
+
@model_validator(mode='after')
|
|
707
|
+
def validate_selected_content(self) -> ObservabilityAttachmentPayload:
|
|
708
|
+
if self.selection.field_ids != self.parameters.field_ids:
|
|
709
|
+
raise ValueError('observability_attachment_selection_mismatch')
|
|
710
|
+
if self.selection.representation_version != 2 and any((column.redaction != 'none' for column in self.columns)):
|
|
711
|
+
raise ValueError('observability_representation_version_required')
|
|
712
|
+
selected = set(self.selection.item_refs)
|
|
713
|
+
represented = set()
|
|
714
|
+
for item in self.data:
|
|
715
|
+
reference = item.evidence_id if item.kind == 'metric' else item.item_id
|
|
716
|
+
if reference in selected:
|
|
717
|
+
represented.add(reference)
|
|
718
|
+
elif item.kind == 'redacted' and item.bucket_refs:
|
|
719
|
+
references = set(item.bucket_refs)
|
|
720
|
+
if references - selected:
|
|
721
|
+
raise ValueError('observability_attachment_selection_mismatch')
|
|
722
|
+
represented.update(references)
|
|
723
|
+
elif item.kind in {'series', 'distribution'}:
|
|
724
|
+
references = {bucket.item_id for bucket in item.buckets}
|
|
725
|
+
if references - selected:
|
|
726
|
+
raise ValueError('observability_attachment_selection_mismatch')
|
|
727
|
+
represented.update(references)
|
|
728
|
+
else:
|
|
729
|
+
raise ValueError('observability_attachment_selection_mismatch')
|
|
730
|
+
if represented != selected:
|
|
731
|
+
raise ValueError('observability_attachment_selection_mismatch')
|
|
732
|
+
if self.selection.include_zero_matches != (self.quality.status == 'zero_matches'):
|
|
733
|
+
raise ValueError('observability_attachment_zero_matches_mismatch')
|
|
734
|
+
return self
|
|
735
|
+
|
|
736
|
+
class ObservabilityEvidenceReference(ObservabilityModel):
|
|
737
|
+
issuer: str = Field(min_length=1, max_length=2048)
|
|
738
|
+
workspace_id: UUID
|
|
739
|
+
project_id: UUID
|
|
740
|
+
user_id: UUID
|
|
741
|
+
snapshot_id: UUID
|
|
742
|
+
attachment_id: UUID
|
|
743
|
+
integration_type: Provider
|
|
744
|
+
evidence_type: Literal['mpulse_data', 'newrelic_data']
|
|
745
|
+
payload_sha256: Sha256
|
|
746
|
+
parent_payload_sha256: Sha256
|
|
747
|
+
_validate_issuer = field_validator('issuer')(validate_issuer)
|
|
748
|
+
|
|
749
|
+
@model_validator(mode='after')
|
|
750
|
+
def validate_provider(self) -> ObservabilityEvidenceReference:
|
|
751
|
+
if self.evidence_type != f'{self.integration_type}_data':
|
|
752
|
+
raise ValueError('observability_provider_mismatch')
|
|
753
|
+
return self
|
|
754
|
+
|
|
755
|
+
class ObservabilityAttachmentMetadata(ObservabilityEvidenceReference):
|
|
756
|
+
representation_version: Literal[1, 2] = 1
|
|
757
|
+
schema_version: Literal[1] = 1
|
|
758
|
+
byte_count: int = Field(ge=1, le=32768, strict=True)
|
|
759
|
+
expires_at: AwareDatetime
|
|
760
|
+
policy_version: int = Field(ge=1, strict=True)
|
|
761
|
+
current_policy_version: int = Field(ge=1, strict=True)
|
|
762
|
+
redaction_version: int = Field(ge=1, strict=True)
|
|
763
|
+
allow_agent_transfer: bool
|
|
764
|
+
allowed_model_providers: list[Identifier] = Field(default_factory=list, max_length=32)
|
|
765
|
+
consent_required: bool = False
|
|
766
|
+
|
|
767
|
+
@model_serializer(mode='wrap')
|
|
768
|
+
def serialize_metadata(self, handler):
|
|
769
|
+
value = handler(self)
|
|
770
|
+
if self.representation_version == 1:
|
|
771
|
+
value.pop('representation_version', None)
|
|
772
|
+
if not self.consent_required:
|
|
773
|
+
value.pop('consent_required', None)
|
|
774
|
+
return value
|
|
775
|
+
MAX_SNAPSHOT_BYTES = 10 * 1024 * 1024
|
|
776
|
+
|
|
777
|
+
def canonical_bytes(value: object) -> bytes:
|
|
778
|
+
try:
|
|
779
|
+
return json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=False, allow_nan=False).encode('utf-8')
|
|
780
|
+
except (ValueError, TypeError, UnicodeError, RecursionError):
|
|
781
|
+
raise ValueError('observability_invalid_json') from None
|
|
782
|
+
|
|
783
|
+
def _unique_object(pairs):
|
|
784
|
+
result = {}
|
|
785
|
+
for key, value in pairs:
|
|
786
|
+
if key in result:
|
|
787
|
+
raise ValueError('observability_invalid_json')
|
|
788
|
+
result[key] = value
|
|
789
|
+
return result
|
|
790
|
+
|
|
791
|
+
def _invalid_constant(_value):
|
|
792
|
+
raise ValueError('observability_invalid_json')
|
|
793
|
+
|
|
794
|
+
def strict_json_loads(payload: bytes, *, max_bytes: int=MAX_SNAPSHOT_BYTES):
|
|
795
|
+
if not isinstance(payload, bytes) or not payload or len(payload) > max_bytes:
|
|
796
|
+
raise ValueError('observability_payload_size_invalid')
|
|
797
|
+
try:
|
|
798
|
+
value = json.loads(payload.decode('utf-8'), object_pairs_hook=_unique_object, parse_constant=_invalid_constant)
|
|
799
|
+
canonical_bytes(value)
|
|
800
|
+
return value
|
|
801
|
+
except (ValueError, TypeError, UnicodeError, RecursionError):
|
|
802
|
+
raise ValueError('observability_invalid_json') from None
|
|
803
|
+
ObservabilityModel.model_rebuild()
|
|
804
|
+
ObservabilityWindow.model_rebuild()
|
|
805
|
+
ObservabilitySelection.model_rebuild()
|
|
806
|
+
ObservabilityCondition.model_rebuild()
|
|
807
|
+
ObservabilityFilterGroup.model_rebuild()
|
|
808
|
+
ObservabilityOrder.model_rebuild()
|
|
809
|
+
ObservabilityQuery.model_rebuild()
|
|
810
|
+
ObservabilityOrigin.model_rebuild()
|
|
811
|
+
ObservabilityCoverageWindow.model_rebuild()
|
|
812
|
+
ObservabilitySource.model_rebuild()
|
|
813
|
+
ObservabilitySafeCondition.model_rebuild()
|
|
814
|
+
ObservabilitySafeFilterGroup.model_rebuild()
|
|
815
|
+
ObservabilityFrozenScope.model_rebuild()
|
|
816
|
+
ObservabilitySafeParameters.model_rebuild()
|
|
817
|
+
ObservabilityColumn.model_rebuild()
|
|
818
|
+
ObservabilityMetricQuality.model_rebuild()
|
|
819
|
+
ObservabilityMetricValue.model_rebuild()
|
|
820
|
+
ObservabilityRow.model_rebuild()
|
|
821
|
+
ObservabilityTimeBucket.model_rebuild()
|
|
822
|
+
ObservabilitySeries.model_rebuild()
|
|
823
|
+
ObservabilityHistory.model_rebuild()
|
|
824
|
+
ObservabilityDistributionBucket.model_rebuild()
|
|
825
|
+
ObservabilityDistribution.model_rebuild()
|
|
826
|
+
ObservabilitySpan.model_rebuild()
|
|
827
|
+
ObservabilityRedactedItem.model_rebuild()
|
|
828
|
+
ObservabilitySnapshotQuality.model_rebuild()
|
|
829
|
+
ObservabilitySnapshotPayload.model_rebuild()
|
|
830
|
+
ObservabilityAttachmentSelection.model_rebuild()
|
|
831
|
+
ObservabilityAttachmentPayload.model_rebuild()
|
|
832
|
+
ObservabilityEvidenceReference.model_rebuild()
|
|
833
|
+
ObservabilityAttachmentMetadata.model_rebuild()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
|
|
7
|
+
EVIDENCE_PROTOCOL_VERSION = 1
|
|
8
|
+
PAYLOAD_MAX_BYTES = 32 * 1024
|
|
9
|
+
TURN_MAX_BYTES = 64 * 1024
|
|
10
|
+
ORIGIN_FIELDS = frozenset({"issuer", "workspace_id", "project_id", "user_id"})
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EvidenceError(ValueError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@lru_cache(maxsize=1)
|
|
18
|
+
def _contract():
|
|
19
|
+
try:
|
|
20
|
+
if __package__ == "app":
|
|
21
|
+
module = import_module("app.schemas.observability_evidence")
|
|
22
|
+
serialization = import_module("app.observability_lifecycle")
|
|
23
|
+
else:
|
|
24
|
+
module = import_module(
|
|
25
|
+
f"{__package__}.observability_contract" if __package__ else "observability_contract"
|
|
26
|
+
)
|
|
27
|
+
serialization = module
|
|
28
|
+
except ImportError:
|
|
29
|
+
raise EvidenceError("observability_evidence_protocol_unsupported") from None
|
|
30
|
+
return (
|
|
31
|
+
module.ObservabilityEvidenceReference, module.ObservabilityAttachmentMetadata,
|
|
32
|
+
module.ObservabilityAttachmentPayload,
|
|
33
|
+
serialization.canonical_bytes, serialization.strict_json_loads,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def validate_reference(reference) -> dict:
|
|
38
|
+
reference_model, _metadata, _payload, canonical, _loads = _contract()
|
|
39
|
+
try:
|
|
40
|
+
parsed = reference_model.model_validate_json(canonical(reference), strict=True)
|
|
41
|
+
normalized = parsed.model_dump(mode="json")
|
|
42
|
+
if normalized != reference:
|
|
43
|
+
raise ValueError
|
|
44
|
+
except (TypeError, ValueError):
|
|
45
|
+
raise EvidenceError("observability_evidence_invalid_reference") from None
|
|
46
|
+
return normalized
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_payload(
|
|
50
|
+
reference, metadata, payload, *, expected_origin, model_provider,
|
|
51
|
+
trusted_transport=False, now=None,
|
|
52
|
+
) -> dict:
|
|
53
|
+
import hashlib
|
|
54
|
+
|
|
55
|
+
if trusted_transport is not True:
|
|
56
|
+
raise EvidenceError("observability_evidence_trusted_transport_required")
|
|
57
|
+
reference = validate_reference(reference)
|
|
58
|
+
if (
|
|
59
|
+
not isinstance(expected_origin, dict) or set(expected_origin) != ORIGIN_FIELDS
|
|
60
|
+
or any(reference[key] != expected_origin[key] for key in ORIGIN_FIELDS)
|
|
61
|
+
):
|
|
62
|
+
raise EvidenceError("observability_evidence_identity_mismatch")
|
|
63
|
+
if not isinstance(payload, str):
|
|
64
|
+
raise EvidenceError("observability_evidence_invalid_payload")
|
|
65
|
+
try:
|
|
66
|
+
encoded = payload.encode("utf-8")
|
|
67
|
+
except UnicodeError:
|
|
68
|
+
raise EvidenceError("observability_evidence_invalid_payload") from None
|
|
69
|
+
if not 1 <= len(encoded) <= PAYLOAD_MAX_BYTES:
|
|
70
|
+
raise EvidenceError("observability_evidence_size_limit")
|
|
71
|
+
if hashlib.sha256(encoded).hexdigest() != reference["payload_sha256"]:
|
|
72
|
+
raise EvidenceError("observability_evidence_integrity_failed")
|
|
73
|
+
_reference, metadata_model, payload_model, canonical, loads = _contract()
|
|
74
|
+
try:
|
|
75
|
+
current = metadata_model.model_validate_json(canonical(metadata), strict=True)
|
|
76
|
+
parsed = loads(encoded)
|
|
77
|
+
body = payload_model.model_validate_json(canonical(parsed), strict=True)
|
|
78
|
+
if canonical(body.model_dump(mode="json")) != encoded:
|
|
79
|
+
raise ValueError
|
|
80
|
+
except (TypeError, ValueError):
|
|
81
|
+
raise EvidenceError("observability_evidence_invalid_payload") from None
|
|
82
|
+
current_fields = current.model_dump(mode="json")
|
|
83
|
+
if any(current_fields[key] != value for key, value in reference.items()):
|
|
84
|
+
raise EvidenceError("observability_evidence_identity_mismatch")
|
|
85
|
+
if (
|
|
86
|
+
str(body.snapshot_id) != reference["snapshot_id"]
|
|
87
|
+
or str(body.attachment_id) != reference["attachment_id"]
|
|
88
|
+
or body.integration_type != reference["integration_type"]
|
|
89
|
+
or body.evidence_type != reference["evidence_type"]
|
|
90
|
+
or body.parent_payload_sha256 != reference["parent_payload_sha256"]
|
|
91
|
+
or body.origin.issuer != reference["issuer"]
|
|
92
|
+
or str(body.origin.workspace_id) != reference["workspace_id"]
|
|
93
|
+
or str(body.origin.project_id) != reference["project_id"]
|
|
94
|
+
or str(body.origin.created_by_user_id) != reference["user_id"]
|
|
95
|
+
):
|
|
96
|
+
raise EvidenceError("observability_evidence_identity_mismatch")
|
|
97
|
+
if (
|
|
98
|
+
current.byte_count != len(encoded)
|
|
99
|
+
or current.representation_version != body.selection.representation_version
|
|
100
|
+
or current.redaction_version != body.redaction_version
|
|
101
|
+
or current.policy_version != body.policy_version
|
|
102
|
+
or current.current_policy_version < current.policy_version
|
|
103
|
+
):
|
|
104
|
+
raise EvidenceError("observability_evidence_integrity_failed")
|
|
105
|
+
timestamp = now or datetime.now(timezone.utc)
|
|
106
|
+
if current.expires_at <= timestamp or body.expires_at < current.expires_at:
|
|
107
|
+
raise EvidenceError("observability_evidence_expired")
|
|
108
|
+
if (
|
|
109
|
+
current.allow_agent_transfer is not True
|
|
110
|
+
or model_provider not in current.allowed_model_providers
|
|
111
|
+
):
|
|
112
|
+
raise EvidenceError("observability_evidence_policy_denied")
|
|
113
|
+
return {
|
|
114
|
+
"reference": reference, "payload_json": payload, "byte_count": len(encoded),
|
|
115
|
+
"policy_version": current.current_policy_version,
|
|
116
|
+
"redaction_version": current.redaction_version,
|
|
117
|
+
"representation_version": current.representation_version,
|
|
118
|
+
"expires_at": current.expires_at.isoformat(),
|
|
119
|
+
"model_provider": model_provider, "data_family": body.data_family,
|
|
120
|
+
"result_type": body.result_type, "field_ids": body.parameters.field_ids,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def validate_turn_budget(payloads: list[bytes]) -> int:
|
|
125
|
+
total = 0
|
|
126
|
+
for payload in payloads:
|
|
127
|
+
if not isinstance(payload, bytes) or not 1 <= len(payload) <= PAYLOAD_MAX_BYTES:
|
|
128
|
+
raise EvidenceError("observability_evidence_size_limit")
|
|
129
|
+
total += len(payload)
|
|
130
|
+
if total > TURN_MAX_BYTES:
|
|
131
|
+
raise EvidenceError("observability_turn_budget_exceeded")
|
|
132
|
+
return total
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: forgexa-cli
|
|
3
|
-
Version: 1.78.
|
|
3
|
+
Version: 1.78.2
|
|
4
4
|
Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
|
|
5
5
|
Author-email: Jason Sun <dev.winds@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,6 +26,8 @@ Requires-Python: >=3.9
|
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
Requires-Dist: httpx>=0.24
|
|
28
28
|
Requires-Dist: socksio>=1.0.0
|
|
29
|
+
Requires-Dist: pydantic<3,>=2.10
|
|
30
|
+
Requires-Dist: pycountry>=24.6
|
|
29
31
|
Provides-Extra: daemon
|
|
30
32
|
Requires-Dist: httpx>=0.24; extra == "daemon"
|
|
31
33
|
Requires-Dist: socksio>=1.0.0; extra == "daemon"
|
|
@@ -9,6 +9,8 @@ forgexa_cli/aws_evidence.py
|
|
|
9
9
|
forgexa_cli/aws_logs_evidence.py
|
|
10
10
|
forgexa_cli/daemon.py
|
|
11
11
|
forgexa_cli/main.py
|
|
12
|
+
forgexa_cli/observability_contract.py
|
|
13
|
+
forgexa_cli/observability_evidence.py
|
|
12
14
|
forgexa_cli/py.typed
|
|
13
15
|
forgexa_cli/runtime_evidence.py
|
|
14
16
|
forgexa_cli/zcode_protocol.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "forgexa-cli"
|
|
3
|
-
version = "1.78.
|
|
3
|
+
version = "1.78.2"
|
|
4
4
|
description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
|
|
5
5
|
requires-python = ">=3.9"
|
|
6
6
|
license = "MIT"
|
|
@@ -28,7 +28,7 @@ classifiers = [
|
|
|
28
28
|
# installs it automatically — no fragile runtime auto-install needed.
|
|
29
29
|
# socksio is required for SOCKS proxy support (httpx picks up ALL_PROXY/SOCKS_PROXY env vars
|
|
30
30
|
# automatically, and raises ImportError if socksio is missing).
|
|
31
|
-
dependencies = ["httpx>=0.24", "socksio>=1.0.0"]
|
|
31
|
+
dependencies = ["httpx>=0.24", "socksio>=1.0.0", "pydantic>=2.10,<3", "pycountry>=24.6"]
|
|
32
32
|
|
|
33
33
|
[project.optional-dependencies]
|
|
34
34
|
# Keep [daemon] extra for backward compatibility / explicit opt-in.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|