From 34553626edef1fc3ade9809c3ea19ea46dcbcfaf Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:29:32 -0600 Subject: [PATCH 1/6] Wire OpenTelemetry into CLI --- dev/TODO.md | 18 +- dev/memory-efficiency-generate.py | 16 +- dev/memory-efficiency-monitor-sourcegraph.sh | 2 +- dev/memory-efficiency.md | 20 +- dev/test-end-to-end.py | 30 +-- pyproject.toml | 5 +- src/src_auth_perms_sync/cli.py | 23 +- tests/unit/test_cli_config.py | 19 +- uv.lock | 238 ++++++++++++++++++- 9 files changed, 305 insertions(+), 66 deletions(-) diff --git a/dev/TODO.md b/dev/TODO.md index 53c7b2e..c962112 100644 --- a/dev/TODO.md +++ b/dev/TODO.md @@ -1,17 +1,11 @@ # TODO -## High priority: Instrument with OpenTelemetry — in progress - -- [ ] Add OTel-native traces, metrics, and wide log events in `src-py-lib`. -- [ ] Add shared OTel bootstrap config/helpers with `--otel` and standard - `OTEL_*` env-var-backed CLI args. -- [ ] Replace custom trace-context propagation with OTel W3C propagation. -- [ ] Instrument shared HTTP and GraphQL clients manually, preserving safe - sanitized attributes and Sourcegraph-specific metadata. -- [ ] Rename Sourcegraph debug tracing from `--trace` to `--fetch-sg-traces`. -- [ ] Wire `src-auth-perms-sync` to the shared OTel bootstrap without doing - import-time logger/provider setup. -- [ ] Verify pyright, tests, and CLI help in both repos. +## High priority: Replace temporary src-py-lib branch source after release + +- Merge and release the `src-py-lib` OpenTelemetry branch. +- Replace the `src-auth-perms-sync` `tool.uv.sources.src-py-lib` branch source + with the released `src-py-lib[otel]` version. +- Re-run pyright, unit tests, and CLI help after the dependency is pinned. ## High priority: End to End test cases diff --git a/dev/memory-efficiency-generate.py b/dev/memory-efficiency-generate.py index e2b6f3d..bcbad6b 100755 --- a/dev/memory-efficiency-generate.py +++ b/dev/memory-efficiency-generate.py @@ -250,7 +250,7 @@ def main() -> int: explicit_permissions_batch_size=arguments.explicit_permissions_batch_size, http_timeout_seconds=arguments.http_timeout_seconds, sample_interval=arguments.sample_interval, - trace=arguments.trace, + fetch_sg_traces=arguments.fetch_sg_traces, sourcegraph_user_count=total_user_count, sourcegraph_inventory_repo_count=inventory_repo_count, ) @@ -371,9 +371,9 @@ def build_parser() -> argparse.ArgumentParser: help="CLI --sample-interval for resource samples (default: 1).", ) parser.add_argument( - "--trace", + "--fetch-sg-traces", action="store_true", - help="Pass --trace to src-auth-perms-sync sweep runs.", + help="Pass --fetch-sg-traces to src-auth-perms-sync sweep runs.", ) return parser @@ -841,7 +841,7 @@ def run_sweep( explicit_permissions_batch_size: int, http_timeout_seconds: float, sample_interval: float, - trace: bool, + fetch_sg_traces: bool, sourcegraph_user_count: int, sourcegraph_inventory_repo_count: int, ) -> list[CommandRunResult]: @@ -858,7 +858,7 @@ def run_sweep( explicit_permissions_batch_size=explicit_permissions_batch_size, http_timeout_seconds=http_timeout_seconds, sample_interval=sample_interval, - trace=trace, + fetch_sg_traces=fetch_sg_traces, ) environment = command_environment(endpoint, access_token) process = subprocess.run( @@ -911,7 +911,7 @@ def command_arguments( explicit_permissions_batch_size: int, http_timeout_seconds: float, sample_interval: float, - trace: bool, + fetch_sg_traces: bool, ) -> list[str]: arguments = [ *shlex.split(command), @@ -932,8 +932,8 @@ def command_arguments( arguments.append("--apply") if mode == "apply-no-backup": arguments.append("--no-backup") - if trace: - arguments.append("--trace") + if fetch_sg_traces: + arguments.append("--fetch-sg-traces") return arguments diff --git a/dev/memory-efficiency-monitor-sourcegraph.sh b/dev/memory-efficiency-monitor-sourcegraph.sh index 07fab15..c56955a 100755 --- a/dev/memory-efficiency-monitor-sourcegraph.sh +++ b/dev/memory-efficiency-monitor-sourcegraph.sh @@ -41,7 +41,7 @@ Examples: --output-dir /tmp/src-auth-perms-sync-load-$(date -u +%Y%m%d-%H%M%S) In another terminal, run: - uv run python dev/test-end-to-end.py --trace --sample-interval 0 --external-sample-interval 0 + uv run python dev/test-end-to-end.py --fetch-sg-traces --sample-interval 0 --external-sample-interval 0 EOF } diff --git a/dev/memory-efficiency.md b/dev/memory-efficiency.md index 931c3f7..aaf90a8 100644 --- a/dev/memory-efficiency.md +++ b/dev/memory-efficiency.md @@ -7,13 +7,14 @@ bulk explicit-permissions APIs. ## Capture a focused trace -Run a small command with `--trace`. This sends `X-Sourcegraph-Should-Trace` and -a sampled W3C `traceparent` header on each GraphQL request. Sourcegraph may -also return `x-trace`, `x-trace-span`, and `x-trace-url` response headers. +Run a small command with `--fetch-sg-traces`. This sends +`X-Sourcegraph-Request-Trace` and a W3C `traceparent` header on each GraphQL +request. Sourcegraph may also return `x-trace`, `x-trace-span`, and +`x-trace-url` response headers. ```bash uv run src-auth-perms-sync get \ - --trace \ + --fetch-sg-traces \ --sample-interval 0 \ --parallelism 2 \ --explicit-permissions-batch-size 25 @@ -87,13 +88,14 @@ artifacts. Keep them in `/tmp` unless a human asks to preserve them. ## Trace the end-to-end matrix -Prefer the end-to-end runner as the single orchestrator. With `--trace`, it -passes tracing to every child CLI command, tails child JSON logs, and fetches -Jaeger traces in the background while each child command is still running. +Prefer the end-to-end runner as the single orchestrator. With +`--fetch-sg-traces`, it passes Sourcegraph debug trace collection to every child +CLI command, tails child JSON logs, and fetches Jaeger traces in the background +while each child command is still running. ```bash uv run python dev/test-end-to-end.py \ - --trace \ + --fetch-sg-traces \ --sample-interval 0 \ --external-sample-interval 0 \ --results-json /tmp/src-auth-perms-sync-end-to-end-trace.json \ @@ -136,7 +138,7 @@ artifact paths into the result JSON: ```bash uv run python dev/test-end-to-end.py \ - --trace \ + --fetch-sg-traces \ --monitor-sourcegraph-load \ --sample-interval 0 \ --external-sample-interval 0 \ diff --git a/dev/test-end-to-end.py b/dev/test-end-to-end.py index 7f5edd1..35d12bc 100755 --- a/dev/test-end-to-end.py +++ b/dev/test-end-to-end.py @@ -191,12 +191,12 @@ class EndToEndConfig(src.SourcegraphClientConfig, src.LoggingConfig): cli_action="store_true", help="Continue after assertion failures where it is safe to do so", ) - trace: bool = src.config_field( + fetch_sg_traces: bool = src.config_field( default=False, - env_var="SRC_AUTH_PERMS_SYNC_E2E_TRACE", - cli_flag="--trace", + env_var="SRC_AUTH_PERMS_SYNC_E2E_FETCH_SG_TRACES", + cli_flag="--fetch-sg-traces", cli_action="store_true", - help="Pass --trace to each child src-auth-perms-sync command", + help="Pass --fetch-sg-traces to each child src-auth-perms-sync command", ) jaeger_trace_limit: int | None = src.config_field( default=DEFAULT_JAEGER_TRACE_LIMIT, @@ -205,7 +205,8 @@ class EndToEndConfig(src.SourcegraphClientConfig, src.LoggingConfig): metavar="N", ge=0, help=( - "When --trace is set, fetch and summarize the N slowest GraphQL Jaeger traces " + "When --fetch-sg-traces is set, fetch and summarize the N slowest GraphQL " + "Jaeger traces " "while each child command runs; omit for all traces, set 0 to disable" ), ) @@ -216,7 +217,7 @@ class EndToEndConfig(src.SourcegraphClientConfig, src.LoggingConfig): metavar="N", ge=1, help=( - "Concurrent Jaeger trace fetch requests when --trace is set " + "Concurrent Jaeger trace fetch requests when --fetch-sg-traces is set " f"(default: {DEFAULT_JAEGER_TRACE_PARALLELISM})" ), ) @@ -238,7 +239,7 @@ class EndToEndConfig(src.SourcegraphClientConfig, src.LoggingConfig): metavar="PATH", help=( "Write Jaeger trace summaries incrementally as JSON Lines. Defaults to a sibling " - "of --results-json or --results-csv when --trace is set." + "of --results-json or --results-csv when --fetch-sg-traces is set." ), ) jaeger_trace_directory: Path | None = src.config_field( @@ -248,7 +249,8 @@ class EndToEndConfig(src.SourcegraphClientConfig, src.LoggingConfig): metavar="PATH", help=( "Directory where complete raw Jaeger trace JSON files are written. Defaults " - "to a sibling directory of --results-json or --results-csv when --trace is set." + "to a sibling directory of --results-json or --results-csv when --fetch-sg-traces " + "is set." ), ) jaeger_retry_delays: tuple[float, ...] = src.config_field( @@ -1068,7 +1070,7 @@ def __init__( *, iteration: int, keep_going: bool, - trace: bool, + fetch_sg_traces: bool, jaeger_trace_limit: int | None, jaeger_trace_fetch_pool: JaegerTraceFetchPool | None, sample_interval: float, @@ -1078,7 +1080,7 @@ def __init__( self.environment = environment self.iteration = iteration self.keep_going = keep_going - self.trace = trace + self.fetch_sg_traces = fetch_sg_traces self.jaeger_trace_limit = jaeger_trace_limit self.jaeger_trace_fetch_pool = jaeger_trace_fetch_pool self.sample_interval = sample_interval @@ -1106,7 +1108,7 @@ def _run_process(self, case: CommandCase) -> CommandResult: full_command = [ *self.variant.executable, *case.arguments, - *(("--trace",) if self.trace else ()), + *(("--fetch-sg-traces",) if self.fetch_sg_traces else ()), "--sample-interval", str(self.sample_interval), ] @@ -1310,7 +1312,7 @@ def run_end_to_end(config: EndToEndConfig) -> None: "end_to_end_matrix", repeat=config.repeat, variant_count=len(variants), - trace=config.trace, + fetch_sg_traces=config.fetch_sg_traces, sourcegraph_load_monitor=sourcegraph_load_monitor is not None, ) as matrix_summary: if sourcegraph_load_monitor is not None: @@ -1325,7 +1327,7 @@ def run_end_to_end(config: EndToEndConfig) -> None: environment, iteration=iteration, keep_going=config.keep_going, - trace=config.trace, + fetch_sg_traces=config.fetch_sg_traces, jaeger_trace_limit=config.jaeger_trace_limit, jaeger_trace_fetch_pool=jaeger_trace_fetch_pool, sample_interval=config.sample_interval, @@ -1382,7 +1384,7 @@ def create_jaeger_trace_fetch_pool( config: EndToEndConfig, ) -> JaegerTraceFetchPool | None: """Return the shared trace fetch pool for this run, if trace collection is enabled.""" - if not config.trace or config.jaeger_trace_limit == 0: + if not config.fetch_sg_traces or config.jaeger_trace_limit == 0: return None return JaegerTraceFetchPool( config, diff --git a/pyproject.toml b/pyproject.toml index de5696e..3e21f1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ classifiers = [ dependencies = [ "json5>=0.14.0", "pyyaml>=6.0.3", - "src-py-lib==0.1.9", + "src-py-lib[otel]", ] keywords = [ "Sourcegraph" @@ -67,3 +67,6 @@ select = ["E", "F", "I", "B", "UP", "SIM"] [tool.uv] package = true + +[tool.uv.sources] +src-py-lib = { git = "https://github.com/sourcegraph/src-py-lib.git", rev = "amp/otel-standard-instrumentation" } diff --git a/src/src_auth_perms_sync/cli.py b/src/src_auth_perms_sync/cli.py index dfbe308..5dc018c 100644 --- a/src/src_auth_perms_sync/cli.py +++ b/src/src_auth_perms_sync/cli.py @@ -36,11 +36,12 @@ COMMON_CONFIG_FIELDS = src.config_field_names( src.SourcegraphClientConfig, src.LoggingConfig, + src.OpenTelemetryConfig, "parallelism", "http_timeout_seconds", "max_attempts", "sample_interval", - "trace", + "fetch_sg_traces", ) GET_CONFIG_FIELDS = src.config_field_names( "users", @@ -146,7 +147,7 @@ class CliCommand: config_fields: tuple[str, ...] -class Config(src.SourcegraphClientConfig, src.LoggingConfig): +class Config(src.SourcegraphClientConfig, src.LoggingConfig, src.OpenTelemetryConfig): """Config values loaded from defaults, .env, environment, and CLI flags.""" maps_path: Path | None = src.config_field( @@ -276,12 +277,12 @@ class Config(src.SourcegraphClientConfig, src.LoggingConfig): help="Seconds between logging compute resource samples; set 0 to disable (default: 10)", help_group="Runtime", ) - trace: bool = src.config_field( + fetch_sg_traces: bool = src.config_field( default=False, - env_var="SRC_AUTH_PERMS_SYNC_TRACE", - cli_flag="--trace", + env_var="SRC_AUTH_PERMS_SYNC_FETCH_SG_TRACES", + cli_flag="--fetch-sg-traces", cli_action="store_true", - help=("Ask Sourcegraph to retain traces for GraphQL requests and return trace metadata"), + help="Ask Sourcegraph to retain GraphQL traces and return debug trace metadata", help_group="Runtime", ) @@ -515,7 +516,8 @@ def run_fields(config: Config, command: ResolvedCommand, endpoint: str) -> dict[ "endpoint": endpoint, "parallelism": config.parallelism, "explicit_permissions_batch_size": config.explicit_permissions_batch_size, - "trace": config.trace, + "fetch_sg_traces": config.fetch_sg_traces, + "open_telemetry": config.open_telemetry, "max_attempts": config.max_attempts, "http_timeout_seconds": config.http_timeout_seconds, "no_backup": config.no_backup, @@ -544,7 +546,7 @@ def run_with_client( endpoint=endpoint, token=config.src_access_token, http=http, - trace=config.trace, + fetch_sg_traces=config.fetch_sg_traces, ) try: run_command(config, command, client, worker_pool) @@ -754,6 +756,11 @@ def _run_or_raise(command_name: CommandName, config: Config) -> None: log_file=backups.run_log_path(run_directory), logs_dir=None, resource_sample_interval_seconds=config.sample_interval, + open_telemetry=src.open_telemetry_settings_from_config( + config, + force_traces=config.fetch_sg_traces, + service_name="src-auth-perms-sync", + ), ) with ( diff --git a/tests/unit/test_cli_config.py b/tests/unit/test_cli_config.py index afc6262..b6bafb7 100644 --- a/tests/unit/test_cli_config.py +++ b/tests/unit/test_cli_config.py @@ -304,13 +304,19 @@ def test_http_timeout_rejects_values_at_or_below_zero(self) -> None: with self.assertRaisesRegex(shared_config.ConfigError, "greater than 0"): load_config_from_env(SRC_AUTH_PERMS_SYNC_HTTP_TIMEOUT_SECONDS="0") - def test_trace_config_is_loaded_from_env(self) -> None: - config = load_config_from_env(SRC_AUTH_PERMS_SYNC_TRACE="true") + def test_fetch_sg_traces_config_is_loaded_from_env(self) -> None: + config = load_config_from_env(SRC_AUTH_PERMS_SYNC_FETCH_SG_TRACES="true") - self.assertTrue(config.trace) + self.assertTrue(config.fetch_sg_traces) + + def test_open_telemetry_config_is_loaded_from_env(self) -> None: + config = load_config_from_env(OTEL_ENABLED="true", OTEL_SERVICE_NAME="src-auth-test") + + self.assertTrue(config.open_telemetry) + self.assertEqual("src-auth-test", config.open_telemetry_service_name) def test_run_with_client_enables_sourcegraph_trace_collection(self) -> None: - configuration = make_config(trace=True) + configuration = make_config(fetch_sg_traces=True) command = cli.resolve_command("get", configuration) captured_clients: list[src.SourcegraphClient] = [] @@ -334,7 +340,7 @@ def capture_client( ) self.assertEqual(1, len(captured_clients)) - self.assertTrue(captured_clients[0].trace) + self.assertTrue(captured_clients[0].fetch_sg_traces) def test_run_with_client_uses_configured_http_timeout(self) -> None: configuration = make_config(http_timeout_seconds=75.0) @@ -419,7 +425,8 @@ def test_run_fields_include_concrete_command(self) -> None: self.assertEqual("users", fields["set_mode"]) self.assertEqual(True, fields["apply_flag"]) self.assertEqual(25, fields["explicit_permissions_batch_size"]) - self.assertEqual(False, fields["trace"]) + self.assertEqual(False, fields["fetch_sg_traces"]) + self.assertEqual(False, fields["open_telemetry"]) self.assertEqual(60.0, fields["http_timeout_seconds"]) def test_run_command_passes_set_data_to_combined_sync(self) -> None: diff --git a/uv.lock b/uv.lock index 3feed68..22b8b80 100644 --- a/uv.lock +++ b/uv.lock @@ -33,6 +33,107 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -97,6 +198,102 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "opentelemetry-api" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", size = 21793, upload-time = "2026-05-21T16:32:35.277Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", size = 71782, upload-time = "2026-05-21T16:32:44.867Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.63b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + [[package]] name = "pydantic" version = "2.13.4" @@ -291,6 +488,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + [[package]] name = "ruff" version = "0.15.16" @@ -322,7 +534,7 @@ source = { editable = "." } dependencies = [ { name = "json5" }, { name = "pyyaml" }, - { name = "src-py-lib" }, + { name = "src-py-lib", extra = ["otel"] }, ] [package.dev-dependencies] @@ -336,7 +548,7 @@ dev = [ requires-dist = [ { name = "json5", specifier = ">=0.14.0" }, { name = "pyyaml", specifier = ">=6.0.3" }, - { name = "src-py-lib", specifier = "==0.1.9" }, + { name = "src-py-lib", extras = ["otel"], git = "https://github.com/sourcegraph/src-py-lib.git?rev=amp%2Fotel-standard-instrumentation" }, ] [package.metadata.requires-dev] @@ -348,16 +560,19 @@ dev = [ [[package]] name = "src-py-lib" -version = "0.1.9" -source = { registry = "https://pypi.org/simple" } +version = "0.1.10.dev2+g37c970897" +source = { git = "https://github.com/sourcegraph/src-py-lib.git?rev=amp%2Fotel-standard-instrumentation#37c970897e832ee71c55b019a0a9fb296d305f94" } dependencies = [ { name = "httpx" }, + { name = "opentelemetry-api" }, { name = "pydantic" }, { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/f9/f8f1105962fbec8ed5ff5e19335fc6e91ef6617bcf5d95dc3fb9ebf4cde5/src_py_lib-0.1.9.tar.gz", hash = "sha256:3f8a15ab776190d7bb717ffd9ebd0440c66c7a9655e06550d2117c721021b9cc", size = 72801, upload-time = "2026-06-09T01:41:20.991Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/95/8a5f402c9c8a000896c651e96893e8cd8a9cbfa67b2d2a3793ed777947c6/src_py_lib-0.1.9-py3-none-any.whl", hash = "sha256:32c88e79b99f34160cc5583b25ce1ab829350223ca2b488e1ca1545bb4ab3582", size = 45631, upload-time = "2026-06-09T01:41:19.738Z" }, + +[package.optional-dependencies] +otel = [ + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, ] [[package]] @@ -389,3 +604,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] From 17f8dfbec219b027efd14230b3300fa55bce9e64 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Mon, 8 Jun 2026 23:44:29 -0600 Subject: [PATCH 2/6] Use renamed src-py-lib span helpers Amp-Thread-ID: https://ampcode.com/threads/T-019eaab7-5224-7118-8bd5-090093ba01d3 Co-authored-by: Amp --- dev/TODO.md | 14 ++++++++ dev/test-end-to-end.py | 2 +- src/src_auth_perms_sync/orgs/sync.py | 22 ++++++------- src/src_auth_perms_sync/permissions/apply.py | 8 ++--- .../permissions/command.py | 6 ++-- .../permissions/full_set.py | 4 +-- src/src_auth_perms_sync/permissions/maps.py | 6 ++-- .../permissions/restore.py | 4 +-- .../permissions/snapshot.py | 32 +++++++++---------- .../permissions/workflow.py | 4 +-- src/src_auth_perms_sync/shared/site_config.py | 2 +- uv.lock | 4 +-- 12 files changed, 61 insertions(+), 47 deletions(-) diff --git a/dev/TODO.md b/dev/TODO.md index c962112..73ba9a8 100644 --- a/dev/TODO.md +++ b/dev/TODO.md @@ -1,5 +1,19 @@ # TODO +## High priority: Instrument with OpenTelemetry — in progress + +- Add OTel-native traces, metrics, and wide log events in `src-py-lib` + - Would we gain anything by auto-instrumenting httpx? + - Need to keep/wrap custom/sanitized fields like request/response bytes, + redacted headers, retry counts, Sourcegraph metadata + - Keep our custom GraphQL + - Would we lose anything by switching to opentelemetry-instrumentation-system-metrics? + - Investigate opentelemetry-instrumentation-threading +- Replace custom trace-context propagation with OTel W3C propagation +- Add shared OTel bootstrap config/helpers with `--otel` and standard `OTEL_*` args +- Wire `src-auth-perms-sync` to the shared OTel bootstrap without doing import-time logger/provider setup +- Verify pyright, tests, and CLI help in both repos + ## High priority: Replace temporary src-py-lib branch source after release - Merge and release the `src-py-lib` OpenTelemetry branch. diff --git a/dev/test-end-to-end.py b/dev/test-end-to-end.py index 35d12bc..5060df9 100755 --- a/dev/test-end-to-end.py +++ b/dev/test-end-to-end.py @@ -1308,7 +1308,7 @@ def run_end_to_end(config: EndToEndConfig) -> None: try: if sourcegraph_load_monitor is not None: sourcegraph_load_monitor.start() - with src.event( + with src.span( "end_to_end_matrix", repeat=config.repeat, variant_count=len(variants), diff --git a/src/src_auth_perms_sync/orgs/sync.py b/src/src_auth_perms_sync/orgs/sync.py index fc170fd..1b5819a 100644 --- a/src/src_auth_perms_sync/orgs/sync.py +++ b/src/src_auth_perms_sync/orgs/sync.py @@ -335,7 +335,7 @@ def cmd_sync_saml_organizations( characters are converted to `-`; any resulting name collision fails before mutation so we never merge unrelated SAML groups accidentally. """ - with src.event( + with src.span( "cmd_sync_saml_organizations", dry_run=dry_run, parallelism=parallelism, @@ -544,7 +544,7 @@ def _load_current_organization_states( states: dict[str, organization_types.OrganizationState] = {} current_user: organization_types.OrgMember | None = None name_batches = list(_chunks(organization_names, ORGANIZATION_LOOKUP_BATCH_SIZE)) - with src.event( + with src.span( "load_current_organization_states", organization_count=len(organization_names), lookup_batch_count=len(name_batches), @@ -606,7 +606,7 @@ def _fetch_organization_batch( } for index, organization_name in enumerate(organization_names): variables[f"name{index}"] = organization_name - with src.event( + with src.span( "organization_batch_lookup", level="DEBUG", organization_count=len(organization_names), @@ -687,7 +687,7 @@ def _fetch_all_members( ) -> list[organization_types.OrgMember]: if state.id is None: return [] - with src.event("organization_members", level="DEBUG", organization_name=state.name): + with src.span("organization_members", level="DEBUG", organization_name=state.name): return [ cast(organization_types.OrgMember, node) for node in client.stream_connection_nodes( @@ -765,7 +765,7 @@ def _apply_create_organizations( ) -> shared_types.MutationCounts: if not organization_names: return shared_types.MutationCounts() - with src.event( + with src.span( "apply_create_organizations", organization_count=len(organization_names), parallelism=parallelism, @@ -820,7 +820,7 @@ def _create_organization( organization_name: str, current_user: organization_types.OrgMember, ) -> organization_types.OrganizationState: - with src.event("create_organization", organization_name=organization_name): + with src.span("create_organization", organization_name=organization_name): try: data = client.graphql( queries.MUTATION_CREATE_ORGANIZATION, @@ -868,7 +868,7 @@ def _apply_user_changes( ) -> shared_types.MutationCounts: if not changes: return shared_types.MutationCounts() - with src.event( + with src.span( "apply_organization_user_changes", change_kind=change_kind, change_count=len(changes), @@ -941,7 +941,7 @@ def _apply_user_change( if state.id is None: raise RuntimeError(f"organization {change.organization_name!r} has no ID") if change_kind == "add": - with src.event( + with src.span( "add_user_to_organization", organization_name=change.organization_name, username=change.username, @@ -961,7 +961,7 @@ def _apply_user_change( return raise return - with src.event( + with src.span( "remove_user_from_organization", organization_name=change.organization_name, username=change.username, @@ -1122,7 +1122,7 @@ def _validate_organization_sync( def _write_organization_snapshot( path: Path, snapshot: organization_types.OrganizationSnapshot ) -> None: - with src.event( + with src.span( "disk_io", level="DEBUG", op="write", @@ -1140,7 +1140,7 @@ def _write_organization_snapshot_diff( before: organization_types.OrganizationSnapshot, after: organization_types.OrganizationSnapshot, ) -> None: - with src.event( + with src.span( "disk_io", level="DEBUG", op="write", diff --git a/src/src_auth_perms_sync/permissions/apply.py b/src/src_auth_perms_sync/permissions/apply.py index 302cef6..1dc8dd3 100644 --- a/src/src_auth_perms_sync/permissions/apply.py +++ b/src/src_auth_perms_sync/permissions/apply.py @@ -143,7 +143,7 @@ def set_repo_permissions( same retry plumbing. Application-level GraphQL errors (auth, validation, schema) are NOT retried — they propagate on the first attempt. """ - with src.event( + with src.span( "set_repo_perms", repo_id=repo_id, user_count=len(user_perms), @@ -174,7 +174,7 @@ def _mutate_repo_permission_for_user( event_name: str, ) -> None: """Apply one additive repo-permission edge mutation.""" - with src.event( + with src.span( event_name, repo_id=change.repo_id, username=change.username, @@ -211,7 +211,7 @@ def _apply_permission_changes( action: str, ) -> shared_types.MutationCounts: """Dispatch additive edge mutations across a thread pool.""" - with src.event( + with src.span( f"apply_{action}_payloads", payload_count=len(changes), parallelism=parallelism, @@ -337,7 +337,7 @@ def _apply_repo_overwrite_plans( """Dispatch per-repo overwrite mutations with bounded in-flight work.""" max_pending_futures = max(1, parallelism * 2) payload_grant_count = sum(len(overwrite.usernames) for overwrite in overwrites) - with src.event( + with src.span( "apply_username_overwrites", payload_count=len(overwrites), parallelism=parallelism, diff --git a/src/src_auth_perms_sync/permissions/command.py b/src/src_auth_perms_sync/permissions/command.py index c9f4e5e..1d4e37b 100644 --- a/src/src_auth_perms_sync/permissions/command.py +++ b/src/src_auth_perms_sync/permissions/command.py @@ -115,7 +115,7 @@ def cmd_get( `auth-providers.yaml` alongside the GraphQL-discovered fields. Providers without an explicit `configID` get only the GraphQL-derived view. """ - with src.event( + with src.span( "cmd_get", code_hosts_path=str(code_hosts_path), auth_providers_path=str(auth_providers_path), @@ -377,7 +377,7 @@ def cmd_set_additive_users( worker_pool: ThreadPoolExecutor | None = None, ) -> run_context.CommandData: """Add missing mapped permissions for resolved users.""" - with src.event( + with src.span( "cmd_set_additive_users", input_path=str(input_path), user_identifiers=user_identifiers, @@ -455,7 +455,7 @@ def cmd_set_additive_users_without_explicit_perms( created_after_filter = sourcegraph_datetime_filter( parse_cli_date(user_created_after, "--created-after") ) - with src.event( + with src.span( "cmd_set_additive_users_without_explicit_perms", input_path=str(input_path), user_created_after=user_created_after, diff --git a/src/src_auth_perms_sync/permissions/full_set.py b/src/src_auth_perms_sync/permissions/full_set.py index a3534d8..b027383 100644 --- a/src/src_auth_perms_sync/permissions/full_set.py +++ b/src/src_auth_perms_sync/permissions/full_set.py @@ -418,7 +418,7 @@ def _filter_full_set_plans( return _FullSetPlanFilter(overwrites=overwrites, skipped_repo_ids=set()) skipped_repo_ids: set[str] = set() - with src.event( + with src.span( "short_circuit_filter", repos_planned=len(overwrites), ) as short_circuit_event: @@ -827,7 +827,7 @@ def cmd_set_full( worker_pool: ThreadPoolExecutor | None = None, ) -> run_context.CommandData: """Overwrite each mapped repo with the union of users from all rules.""" - with src.event( + with src.span( "cmd_set", input_path=str(input_path), user_created_after=user_created_after, diff --git a/src/src_auth_perms_sync/permissions/maps.py b/src/src_auth_perms_sync/permissions/maps.py index d6cb86d..8f37bb0 100644 --- a/src/src_auth_perms_sync/permissions/maps.py +++ b/src/src_auth_perms_sync/permissions/maps.py @@ -215,7 +215,7 @@ def _dump_readonly_discovery_yaml( section_name: str, entries: list[dict[str, Any]], ) -> None: - with src.event( + with src.span( "disk_io", level="DEBUG", op="write", @@ -250,7 +250,7 @@ def create_maps_yaml_if_missing(path: Path) -> bool: "\n" "- name: Map 1\n" ) - with src.event( + with src.span( "disk_io", level="DEBUG", op="write", @@ -270,7 +270,7 @@ def create_maps_yaml_if_missing(path: Path) -> bool: def load_maps_yaml(path: Path) -> permission_types.ConfigFile: - with src.event( + with src.span( "disk_io", level="DEBUG", op="read", diff --git a/src/src_auth_perms_sync/permissions/restore.py b/src/src_auth_perms_sync/permissions/restore.py index 5ab1ece..26902c9 100644 --- a/src/src_auth_perms_sync/permissions/restore.py +++ b/src/src_auth_perms_sync/permissions/restore.py @@ -83,7 +83,7 @@ def cmd_restore_user_scoped( worker_pool: ThreadPoolExecutor | None = None, ) -> None: """Restore explicit permissions for the users present in a scoped snapshot.""" - with src.event( + with src.span( "cmd_restore_user_scoped", snapshot_path=str(snapshot_path), dry_run=dry_run, @@ -856,7 +856,7 @@ def cmd_restore( return target_full_snapshot = cast(permission_snapshot.Snapshot, target_snapshot) - with src.event( + with src.span( "cmd_restore", snapshot_path=str(snapshot_path), dry_run=dry_run, diff --git a/src/src_auth_perms_sync/permissions/snapshot.py b/src/src_auth_perms_sync/permissions/snapshot.py index 3073205..63cd2d7 100644 --- a/src/src_auth_perms_sync/permissions/snapshot.py +++ b/src/src_auth_perms_sync/permissions/snapshot.py @@ -200,12 +200,12 @@ def _fetch( batch_users: list[SnapshotUserInput], ) -> tuple[dict[str, list[str]], int]: # High-frequency (one per user-batch): - # - log the whole event (start + end) at DEBUG; failures still - # get bumped to ERROR by the event() helper - # - drop the per-event `status="ok"` / `error_type=null` noise on + # - log the whole span (start + end) at DEBUG; failures still + # get bumped to ERROR by the span() helper + # - drop the per-span `status="ok"` / `error_type=null` noise on # successes (failures still carry both fields) # - omit user IDs since usernames are far more readable - with src.event( + with src.span( "user_explicit_repos_batch_fetch", level="DEBUG", omit_success_status=True, @@ -259,7 +259,7 @@ def _fetch_one_user_at_a_time( repository_ids_by_user_id[user["id"]] = [] return repository_ids_by_user_id, failures - with src.event( + with src.span( "capture_explicit_grants", total_users=total_users, explicit_permissions_batch_size=explicit_permissions_batch_size, @@ -375,7 +375,7 @@ def _record_completed_futures(done_futures: Iterable[Any]) -> None: for usernames in usernames_by_repository_id.values(): usernames.sort() - with src.event( + with src.span( "hydrate_explicit_repository_names", repository_count=len(usernames_by_repository_id), ) as hydrate_event: @@ -430,7 +430,7 @@ def build_snapshot( `total_users`, when known, drives percentage + ETA in the per-batch progress log lines emitted by `capture_explicit_grants`. """ - with src.event("build_snapshot", bind_id_mode=bind_id_mode) as build_event: + with src.span("build_snapshot", bind_id_mode=bind_id_mode) as build_event: repos, user_count = capture_explicit_grants( client, users, @@ -485,7 +485,7 @@ def capture_user_scoped_explicit_grants( scoped_users: dict[str, UserScopedUserSnapshot] = {} def _fetch(user: SnapshotUser) -> tuple[SnapshotUser, list[permission_types.Repository]]: - with src.event( + with src.span( "user_scoped_explicit_repos_fetch", level="DEBUG", omit_success_status=True, @@ -495,7 +495,7 @@ def _fetch(user: SnapshotUser) -> tuple[SnapshotUser, list[permission_types.Repo fetch_event["repo_count"] = len(repos) return user, repos - with src.event("capture_user_scoped_explicit_grants") as capture_event: + with src.span("capture_user_scoped_explicit_grants") as capture_event: futures: dict[Any, SnapshotUser] = {} with run_context.thread_pool(parallelism, worker_pool) as executor: for user in users: @@ -533,7 +533,7 @@ def build_user_scoped_snapshot( worker_pool: ThreadPoolExecutor | None = None, ) -> UserScopedSnapshot: """Capture a reversible snapshot for only the supplied users.""" - with src.event("build_user_scoped_snapshot", bind_id_mode=bind_id_mode) as build_event: + with src.span("build_user_scoped_snapshot", bind_id_mode=bind_id_mode) as build_event: scoped_users = capture_user_scoped_explicit_grants( client, users, @@ -711,7 +711,7 @@ def write_snapshot_with_repos( repos: Iterable[tuple[str, RepoSnapshot]], ) -> None: """Persist a full snapshot from an iterable of repo entries.""" - with src.event( + with src.span( "disk_io", level="DEBUG", op="write", @@ -736,7 +736,7 @@ def write_snapshot(path: Path, snapshot: Snapshot) -> None: def write_user_scoped_snapshot(path: Path, snapshot: UserScopedSnapshot) -> None: """Persist a user-scoped snapshot with readable repository IDs.""" - with src.event( + with src.span( "disk_io", level="DEBUG", op="write", @@ -785,7 +785,7 @@ def write_user_scoped_snapshot(path: Path, snapshot: UserScopedSnapshot) -> None def _read_snapshot_raw(path: Path, file_kind: str) -> dict[str, Any]: - with src.event( + with src.span( "disk_io", level="DEBUG", op="read", @@ -1167,7 +1167,7 @@ def write_snapshot_diff_from_snapshot_parts( ) -> None: """Persist a full-snapshot diff without materializing every repo diff.""" plan = _plan_snapshot_diff(before, after, repo_ids, after_repo_for_id) - with src.event( + with src.span( "disk_io", level="DEBUG", op="write", @@ -1197,7 +1197,7 @@ def write_snapshot_diff_from_snapshots(path: Path, before: Snapshot, after: Snap def write_snapshot_diff(path: Path, diff: SnapshotDiff) -> None: """Persist a compact full-snapshot diff as pretty-printed JSON.""" - with src.event( + with src.span( "disk_io", level="DEBUG", op="write", @@ -1270,7 +1270,7 @@ def build_user_scoped_snapshot_diff( def write_user_scoped_snapshot_diff(path: Path, diff: UserScopedSnapshotDiff) -> None: """Persist a compact user-scoped snapshot diff as pretty-printed JSON.""" - with src.event( + with src.span( "disk_io", level="DEBUG", op="write", diff --git a/src/src_auth_perms_sync/permissions/workflow.py b/src/src_auth_perms_sync/permissions/workflow.py index 7f0ab66..3e42cc6 100644 --- a/src/src_auth_perms_sync/permissions/workflow.py +++ b/src/src_auth_perms_sync/permissions/workflow.py @@ -58,7 +58,7 @@ def load_repos_by_external_service( services_by_id: dict[int, permission_types.ExternalService], ) -> dict[int, list[permission_types.Repository]]: """Fetch repos once per discovered code host connection.""" - with src.event( + with src.span( "load_repos_by_external_service", external_service_count=len(services_by_id), ) as load_event: @@ -246,7 +246,7 @@ def write_maps_backup( return None output_path = maps_backup_path(input_path, timestamp, endpoint, command) - with src.event( + with src.span( "disk_io", level="DEBUG", op="write", diff --git a/src/src_auth_perms_sync/shared/site_config.py b/src/src_auth_perms_sync/shared/site_config.py index e5967ba..133d46a 100644 --- a/src/src_auth_perms_sync/shared/site_config.py +++ b/src/src_auth_perms_sync/shared/site_config.py @@ -172,7 +172,7 @@ def _query_site_configuration( # error_reason) instead of just bubbling up as an un-annotated # `error_type=SystemExit` on the run end event. Each underlying GraphQL/HTTP # attempt still emits shared-library `graphql_query` / `http_request` events. - with src.event(event_name) as site_config_event: + with src.span(event_name) as site_config_event: try: data = client.graphql(queries.QUERY_VALIDATE_PERMISSIONS_CONFIG) except src.GraphQLError as exception: diff --git a/uv.lock b/uv.lock index 22b8b80..e336024 100644 --- a/uv.lock +++ b/uv.lock @@ -560,8 +560,8 @@ dev = [ [[package]] name = "src-py-lib" -version = "0.1.10.dev2+g37c970897" -source = { git = "https://github.com/sourcegraph/src-py-lib.git?rev=amp%2Fotel-standard-instrumentation#37c970897e832ee71c55b019a0a9fb296d305f94" } +version = "0.1.10.dev5+g1c6337441" +source = { git = "https://github.com/sourcegraph/src-py-lib.git?rev=amp%2Fotel-standard-instrumentation#1c633744169aba16fa629ae4e65144e6a53a886b" } dependencies = [ { name = "httpx" }, { name = "opentelemetry-api" }, From 8497e6016ca872244a4782fa09cce85bdcd39423 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:23:59 -0600 Subject: [PATCH 3/6] Remove PyPI release environment approval Amp-Thread-ID: https://ampcode.com/threads/T-019eaab7-5224-7118-8bd5-090093ba01d3 Co-authored-by: Amp --- .github/workflows/release.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 43ca985..b25e2a5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -455,9 +455,6 @@ jobs: permissions: contents: read id-token: write - environment: - name: pypi - url: https://pypi.org/project/src-auth-perms-sync/ steps: - name: Download built distribution From 420796f1fdc3b91dcd14dfdd1e7fcbaad1c7a668 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:30:35 -0600 Subject: [PATCH 4/6] Restore PyPI release environment approval Amp-Thread-ID: https://ampcode.com/threads/T-019eaab7-5224-7118-8bd5-090093ba01d3 Co-authored-by: Amp --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b25e2a5..43ca985 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -455,6 +455,9 @@ jobs: permissions: contents: read id-token: write + environment: + name: pypi + url: https://pypi.org/project/src-auth-perms-sync/ steps: - name: Download built distribution From bfd0fe4b3a47680ffe9b712ac26713bbcc66bd52 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:32:56 -0600 Subject: [PATCH 5/6] Use src-py-lib 0.2.1 from PyPI Amp-Thread-ID: https://ampcode.com/threads/T-019eaab7-5224-7118-8bd5-090093ba01d3 Co-authored-by: Amp --- pyproject.toml | 5 +---- uv.lock | 10 +++++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3e21f1e..f8b459a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ classifiers = [ dependencies = [ "json5>=0.14.0", "pyyaml>=6.0.3", - "src-py-lib[otel]", + "src-py-lib[otel]==0.2.1", ] keywords = [ "Sourcegraph" @@ -67,6 +67,3 @@ select = ["E", "F", "I", "B", "UP", "SIM"] [tool.uv] package = true - -[tool.uv.sources] -src-py-lib = { git = "https://github.com/sourcegraph/src-py-lib.git", rev = "amp/otel-standard-instrumentation" } diff --git a/uv.lock b/uv.lock index e336024..a665bea 100644 --- a/uv.lock +++ b/uv.lock @@ -548,7 +548,7 @@ dev = [ requires-dist = [ { name = "json5", specifier = ">=0.14.0" }, { name = "pyyaml", specifier = ">=6.0.3" }, - { name = "src-py-lib", extras = ["otel"], git = "https://github.com/sourcegraph/src-py-lib.git?rev=amp%2Fotel-standard-instrumentation" }, + { name = "src-py-lib", extras = ["otel"], specifier = "==0.2.1" }, ] [package.metadata.requires-dev] @@ -560,14 +560,18 @@ dev = [ [[package]] name = "src-py-lib" -version = "0.1.10.dev5+g1c6337441" -source = { git = "https://github.com/sourcegraph/src-py-lib.git?rev=amp%2Fotel-standard-instrumentation#1c633744169aba16fa629ae4e65144e6a53a886b" } +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, { name = "opentelemetry-api" }, { name = "pydantic" }, { name = "python-dotenv" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/31/62/6bfbc05c31522ebabe054175505d833516ecb5a52e46776fabb37629f45e/src_py_lib-0.2.1.tar.gz", hash = "sha256:ad1b4ea92245cc9b144f48dd8a985003309fd0ee028ff2d25e55f73647abdd78", size = 88498, upload-time = "2026-06-09T06:26:57.789Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/b8/ab4433e688deeb321aa992c8098681f2eefe3b705d0f52ebd2186c823596/src_py_lib-0.2.1-py3-none-any.whl", hash = "sha256:d1f736717b4d559974ee61f89c0dff360d33064850d27ec07bfbd41495baa198", size = 49749, upload-time = "2026-06-09T06:26:56.476Z" }, +] [package.optional-dependencies] otel = [ From 898977d504f1ad8c0d8ec08894a52ccb17ef1c2c Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Tue, 9 Jun 2026 01:16:11 -0600 Subject: [PATCH 6/6] Update TODO --- dev/TODO.md | 68 ++++++++++++++++++++++------------------------------- 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/dev/TODO.md b/dev/TODO.md index 73ba9a8..72d9cb7 100644 --- a/dev/TODO.md +++ b/dev/TODO.md @@ -1,25 +1,33 @@ # TODO -## High priority: Instrument with OpenTelemetry — in progress - -- Add OTel-native traces, metrics, and wide log events in `src-py-lib` - - Would we gain anything by auto-instrumenting httpx? - - Need to keep/wrap custom/sanitized fields like request/response bytes, - redacted headers, retry counts, Sourcegraph metadata - - Keep our custom GraphQL - - Would we lose anything by switching to opentelemetry-instrumentation-system-metrics? - - Investigate opentelemetry-instrumentation-threading -- Replace custom trace-context propagation with OTel W3C propagation -- Add shared OTel bootstrap config/helpers with `--otel` and standard `OTEL_*` args -- Wire `src-auth-perms-sync` to the shared OTel bootstrap without doing import-time logger/provider setup -- Verify pyright, tests, and CLI help in both repos - -## High priority: Replace temporary src-py-lib branch source after release - -- Merge and release the `src-py-lib` OpenTelemetry branch. -- Replace the `src-auth-perms-sync` `tool.uv.sources.src-py-lib` branch source - with the released `src-py-lib[otel]` version. -- Re-run pyright, unit tests, and CLI help after the dependency is pinned. +## High priority: Sync modes + +### Fast + +- Additive modes, to add new users’ perms quickly, + without the extraneous load on the database of a full sync +- Take a list of usernames and/or email addresses as input, + query users on the instance for these, + then trigger a perms sync for found users +- Query the instance for all new users, which do not yet have explicit perms +- Query the instance for all new repos, which do not yet have explicit perms + +### Full: Overwrite all perms + +- Separate full sync mode with an arg + +## High priority: Remote trigger on demand + +- Sourcegraph webhook for new user coming in v7.4.0 +- Requested a webhook for new repos +- Receive the webhook event +- Parse the new user / repo name +- Run a lightweight sync for the changed user / repo + +- Where does this run? Sidecar in the customer's environment? CI job? + Sourcegraph executor? +- How do we avoid stampedes (e.g., bulk repo sync triggering thousands + of re-runs)? ## High priority: End to End test cases @@ -46,26 +54,6 @@ replace aliased `User.permissionsInfo.repositories(source: API)` calls before raising concurrency further. -## Medium priority: Lightweight incremental updates - -- When a new user's account is created, or a new repo is synced from a code host, - they sit outside the mapping until this script runs again -- Find ways to create lightweight run modes to check for new users / repos, - and create the needed perms for them - -## Medium priority: Remote trigger on demand - -- Sourcegraph webhook for new user coming in v7.4.0 -- Requested a webhook for new repos -- Receive the webhook event -- Parse the new user / repo name -- Run a lightweight sync for the changed user / repo - -- Where does this run? Sidecar in the customer's environment? CI job? - Sourcegraph executor? -- How do we avoid stampedes (e.g., bulk repo sync triggering thousands - of re-runs)? - ## Low priority: Repo-centric path, when users > repos, or for cross-checking We previously had a repo-centric capture path