diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index f46a8c0a72..e78dc8f53a 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -2284,674 +2284,6 @@ } } ], - "./monitoring/mock_uss/tracer/context.py": [ - { - "code": "reportArgumentType", - "range": { - "startColumn": 33, - "endColumn": 44, - "lineCount": 1 - } - }, - { - "code": "reportAssignmentType", - "range": { - "startColumn": 24, - "endColumn": 44, - "lineCount": 1 - } - } - ], - "./monitoring/mock_uss/tracer/diff.py": [ - { - "code": "reportArgumentType", - "range": { - "startColumn": 49, - "endColumn": 58, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 60, - "endColumn": 69, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 49, - "endColumn": 58, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 60, - "endColumn": 69, - "lineCount": 1 - } - } - ], - "./monitoring/mock_uss/tracer/kml.py": [ - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 17, - "endColumn": 22, - "lineCount": 1 - } - }, - { - "code": "reportOperatorIssue", - "range": { - "startColumn": 29, - "endColumn": 65, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 9, - "endColumn": 42, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 1, - "endColumn": 59, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 12, - "endColumn": 34, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 30, - "endColumn": 55, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 20, - "endColumn": 27, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 1, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 16, - "endColumn": 33, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 16, - "endColumn": 40, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 35, - "endColumn": 43, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 35, - "endColumn": 43, - "lineCount": 1 - } - }, - { - "code": "reportInvalidTypeForm", - "range": { - "startColumn": 31, - "endColumn": 34, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 59, - "endColumn": 67, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 34, - "endColumn": 53, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 61, - "endColumn": 78, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 30, - "endColumn": 38, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 32, - "endColumn": 40, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 34, - "endColumn": 42, - "lineCount": 1 - } - } - ], - "./monitoring/mock_uss/tracer/observation_area_operations.py": [ - { - "code": "reportArgumentType", - "range": { - "startColumn": 12, - "endColumn": 38, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 56, - "endColumn": 82, - "lineCount": 1 - } - } - ], - "./monitoring/mock_uss/tracer/observation_areas.py": [ - { - "code": "reportReturnType", - "range": { - "startColumn": 15, - "endColumn": 83, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 15, - "endColumn": 83, - "lineCount": 1 - } - } - ], - "./monitoring/mock_uss/tracer/routes/observation_areas.py": [ - { - "code": "reportArgumentType", - "range": { - "startColumn": 1, - "endColumn": 59, - "lineCount": 1 - } - }, - { - "code": "reportOptionalIterable", - "range": { - "startColumn": 25, - "endColumn": 69, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 51, - "endColumn": 78, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 49, - "endColumn": 74, - "lineCount": 1 - } - } - ], - "./monitoring/mock_uss/tracer/routes/scd.py": [ - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 18, - "endColumn": 21, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 16, - "endColumn": 19, - "lineCount": 1 - } - }, - { - "code": "reportOptionalSubscript", - "range": { - "startColumn": 17, - "endColumn": 21, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 18, - "endColumn": 21, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 16, - "endColumn": 19, - "lineCount": 1 - } - }, - { - "code": "reportOptionalSubscript", - "range": { - "startColumn": 25, - "endColumn": 29, - "lineCount": 1 - } - } - ], - "./monitoring/mock_uss/tracer/routes/ui.py": [ - { - "code": "reportArgumentType", - "range": { - "startColumn": 1, - "endColumn": 46, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 1, - "endColumn": 35, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 44, - "endColumn": 60, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 1, - "endColumn": 36, - "lineCount": 1 - } - }, - { - "code": "reportCallIssue", - "range": { - "startColumn": 8, - "endColumn": 27, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 1, - "endColumn": 34, - "lineCount": 1 - } - }, - { - "code": "reportCallIssue", - "range": { - "startColumn": 8, - "endColumn": 27, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 1, - "endColumn": 43, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 8, - "endColumn": 69, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 8, - "endColumn": 56, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 25, - "endColumn": 78, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 1, - "endColumn": 62, - "lineCount": 1 - } - } - ], - "./monitoring/mock_uss/tracer/subscriptions.py": [ - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 35, - "endColumn": 43, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 31, - "endColumn": 39, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 24, - "endColumn": 32, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 22, - "endColumn": 30, - "lineCount": 1 - } - } - ], - "./monitoring/mock_uss/tracer/tracer_poll.py": [ - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 47, - "endColumn": 56, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 69, - "endColumn": 81, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 19, - "endColumn": 22, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 29, - "endColumn": 37, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 27, - "endColumn": 35, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 19, - "endColumn": 30, - "lineCount": 1 - } - }, - { - "code": "reportInvalidTypeForm", - "range": { - "startColumn": 50, - "endColumn": 9, - "lineCount": 3 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 23, - "endColumn": 26, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 19, - "endColumn": 22, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 29, - "endColumn": 37, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 27, - "endColumn": 35, - "lineCount": 1 - } - }, - { - "code": "reportInvalidTypeForm", - "range": { - "startColumn": 42, - "endColumn": 76, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 58, - "endColumn": 61, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 19, - "endColumn": 22, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 29, - "endColumn": 37, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 27, - "endColumn": 35, - "lineCount": 1 - } - } - ], - "./monitoring/mock_uss/tracer/tracerlog.py": [ - { - "code": "reportArgumentType", - "range": { - "startColumn": 80, - "endColumn": 84, - "lineCount": 1 - } - } - ], - "./monitoring/mock_uss/ui/auth.py": [ - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 51, - "endColumn": 56, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 27, - "endColumn": 49, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 44, - "endColumn": 65, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 13, - "endColumn": 9, - "lineCount": 4 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 17, - "endColumn": 44, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 38, - "endColumn": 47, - "lineCount": 1 - } - } - ], - "./monitoring/mock_uss/uspace/flight_auth.py": [ - { - "code": "reportArgumentType", - "range": { - "startColumn": 8, - "endColumn": 47, - "lineCount": 1 - } - } - ], - "./monitoring/mock_uss/versioning/routes.py": [ - { - "code": "reportReturnType", - "range": { - "startColumn": 11, - "endColumn": 5, - "lineCount": 6 - } - } - ], "./monitoring/monitorlib/auth.py": [ { "code": "reportArgumentType", @@ -4797,22 +4129,6 @@ "lineCount": 1 } }, - { - "code": "reportGeneralTypeIssues", - "range": { - "startColumn": 28, - "endColumn": 36, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 19, - "endColumn": 53, - "lineCount": 1 - } - }, { "code": "reportOptionalMemberAccess", "range": { @@ -4829,14 +4145,6 @@ "lineCount": 1 } }, - { - "code": "reportReturnType", - "range": { - "startColumn": 19, - "endColumn": 60, - "lineCount": 1 - } - }, { "code": "reportOptionalMemberAccess", "range": { @@ -4853,22 +4161,6 @@ "lineCount": 1 } }, - { - "code": "reportGeneralTypeIssues", - "range": { - "startColumn": 26, - "endColumn": 34, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 19, - "endColumn": 51, - "lineCount": 1 - } - }, { "code": "reportOptionalMemberAccess", "range": { @@ -4885,14 +4177,6 @@ "lineCount": 1 } }, - { - "code": "reportReturnType", - "range": { - "startColumn": 19, - "endColumn": 58, - "lineCount": 1 - } - }, { "code": "reportOptionalMemberAccess", "range": { @@ -7011,14 +6295,6 @@ } ], "./monitoring/uss_qualifier/reports/sequence_view/kml.py": [ - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 17, - "endColumn": 22, - "lineCount": 1 - } - }, { "code": "reportReturnType", "range": { @@ -7043,14 +6319,6 @@ "lineCount": 1 } }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 85, - "endColumn": 90, - "lineCount": 1 - } - }, { "code": "reportArgumentType", "range": { @@ -9296,18 +8564,18 @@ } }, { - "code": "reportOperatorIssue", + "code": "reportArgumentType", "range": { - "startColumn": 31, - "endColumn": 69, + "startColumn": 22, + "endColumn": 32, "lineCount": 1 } }, { - "code": "reportOperatorIssue", + "code": "reportArgumentType", "range": { - "startColumn": 29, - "endColumn": 65, + "startColumn": 22, + "endColumn": 32, "lineCount": 1 } }, @@ -10055,14 +9323,6 @@ "lineCount": 1 } }, - { - "code": "reportOperatorIssue", - "range": { - "startColumn": 19, - "endColumn": 17, - "lineCount": 4 - } - }, { "code": "reportOptionalMemberAccess", "range": { @@ -10103,14 +9363,6 @@ "lineCount": 1 } }, - { - "code": "reportOperatorIssue", - "range": { - "startColumn": 19, - "endColumn": 87, - "lineCount": 1 - } - }, { "code": "reportOptionalMemberAccess", "range": { @@ -23148,16 +22400,6 @@ } } ], - "./monitoring/uss_qualifier/test_data/make_flight_intent_kml.py": [ - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 17, - "endColumn": 22, - "lineCount": 1 - } - } - ], "./schemas/manage_type_schemas.py": [ { "code": "reportArgumentType", diff --git a/monitoring/mock_uss/tracer/context.py b/monitoring/mock_uss/tracer/context.py index dfe19e08df..24b0e66942 100644 --- a/monitoring/mock_uss/tracer/context.py +++ b/monitoring/mock_uss/tracer/context.py @@ -10,7 +10,7 @@ KEY_TRACER_OUTPUT_FOLDER, ) from monitoring.mock_uss.tracer.observation_areas import ObservationAreaID -from monitoring.mock_uss.tracer.tracerlog import Logger +from monitoring.mock_uss.tracer.tracerlog import DummyLogger, Logger from monitoring.monitorlib import infrastructure from monitoring.monitorlib.auth import make_auth_adapter from monitoring.monitorlib.fetch import scd @@ -23,7 +23,7 @@ scd_cache: dict[ObservationAreaID, dict[str, scd.FetchedEntity]] = {} -def _get_tracer_logger() -> Logger | None: +def _get_tracer_logger() -> Logger: kml_server = webapp.config[KEY_TRACER_KML_SERVER] kml_folder = webapp.config[KEY_TRACER_KML_FOLDER] output_folder = webapp.config[KEY_TRACER_OUTPUT_FOLDER] @@ -36,7 +36,7 @@ def _get_tracer_logger() -> Logger | None: if kml_server else None ) - return Logger(output_folder, kml_session) if output_folder else None + return Logger(output_folder, kml_session) if output_folder else DummyLogger() tracer_logger: Logger = _get_tracer_logger() @@ -57,14 +57,15 @@ def resolve_auth_spec(requested_auth_spec: AuthSpec | None) -> AuthSpec: return requested_auth_spec -def resolve_rid_dss_base_url(dss_base_url: str, rid_version: RIDVersion) -> str: +def resolve_rid_dss_base_url(dss_base_url: str | None, rid_version: RIDVersion) -> str: if not dss_base_url: - if KEY_DSS_URL not in webapp.config or not webapp.config[KEY_DSS_URL]: + dss_base_url = webapp.config.get(KEY_DSS_URL) + + if not dss_base_url: raise ValueError( "DSS base URL was not specified explicitly nor in mock_uss_configuration" ) - else: - dss_base_url = webapp.config[KEY_DSS_URL] + if rid_version == RIDVersion.f3411_19: return dss_base_url elif rid_version == RIDVersion.f3411_22a: @@ -75,14 +76,15 @@ def resolve_rid_dss_base_url(dss_base_url: str, rid_version: RIDVersion) -> str: ) -def resolve_scd_dss_base_url(dss_base_url: str) -> str: +def resolve_scd_dss_base_url(dss_base_url: str | None) -> str: if not dss_base_url: - if KEY_DSS_URL not in webapp.config or not webapp.config[KEY_DSS_URL]: + dss_base_url = webapp.config.get(KEY_DSS_URL) + + if not dss_base_url: raise ValueError( "DSS base URL was not specified explicitly nor in mock_uss_configuration" ) - else: - dss_base_url = webapp.config[KEY_DSS_URL] + return dss_base_url diff --git a/monitoring/mock_uss/tracer/diff.py b/monitoring/mock_uss/tracer/diff.py index 06502d61fb..5d3d7e5228 100644 --- a/monitoring/mock_uss/tracer/diff.py +++ b/monitoring/mock_uss/tracer/diff.py @@ -1,3 +1,5 @@ +from typing import cast + from monitoring.monitorlib import formatting from monitoring.monitorlib.fetch import rid, scd, summarize @@ -5,9 +7,9 @@ def isa_diff_text(a: rid.FetchedISAs | None, b: rid.FetchedISAs | None) -> str: """Create text to display to a real-time user describing a change in ISAs.""" a_summary = summarize.isas(a) if a else {} - a_summary = summarize.limit_long_arrays(a_summary, 6) + a_summary = cast(dict, summarize.limit_long_arrays(a_summary, 6)) b_summary = summarize.isas(b) if b else {} - b_summary = summarize.limit_long_arrays(b_summary, 6) + b_summary = cast(dict, summarize.limit_long_arrays(b_summary, 6)) if b is not None and b.success and a is not None and not a.success: a_summary = {} if a is not None and a.success and b is not None and not b.success: @@ -26,9 +28,9 @@ def entity_diff_text( if entity_type and "_" in entity_type: entity_type = entity_type[0 : entity_type.index("_")] a_summary = summarize.entities(a, entity_type) if a else {} - a_summary = summarize.limit_long_arrays(a_summary, 6) + a_summary = cast(dict, summarize.limit_long_arrays(a_summary, 6)) b_summary = summarize.entities(b, entity_type) if b else {} - b_summary = summarize.limit_long_arrays(b_summary, 6) + b_summary = cast(dict, summarize.limit_long_arrays(b_summary, 6)) if b is not None and b.success and a is not None and not a.success: a_summary = {} if a is not None and a.success and b is not None and not b.success: diff --git a/monitoring/mock_uss/tracer/kml.py b/monitoring/mock_uss/tracer/kml.py index 3d8873b524..5b7ebbdfa0 100644 --- a/monitoring/mock_uss/tracer/kml.py +++ b/monitoring/mock_uss/tracer/kml.py @@ -3,10 +3,11 @@ import glob import os import re +from abc import abstractmethod from dataclasses import dataclass from datetime import UTC, datetime, timedelta from enum import Enum -from typing import Protocol +from typing import Protocol, TypeVar import yaml from implicitdict import ImplicitDict @@ -40,7 +41,8 @@ def __enter__(self): self._start_time = datetime.now(UTC) def __exit__(self, exc_type, exc_val, exc_tb): - self.elapsed_time += datetime.now(UTC) - self._start_time + if self._start_time: + self.elapsed_time += datetime.now(UTC) - self._start_time class VolumeType(str, Enum): @@ -57,10 +59,14 @@ class HistoricalVolumesCollection: active_at: datetime -class HistoricalVolumesRenderer(Protocol): +TracerLogEntryType = TypeVar("TracerLogEntryType", bound=TracerLogEntry) + + +class HistoricalVolumesRenderer[TracerLogEntryType](Protocol): + @abstractmethod def __call__( self, - log_entry: TracerLogEntry, + log_entry: TracerLogEntryType, existing_volume_collections: list[HistoricalVolumesCollection], ) -> list[HistoricalVolumesCollection]: """Function that generates named collections of 4D volumes from a tracer log entry. @@ -71,6 +77,7 @@ def __call__( Returns: Collection of 4D volume collections. """ + raise NotImplementedError @dataclass @@ -126,6 +133,9 @@ def _historical_volumes_op_intent_notification( existing_volume_collections: list[HistoricalVolumesCollection], ) -> list[HistoricalVolumesCollection]: try: + if log_entry.request.json is None: + raise ValueError("No json data in log entry") + req = ImplicitDict.parse( log_entry.request.json, PutOperationalIntentDetailsParameters ) @@ -136,7 +146,7 @@ def _historical_volumes_op_intent_notification( return [] assert isinstance(req, PutOperationalIntentDetailsParameters) - claims = get_token_claims(log_entry.request.headers) + claims = get_token_claims(log_entry.request.headers or {}) manager = claims.get("sub", "[Unknown manager]") name = f"{manager} {req.operational_intent_id}" @@ -148,7 +158,7 @@ def _historical_volumes_op_intent_notification( else: version = "[deleted]" state = "Ended" - volumes = [] + volumes = Volume4DCollection() # See if this op intent version already has a volumes collection already_defined = False @@ -183,6 +193,9 @@ def _historical_volumes_op_intent_poll( # Add newly-polled operational intents for op_intent_id, query in log_entry.poll.uss_queries.items(): try: + if query.json_result is None: + raise ValueError("No json result in query") + resp = ImplicitDict.parse( query.json_result, GetOperationalIntentDetailsResponse ) @@ -222,6 +235,9 @@ def _historical_volumes_op_intent_poll( # Remove any existing operational intents that no longer exist as of this poll for cached_op_intent_id, cached_query in log_entry.poll.cached_uss_queries.items(): try: + if cached_query.json_result is None: + raise ValueError("No json result in query") + resp = ImplicitDict.parse( cached_query.json_result, GetOperationalIntentDetailsResponse ) @@ -274,26 +290,38 @@ class VolumesFolder: def truncate(self, latest_time: Time) -> None: to_remove = [] for v in self.volumes: - if v.volume.time_start.datetime > latest_time.datetime: + if ( + v.volume.time_start + and v.volume.time_start.datetime > latest_time.datetime + ): to_remove.append(v) - elif v.volume.time_end.datetime > latest_time.datetime: + elif ( + v.volume.time_end and v.volume.time_end.datetime > latest_time.datetime + ): v.volume.time_end = latest_time for v in to_remove: self.volumes.remove(v) for c in self.children: c.truncate(latest_time) - def to_kml_folder(self) -> kml.Folder: - def dt(t: Time) -> int: - return round((t.datetime - self.reference_time.datetime).total_seconds()) - + def to_kml_folder(self): if self.reference_time: description = f"Relative to {self.reference_time}" folder = kml.Folder(kml.name(self.name), kml.description(description)) else: folder = kml.Folder(kml.name(self.name)) + for v in self.volumes: - name = f"{v.name} {dt(v.volume.time_start)}s-{dt(v.volume.time_end)}s" + name = v.name + + if self.reference_time: + base_time = self.reference_time.datetime + + def dt(t: Time) -> int: + return round((t.datetime - base_time).total_seconds()) + + name = f"{name} {dt(v.volume.time_start) if v.volume.time_start else '?'}s-{dt(v.volume.time_end) if v.volume.time_end else '?'}s" + folder.append( make_placemark_from_volume(v.volume, name=name, style_url=v.style) ) @@ -408,13 +436,13 @@ def render_historical_kml(log_folder: str) -> str: version_folder.children.append(future_folder) for i, v in enumerate(hvc.volumes): - if v.time_end.datetime <= hvc.active_at: + if v.time_end and v.time_end.datetime <= hvc.active_at: # This volume ended before the collection was declared, so it never actually existed continue - if v.time_start.datetime < hvc.active_at: + if v.time_start and v.time_start.datetime < hvc.active_at: # Volume is declared in the past, but it's only visible starting now v.time_start = t_hvc - elif v.time_start.datetime > hvc.active_at: + elif v.time_start and v.time_start.datetime > hvc.active_at: # Add a "future" volume between when this volume was declared and its start time future_v = Volume4D(v) future_v.time_end = v.time_start diff --git a/monitoring/mock_uss/tracer/observation_areas.py b/monitoring/mock_uss/tracer/observation_areas.py index dc11b4da1a..217504fae2 100644 --- a/monitoring/mock_uss/tracer/observation_areas.py +++ b/monitoring/mock_uss/tracer/observation_areas.py @@ -67,7 +67,9 @@ class ObservationArea(ImplicitDict): @property def polls(self) -> bool: """Whether any of the observation activity involves periodic polling.""" - return (self.f3411 and self.f3411.poll) or (self.f3548 and self.f3548.poll) + return bool(self.f3411 and self.f3411.poll) or bool( + self.f3548 and self.f3548.poll + ) class F3411ObservationAreaRequest(ImplicitDict): @@ -134,7 +136,9 @@ class ObservationAreaRequest(ImplicitDict): @property def polls(self) -> bool: """Whether any of the observation activity requested involves periodic polling.""" - return (self.f3411 and self.f3411.poll) or (self.f3548 and self.f3548.poll) + return bool(self.f3411 and self.f3411.poll) or bool( + self.f3548 and self.f3548.poll + ) class ListObservationAreasResponse(ImplicitDict): diff --git a/monitoring/mock_uss/tracer/routes/observation_areas.py b/monitoring/mock_uss/tracer/routes/observation_areas.py index de7d03fa88..b05ec1b444 100644 --- a/monitoring/mock_uss/tracer/routes/observation_areas.py +++ b/monitoring/mock_uss/tracer/routes/observation_areas.py @@ -36,7 +36,7 @@ @webapp.route("/tracer/observation_areas", methods=["GET"]) -@ui_auth.login_required +@ui_auth.login_required() def tracer_list_observation_areas() -> flask.Response: with db as tx: result = ListObservationAreasResponse( @@ -131,7 +131,7 @@ def tracer_import_observation_areas() -> tuple[str, int] | flask.Response: elif request.area.volume.outline_polygon: points = [ s2sphere.LatLng.from_degrees(p.lat, p.lng) - for p in request.area.volume.outline_polygon.vertices + for p in request.area.volume.outline_polygon.vertices or [] ] else: raise NotImplementedError( diff --git a/monitoring/mock_uss/tracer/routes/scd.py b/monitoring/mock_uss/tracer/routes/scd.py index 02e419888e..eb20c8cc13 100644 --- a/monitoring/mock_uss/tracer/routes/scd.py +++ b/monitoring/mock_uss/tracer/routes/scd.py @@ -39,6 +39,10 @@ def tracer_scd_v21_operation_notification(observation_area_id: str) -> tuple[str label = colored("Operation", "blue") try: json = flask.request.json + + if json is None: + raise ValueError("No json in request") + id = json.get("operational_intent_id", "") if json.get("operational_intent"): op = json["operational_intent"] @@ -94,6 +98,10 @@ def tracer_scd_v21_constraint_notification(observation_area_id: str) -> tuple[st label = colored("Constraint", "magenta") try: json = flask.request.json + + if json is None: + raise ValueError("No json in request") + id = json.get("constraint_id", "") if json.get("constraint"): constraint = json["constraint"] diff --git a/monitoring/mock_uss/tracer/routes/ui.py b/monitoring/mock_uss/tracer/routes/ui.py index 426549c9e1..d2706d484f 100644 --- a/monitoring/mock_uss/tracer/routes/ui.py +++ b/monitoring/mock_uss/tracer/routes/ui.py @@ -3,6 +3,7 @@ import io import os import zipfile +from typing import cast import arrow import flask @@ -23,7 +24,7 @@ @webapp.route("/tracer/logs", methods=["GET"]) -@ui_auth.login_required +@ui_auth.login_required() def tracer_list_logs(): logger.debug(f"Handling tracer_list_logs from {os.getpid()}") logs = [ @@ -109,7 +110,7 @@ def _redact_and_augment_log(obj): @webapp.route("/tracer/logs/") -@ui_auth.login_required +@ui_auth.login_required() def tracer_logs(log): logger.debug(f"Handling tracer_logs from {os.getpid()}") logfile = os.path.join(context.tracer_logger.log_path, log) @@ -122,7 +123,7 @@ def tracer_logs(log): else: obj = {"entries": objs} - object_type_name = obj.get("object_type", None) + object_type_name = cast(str | None, obj.get("object_type", None)) object_type = TracerLogEntry.entry_type(object_type_name) if object_type: parsed: TracerLogEntry = ImplicitDict.parse(obj, object_type) @@ -137,7 +138,7 @@ def tracer_logs(log): @webapp.route("/tracer/kml/now.kml") -@ui_auth.login_required +@ui_auth.login_required() def tracer_kml_now(): logger.debug(f"Handling tracer_kml_now from {os.getpid()}") all_kmls = glob.glob(os.path.join(context.tracer_logger.log_path, "kml", "*.kml")) @@ -147,13 +148,13 @@ def tracer_kml_now(): return flask.send_file( latest_kml, mimetype="application/vnd.google-earth.kml+xml", - attachment_filename="now.kml", + download_name="now.kml", as_attachment=True, ) @webapp.route("/tracer/kml/") -@ui_auth.login_required +@ui_auth.login_required() def tracer_kmls(kml): logger.debug(f"Handling tracer_kmls from {os.getpid()}") kmlfile = os.path.join(context.tracer_logger.log_path, "kml", kml) @@ -162,13 +163,13 @@ def tracer_kmls(kml): return flask.send_file( kmlfile, mimetype="application/vnd.google-earth.kml+xml", - attachment_filename=kml, + download_name=kml, as_attachment=True, ) @webapp.route("/tracer/kml/historical.kml") -@ui_auth.login_required +@ui_auth.login_required() def tracer_kml_historical(): kml_name = f"historical_{datetime.datetime.now(datetime.UTC).isoformat().split('.')[0]}.kml" return flask.Response( @@ -227,18 +228,21 @@ def tracer_rid_request_poll(observation_area_id: str): area = _get_validated_obs_area(observation_area_id) if not area.f3411: flask.abort(400, "Specified observation area is not observing F3411 remote ID") + raise RuntimeError("An exception should have been raised.") if not area.area.volume.outline_polygon and not area.area.volume.outline_circle: flask.abort( 400, "Specified observation area does not define its spatial outline" ) + raise RuntimeError("An exception should have been raised.") + rid_client = context.get_client(area.f3411.auth_spec, area.f3411.dss_base_url) flights_result = rid.all_flights( geo.make_latlng_rect(area.area.volume), - flask.request.form.get("include_recent_positions", type=bool), - flask.request.form.get("get_details", type=bool), + flask.request.form.get("include_recent_positions", False, type=bool), + flask.request.form.get("get_details", False, type=bool), area.f3411.rid_version, rid_client, - enhanced_details=flask.request.form.get("enhanced_details", type=bool), + enhanced_details=flask.request.form.get("enhanced_details", False, type=bool), ) log_name = context.tracer_logger.log_new( PollFlights( @@ -251,7 +255,7 @@ def tracer_rid_request_poll(observation_area_id: str): @webapp.route("/tracer/observation_areas/ui", methods=["GET"]) -@ui_auth.login_required +@ui_auth.login_required() def tracer_observation_areas_ui(): return flask.render_template( "tracer/observation_areas_ui.html", diff --git a/monitoring/mock_uss/tracer/subscriptions.py b/monitoring/mock_uss/tracer/subscriptions.py index a13debfc64..df9722b0a6 100644 --- a/monitoring/mock_uss/tracer/subscriptions.py +++ b/monitoring/mock_uss/tracer/subscriptions.py @@ -54,8 +54,8 @@ def subscribe_rid( area_vertices=vertices, alt_lo=area.volume.altitude_lower_wgs84_m(0), alt_hi=area.volume.altitude_upper_wgs84_m(3048), - start_time=area.time_start.datetime, - end_time=area.time_end.datetime, + start_time=area.time_start.datetime if area.time_start else None, + end_time=area.time_end.datetime if area.time_end else None, uss_base_url=uss_base_url, subscription_id=subscription_id, rid_version=rid_version, @@ -85,6 +85,11 @@ def subscribe_scd( base_url = webapp.config[config.KEY_BASE_URL] uss_base_url = f"{base_url}/tracer/f3548v21/{area_id}" + if not area.time_start or not area.time_end: + raise SubscriptionManagementError( + "Could not create new SCD Subscription -> time_start or time_end not set" + ) + create_result = mutate_scd.upsert_subscription( scd_client, box, diff --git a/monitoring/mock_uss/tracer/tracer_poll.py b/monitoring/mock_uss/tracer/tracer_poll.py index 9b1ee16dc0..def9c84f64 100755 --- a/monitoring/mock_uss/tracer/tracer_poll.py +++ b/monitoring/mock_uss/tracer/tracer_poll.py @@ -23,9 +23,18 @@ ObservationArea, ObservationAreaID, ) -from monitoring.monitorlib import fetch, versioning +from monitoring.monitorlib import versioning from monitoring.monitorlib.fetch.rid import FetchedISAs -from monitoring.monitorlib.fetch.scd import FetchedEntities +from monitoring.monitorlib.fetch.rid import isas as fetch_rid_isas +from monitoring.monitorlib.fetch.scd import ( + FetchedEntities, +) +from monitoring.monitorlib.fetch.scd import ( + constraints as fetch_scd_constraints, +) +from monitoring.monitorlib.fetch.scd import ( + operations as fetch_scd_operations, +) from monitoring.monitorlib.geo import get_latlngrect_vertices, make_latlng_rect from monitoring.monitorlib.infrastructure import UTMClientSession from monitoring.monitorlib.multiprocessing import SynchronizedValue @@ -102,14 +111,17 @@ def poll_observation_areas() -> None: def poll_isas(area: ObservationArea, logger: tracerlog.Logger) -> None: + if not area.f3411: + return + rid_client = context.get_client(area.f3411.auth_spec, area.f3411.dss_base_url) box = get_latlngrect_vertices(make_latlng_rect(area.area.volume)) t0 = datetime.datetime.now(datetime.UTC) - result = fetch.rid.isas( + result = fetch_rid_isas( box, - area.area.time_start.datetime, - area.area.time_end.datetime, + area.area.time_start.datetime if area.area.time_start else None, + area.area.time_end.datetime if area.area.time_end else None, area.f3411.rid_version, rid_client, ) @@ -144,13 +156,14 @@ def poll_isas(area: ObservationArea, logger: tracerlog.Logger) -> None: def poll_ops( area: ObservationArea, scd_client: UTMClientSession, logger: tracerlog.Logger ) -> None: + if not area.area.time_start or not area.area.time_end: + return + box = make_latlng_rect(area.area.volume) t0 = datetime.datetime.now(datetime.UTC) if "operational_intents" not in context.scd_cache: - context.scd_cache["operational_intents"]: dict[ - str, fetch.scd.FetchedEntity - ] = {} - result = fetch.scd.operations( + context.scd_cache["operational_intents"] = {} + result = fetch_scd_operations( scd_client, box, area.area.time_start.datetime, @@ -189,11 +202,14 @@ def poll_ops( def poll_constraints( area: ObservationArea, scd_client: UTMClientSession, logger: tracerlog.Logger ) -> None: + if not area.area.time_start or not area.area.time_end: + return + box = make_latlng_rect(area.area.volume) t0 = datetime.datetime.now(datetime.UTC) if "constraints" not in context.scd_cache: - context.scd_cache["constraints"]: dict[str, fetch.scd.FetchedEntity] = {} - result = fetch.scd.constraints( + context.scd_cache["constraints"] = {} + result = fetch_scd_constraints( scd_client, box, area.area.time_start.datetime, diff --git a/monitoring/mock_uss/tracer/tracerlog.py b/monitoring/mock_uss/tracer/tracerlog.py index 8535974818..6eea5c3343 100644 --- a/monitoring/mock_uss/tracer/tracerlog.py +++ b/monitoring/mock_uss/tracer/tracerlog.py @@ -10,7 +10,9 @@ class Logger: def __init__( - self, log_path: str, kml_session: infrastructure.KMLGenerationSession = None + self, + log_path: str, + kml_session: infrastructure.KMLGenerationSession | None = None, ): self.log_path = log_path os.makedirs(self.log_path, exist_ok=True) @@ -54,3 +56,14 @@ def log_new(self, content: TracerLogEntry) -> str: print(f"Error posting {kml_server_filename} to KML server: {e}") return logname + + +class DummyLogger(Logger): + def __init__(self): + pass + + def log_same(self, t0: datetime.datetime, t1: datetime.datetime, code: str) -> None: + pass + + def log_new(self, content: TracerLogEntry) -> str: + return "dummy" diff --git a/monitoring/mock_uss/ui/auth.py b/monitoring/mock_uss/ui/auth.py index 259868b51f..83c2ebe714 100644 --- a/monitoring/mock_uss/ui/auth.py +++ b/monitoring/mock_uss/ui/auth.py @@ -63,7 +63,7 @@ def is_admin(self) -> bool: def _get_users() -> list[User]: users = [] - user_strings = webapp.config.get(KEY_UI_USERS).split(";") + user_strings = (webapp.config.get(KEY_UI_USERS) or "").split(";") for user_string in user_strings: if not user_string.strip(): continue @@ -122,7 +122,9 @@ def ui_login_usernamepassword(): if not users: flask.flash("Invalid username/password combination") return flask.redirect(flask.url_for("ui_login")) - if check_password_hash(users[0].password_hash, flask.request.form["password"]): + if users[0].password_hash and check_password_hash( + users[0].password_hash, flask.request.form["password"] + ): flask_login.login_user(users[0]) return flask.redirect(flask.url_for("ui_login_successful")) else: @@ -132,6 +134,9 @@ def ui_login_usernamepassword(): @webapp.route("/ui/login/callback") def ui_login_callback(): + if not oauth_client: + return "Not in oauth mode", 400 + if "code" not in flask.request.args: return "Missing `code` in request arguments", 400 code = flask.request.args.get("code") @@ -150,8 +155,8 @@ def ui_login_callback(): headers=headers, data=body, auth=( - webapp.config.get(KEY_GOOGLE_OAUTH_CLIENT_ID), - webapp.config.get(KEY_GOOGLE_OAUTH_CLIENT_SECRET), + webapp.config.get(KEY_GOOGLE_OAUTH_CLIENT_ID, ""), + webapp.config.get(KEY_GOOGLE_OAUTH_CLIENT_SECRET, ""), ), ) oauth_client.parse_request_body_response(json.dumps(token_response.json())) diff --git a/monitoring/mock_uss/uspace/flight_auth.py b/monitoring/mock_uss/uspace/flight_auth.py index 9929b9306f..6f77d337fb 100644 --- a/monitoring/mock_uss/uspace/flight_auth.py +++ b/monitoring/mock_uss/uspace/flight_auth.py @@ -9,6 +9,10 @@ def validate_request(flight_info: FlightInfo) -> None: Args: flight_info: Information about the requested flight. """ + + if not flight_info.uspace_flight_authorisation: + return + problems = problems_with_flight_authorisation( flight_info.uspace_flight_authorisation ) diff --git a/monitoring/mock_uss/versioning/routes.py b/monitoring/mock_uss/versioning/routes.py index b26942dbb6..fc9a47ff0d 100644 --- a/monitoring/mock_uss/versioning/routes.py +++ b/monitoring/mock_uss/versioning/routes.py @@ -8,7 +8,7 @@ @webapp.route("/versioning/versions/", methods=["GET"]) @requires_scope(constants.Scope.ReadSystemVersions) -def versioning_get_version(system_identity: str) -> tuple[str, int]: +def versioning_get_version(system_identity: str) -> flask.Response: version = versioning.get_code_version() return flask.jsonify( api.GetVersionResponse( diff --git a/monitoring/monitorlib/fetch/rid.py b/monitoring/monitorlib/fetch/rid.py index 999873f3fd..209b5d0273 100644 --- a/monitoring/monitorlib/fetch/rid.py +++ b/monitoring/monitorlib/fetch/rid.py @@ -734,7 +734,7 @@ def version(self) -> str: return self.raw.version @property - def time_start(self) -> datetime: + def time_start(self) -> datetime.datetime: if self.rid_version == RIDVersion.f3411_19: return self.v19_value.time_start.datetime elif self.rid_version == RIDVersion.f3411_22a: @@ -745,7 +745,7 @@ def time_start(self) -> datetime: ) @property - def time_end(self) -> datetime: + def time_end(self) -> datetime.datetime: if self.rid_version == RIDVersion.f3411_19: return self.v19_value.time_end.datetime elif self.rid_version == RIDVersion.f3411_22a: diff --git a/pyproject.toml b/pyproject.toml index 2d3f8a7ff3..734798bdf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,3 +79,8 @@ exclude = [ "monitoring/mock_uss/output/*", "monitoring/uss_qualifier/output/*", ] + +[dependency-groups] +dev = [ + "types-lxml>=2025.8.25", +] diff --git a/uv.lock b/uv.lock index 93dc8f131f..8e8534121b 100644 --- a/uv.lock +++ b/uv.lock @@ -104,6 +104,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/88/27b4b4374e96bfd6b8e49cdde4e5aaa61eb9046b8ead9b18dd2d3ad6a154/bc_jsonpath_ng-1.6.1-py3-none-any.whl", hash = "sha256:2c85bb1d194376808fe1fc49558dd484e39024b15c719995e22de811e6ba4dc8", size = 29783, upload-time = "2023-11-26T13:29:28.789Z" }, ] +[[package]] +name = "beautifulsoup4" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822, upload-time = "2025-09-29T10:05:42.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" }, +] + [[package]] name = "bidict" version = "0.23.1" @@ -288,6 +301,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3e/7c/15ad426257615f9be8caf7f97990cf3dcbb5b8dd7ed7e0db581a1c4759dd/cryptography-46.0.2-cp38-abi3-win_arm64.whl", hash = "sha256:91447f2b17e83c9e0c89f133119d83f94ce6e0fb55dd47da0a959316e6e9cfa1", size = 2918153, upload-time = "2025-10-01T00:28:51.003Z" }, ] +[[package]] +name = "cssselect" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/0a/c3ea9573b1dc2e151abfe88c7fe0c26d1892fe6ed02d0cdb30f0d57029d5/cssselect-1.3.0.tar.gz", hash = "sha256:57f8a99424cfab289a1b6a816a43075a4b00948c86b4dcf3ef4ee7e15f7ab0c7", size = 42870, upload-time = "2025-03-10T09:30:29.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/58/257350f7db99b4ae12b614a36256d9cc870d71d9e451e79c2dc3b23d7c3c/cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d", size = 18786, upload-time = "2025-03-10T09:30:28.048Z" }, +] + [[package]] name = "decorator" version = "5.2.1" @@ -864,6 +886,11 @@ dependencies = [ { name = "uuid6" }, ] +[package.dev-dependencies] +dev = [ + { name = "types-lxml" }, +] + [package.metadata] requires-dist = [ { name = "aiohttp" }, @@ -913,6 +940,9 @@ requires-dist = [ { name = "uuid6" }, ] +[package.metadata.requires-dev] +dev = [{ name = "types-lxml", specifier = ">=2025.8.25" }] + [[package]] name = "msgpack" version = "1.1.1" @@ -1720,6 +1750,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, +] + [[package]] name = "structlog" version = "25.4.0" @@ -1754,6 +1793,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/30/f0660686920e09680b8afb0d2738580223dbef087a9bd92f3f14163c2fa6/testcontainers-4.13.1-py3-none-any.whl", hash = "sha256:10e6013a215eba673a0bcc153c8809d6f1c53c245e0a236e3877807652af4952", size = 123995, upload-time = "2025-09-24T22:47:45.44Z" }, ] +[[package]] +name = "types-html5lib" +version = "1.1.11.20250917" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/4b/a970718e8bd9324ee8fb8eaf02ff069f6d03c20d4523bb4232892ecc3d06/types_html5lib-1.1.11.20250917.tar.gz", hash = "sha256:7b52743377f33f9b4fd7385afbd2d457b8864ee51f90ff2a795ad9e8c053373a", size = 16868, upload-time = "2025-09-17T02:47:41.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/8a/da91a9c64dcb5e69beb567519857411996d8ecae9f6f128bcef8260e7a8d/types_html5lib-1.1.11.20250917-py3-none-any.whl", hash = "sha256:b294fd06d60da205daeb2f615485ca4d475088d2eff1009cf427f4a80fcd5346", size = 22908, upload-time = "2025-09-17T02:47:40.39Z" }, +] + +[[package]] +name = "types-lxml" +version = "2025.8.25" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "cssselect" }, + { name = "types-html5lib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/3e/a545ece610c1bd9699addd887edfe9477a8f647c4336ba75cfb0561d197c/types_lxml-2025.8.25.tar.gz", hash = "sha256:79b9f5b1f236f937f14fe3add9dc687bd8d4111ca5df58eb9f1bde1a3b032fd5", size = 156126, upload-time = "2025-08-26T06:28:56.793Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/29/c45f567b4142288b8184f073af8f659abd134c21de055f971c65f2d755bd/types_lxml-2025.8.25-py3-none-any.whl", hash = "sha256:d61340e5329e102d3f8d64124e90d50c12c0bfeaa9088d65558279ef4e7138ac", size = 95318, upload-time = "2025-08-26T06:28:54.066Z" }, +] + [[package]] name = "types-python-dateutil" version = "2.9.0.20250809"