diff --git a/.codespellrc b/.codespellrc index afe8ce0a9..f1f293d32 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,4 +1,4 @@ [codespell] # skipping auto generated folders -skip = ./.tox,./.mypy_cache,./target,*/LICENSE,./venv,*/sql_dialect_keywords.json -ignore-words-list = afterall,assertIn, crate \ No newline at end of file +skip = ./.tox,./.mypy_cache,./target,*/LICENSE,./venv,*/sql_dialect_keywords.json,*/3rd.txt +ignore-words-list = afterall,assertIn, crate diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py index 1eb183c3c..0eabca5e7 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py @@ -473,6 +473,13 @@ def _customize_logs_exporter(log_exporter: LogExporter) -> LogExporter: def _customize_span_processors(provider: TracerProvider, resource: Resource) -> None: + + if get_code_correlation_enabled_status() is True: + # pylint: disable=import-outside-toplevel + from amazon.opentelemetry.distro.code_correlation import CodeAttributesSpanProcessor + + provider.add_span_processor(CodeAttributesSpanProcessor()) + # Add LambdaSpanProcessor to list of processors regardless of application signals. if _is_lambda_environment(): provider.add_span_processor(AwsLambdaSpanProcessor()) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/__init__.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/__init__.py index 73e58d4df..3467eeed7 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/__init__.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/__init__.py @@ -6,123 +6,31 @@ This module provides functionality for correlating code execution with telemetry data. """ - -import inspect -from functools import wraps -from typing import Any, Callable - -from opentelemetry import trace - +# Import code attributes span processor +from .code_attributes_span_processor import CodeAttributesSpanProcessor + +# Import main utilities to maintain API compatibility +from .utils import ( + add_code_attributes_to_span, + add_code_attributes_to_span_from_frame, + get_callable_fullname, + get_function_fullname_from_frame, + record_code_attributes, +) + +# Version information __version__ = "1.0.0" - -# Code correlation attribute constants -CODE_FUNCTION_NAME = "code.function.name" -CODE_FILE_PATH = "code.file.path" -CODE_LINE_NUMBER = "code.line.number" - - -def add_code_attributes_to_span(span, func_or_class: Callable[..., Any]) -> None: - """ - Add code-related attributes to a span based on a Python function. - - This utility method extracts function metadata and adds the following - span attributes: - - CODE_FUNCTION_NAME: The name of the function - - CODE_FILE_PATH: The file path where the function is defined - - CODE_LINE_NUMBER: The line number where the function is defined - - Args: - span: The OpenTelemetry span to add attributes to - func: The Python function to extract metadata from - """ - if not span.is_recording(): - return - - try: - # Check if it's a class first, with proper exception handling - try: - is_class = inspect.isclass(func_or_class) - except Exception: # pylint: disable=broad-exception-caught - # If inspect.isclass fails, we can't safely determine the type, so return early - return - - if is_class: - span.set_attribute(CODE_FUNCTION_NAME, f"{func_or_class.__module__}.{func_or_class.__qualname__}") - span.set_attribute(CODE_FILE_PATH, inspect.getfile(func_or_class)) - else: - code = getattr(func_or_class, "__code__", None) - if code: - span.set_attribute(CODE_FUNCTION_NAME, f"{func_or_class.__module__}.{func_or_class.__qualname__}") - span.set_attribute(CODE_FILE_PATH, code.co_filename) - span.set_attribute(CODE_LINE_NUMBER, code.co_firstlineno) - except Exception: # pylint: disable=broad-exception-caught - pass - - -def record_code_attributes(func: Callable[..., Any]) -> Callable[..., Any]: - """ - Decorator to automatically add code attributes to the current OpenTelemetry span. - - This decorator extracts metadata from the decorated function and adds it as - attributes to the current active span. The attributes added are: - - code.function.name: The name of the function - - code.file.path: The file path where the function is defined - - code.line.number: The line number where the function is defined - - This decorator supports both synchronous and asynchronous functions. - - Usage: - @record_code_attributes - def my_sync_function(): - # Sync function implementation - pass - - @record_code_attributes - async def my_async_function(): - # Async function implementation - pass - - Args: - func: The function to be decorated - - Returns: - The wrapped function with current span code attributes tracing - """ - # Detect async functions - is_async = inspect.iscoroutinefunction(func) - - if is_async: - # Async function wrapper - @wraps(func) - async def async_wrapper(*args, **kwargs): - # Add code attributes to current span - try: - current_span = trace.get_current_span() - if current_span: - add_code_attributes_to_span(current_span, func) - except Exception: # pylint: disable=broad-exception-caught - # Silently handle any unexpected errors - pass - - # Call and await the original async function - return await func(*args, **kwargs) - - return async_wrapper - - # Sync function wrapper - @wraps(func) - def sync_wrapper(*args, **kwargs): - # Add code attributes to current span - try: - current_span = trace.get_current_span() - if current_span: - add_code_attributes_to_span(current_span, func) - except Exception: # pylint: disable=broad-exception-caught - # Silently handle any unexpected errors - pass - - # Call the original sync function - return func(*args, **kwargs) - - return sync_wrapper +# Define public API +__all__ = [ + # Functions + "add_code_attributes_to_span", + "add_code_attributes_to_span_from_frame", + "get_callable_fullname", + "get_function_fullname_from_frame", + "record_code_attributes", + # Classes + "CodeAttributesSpanProcessor", + # Version + "__version__", +] diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/code_attributes_span_processor.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/code_attributes_span_processor.py new file mode 100644 index 000000000..328987330 --- /dev/null +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/code_attributes_span_processor.py @@ -0,0 +1,117 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Code Attributes Span Processor implementation for OpenTelemetry Python. + +This processor captures stack traces and attaches them to spans as attributes. +It's based on the OpenTelemetry Java contrib StackTraceSpanProcessor. +""" + +import sys +import typing as t +from types import FrameType +from typing import Optional + +from opentelemetry.context import Context +from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor +from opentelemetry.semconv.attributes.code_attributes import CODE_FUNCTION_NAME +from opentelemetry.trace import SpanKind + +from .internal.packages_resolver import _build_package_mapping, _load_third_party_packages, is_user_code +from .utils import add_code_attributes_to_span_from_frame + + +class CodeAttributesSpanProcessor(SpanProcessor): + """ + A SpanProcessor that captures and attaches code attributes to spans. + + This processor adds stack trace information as span attributes, which can be + useful for debugging and understanding the call flow that led to span creation. + """ + + # Maximum number of stack frames to examine + MAX_STACK_FRAMES = 50 + + @staticmethod + def _iter_stack_frames(frame: FrameType) -> t.Iterator[FrameType]: + """Iterate through stack frames starting from the given frame.""" + _frame: t.Optional[FrameType] = frame + while _frame is not None: + yield _frame + _frame = _frame.f_back + + def __init__(self): + """Initialize the CodeAttributesSpanProcessor.""" + # Pre-initialize expensive operations to avoid runtime performance overhead + # These @execute_once methods are slow, so we call them during initialization + # to cache their results ahead of time + _build_package_mapping() + _load_third_party_packages() + + def on_start( + self, + span: Span, + parent_context: Optional[Context] = None, + ) -> None: + """ + Called when a span is started. Captures and attaches code attributes from stack trace. + + Args: + span: The span that was started + parent_context: The parent context (unused) + """ + # Skip if span should not be processed + if not self._should_process_span(span): + return + + # Capture code attributes from stack trace + self._capture_code_attributes(span) + + @staticmethod + def _should_process_span(span: Span) -> bool: + """ + Determine if span should be processed for code attributes. + + Returns False if: + - Span already has code attributes + - Span is SERVER or INTERNAL span + """ + # Skip if span already has code attributes + if span.attributes is not None and CODE_FUNCTION_NAME in span.attributes: + return False + + # Process spans except SERVER and INTERNAL spans + return span.kind not in (SpanKind.SERVER, SpanKind.INTERNAL) + + def _capture_code_attributes(self, span: Span) -> None: + """Capture and attach code attributes from current stack trace.""" + try: + current_frame = sys._getframe(1) + + for frame_index, frame in enumerate(self._iter_stack_frames(current_frame)): + if frame_index >= self.MAX_STACK_FRAMES: + break + + code = frame.f_code + + if is_user_code(code.co_filename): + add_code_attributes_to_span_from_frame(frame, span) + break # Only capture the first user code frame + + except (OSError, ValueError): + # sys._getframe may not be available on all platforms + pass + + def on_end(self, span: ReadableSpan) -> None: + """ + Called when a span is ended. Captures and attaches stack trace if conditions are met. + """ + + def shutdown(self) -> None: + """Called when the processor is shutdown. No cleanup needed.""" + # No cleanup needed for code attributes processor + + def force_flush(self, timeout_millis: int = 30000) -> bool: # pylint: disable=no-self-use,unused-argument + """Force flush any pending spans. Always returns True as no pending work.""" + return True diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/config.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/config.py new file mode 100644 index 000000000..5271a0fe6 --- /dev/null +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/config.py @@ -0,0 +1,175 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Configuration management for AWS OpenTelemetry code correlation features. + +This module provides a configuration class that handles environment variable +parsing for code correlation settings, including package inclusion/exclusion +rules and stack depth configuration. +""" + +import json +import logging +import os +from typing import Any, Dict, List, Optional + +# Environment variable constants +_ENV_CONFIG = "OTEL_AWS_CODE_CORRELATION_CONFIG" + +_logger = logging.getLogger(__name__) + + +class AwsCodeCorrelationConfig: + """ + Configuration manager for AWS OpenTelemetry code correlation features. + + This class encapsulates the parsing of environment variables that control + code correlation behavior, including package inclusion/exclusion lists + and stack trace depth configuration. + + Environment Variables: + OTEL_AWS_CODE_CORRELATION_CONFIG: JSON configuration with detailed settings + + Example Configuration: + export OTEL_AWS_CODE_CORRELATION_CONFIG='{ + "include": ["myapp", "mylib"], + "exclude": ["third-party", "vendor"], + "stack_depth": 5 + }' + """ + + def __init__( + self, include: Optional[List[str]] = None, exclude: Optional[List[str]] = None, stack_depth: int = 0 + ) -> None: + """ + Initialize the configuration object. + + Args: + include: List of package names to include (default: empty list) + exclude: List of package names to exclude (default: empty list) + stack_depth: Maximum stack trace depth (default: 0, meaning unlimited) + """ + self.include = include or [] + self.exclude = exclude or [] + self.stack_depth = stack_depth + + @classmethod + def from_env(cls) -> "AwsCodeCorrelationConfig": + """ + Create configuration instance from environment variables. + + Returns: + AwsCodeCorrelationConfig: Configured instance + """ + config_data = cls._parse_config_data() + include_value = cls._validate_string_list(config_data, "include") + exclude_value = cls._validate_string_list(config_data, "exclude") + stack_depth_value = cls._validate_stack_depth(config_data) + + return cls( + include=include_value, + exclude=exclude_value, + stack_depth=stack_depth_value, + ) + + @classmethod + def _parse_config_data(cls) -> Dict[str, Any]: + """Parse configuration data from environment variable.""" + config_str = os.getenv(_ENV_CONFIG, "{}").strip() + if not config_str: + config_str = "{}" + + try: + config_data = json.loads(config_str) + except json.JSONDecodeError as json_error: + _logger.warning("Invalid JSON in %s: %s. Using empty configuration.", _ENV_CONFIG, json_error) + return {} + + if not isinstance(config_data, dict): + _logger.warning( + "Configuration in %s must be a JSON object, got %s. Using empty configuration.", + _ENV_CONFIG, + type(config_data).__name__, + ) + return {} + + return config_data + + @classmethod + def _validate_string_list(cls, config_data: Dict[str, Any], field_name: str) -> List[str]: + """Validate and extract a string list from config data.""" + field_value = config_data.get(field_name, []) + if not isinstance(field_value, list): + _logger.warning( + "Configuration '%s' in %s must be a list, got %s. Using empty list.", + field_name, + _ENV_CONFIG, + type(field_value).__name__, + ) + return [] + + validated_list = [] + for item in field_value: + if isinstance(item, str): + validated_list.append(item) + else: + _logger.warning( + "Configuration '%s' list item in %s must be a string, got %s. Skipping item.", + field_name, + _ENV_CONFIG, + type(item).__name__, + ) + return validated_list + + @classmethod + def _validate_stack_depth(cls, config_data: Dict[str, Any]) -> int: + """Validate and extract stack depth from config data.""" + stack_depth_value = config_data.get("stack_depth", 0) + if not isinstance(stack_depth_value, int): + _logger.warning( + "Configuration 'stack_depth' in %s must be an integer, got %s. Using default value 0.", + _ENV_CONFIG, + type(stack_depth_value).__name__, + ) + return 0 + + if stack_depth_value < 0: + _logger.warning( + "Configuration 'stack_depth' in %s must be non-negative, got %s. Using default value 0.", + _ENV_CONFIG, + stack_depth_value, + ) + return 0 + + return stack_depth_value + + def to_dict(self) -> Dict[str, Any]: + """ + Export configuration as a dictionary. + + Returns: + Dict[str, Any]: Configuration dictionary + """ + return {"include": self.include, "exclude": self.exclude, "stack_depth": self.stack_depth} + + def to_json(self, indent: Optional[int] = 2) -> str: + """ + Export configuration as a JSON string. + + Args: + indent: JSON indentation level (None for compact format) + + Returns: + str: JSON representation of the configuration + """ + return json.dumps(self.to_dict(), indent=indent) + + def __repr__(self) -> str: + """Return string representation of the configuration.""" + return ( + f"AwsCodeCorrelationConfig(" + f"include={self.include}, " + f"exclude={self.exclude}, " + f"stack_depth={self.stack_depth})" + ) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/internal/3rd.txt b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/internal/3rd.txt new file mode 100644 index 000000000..0a5711973 --- /dev/null +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/internal/3rd.txt @@ -0,0 +1,6000 @@ +boto3 +botocore +urllib3 +typing-extensions +requests +certifi +charset-normalizer +aiobotocore +idna +grpcio-status +setuptools +packaging +python-dateutil +s3transfer +six +s3fs +numpy +pyyaml +cryptography +fsspec +pydantic +cffi +pandas +pycparser +attrs +click +pip +protobuf +platformdirs +jmespath +markupsafe +pydantic-core +rsa +pytz +jinja2 +h11 +anyio +google-auth +importlib-metadata +sniffio +pyasn1 +zipp +pluggy +annotated-types +pygments +cachetools +wheel +google-api-core +filelock +colorama +awscli +httpx +httpcore +jsonschema +tzdata +pyasn1-modules +pyjwt +aiohttp +pytest +virtualenv +googleapis-common-protos +multidict +python-dotenv +yarl +wrapt +pyarrow +typing-inspection +iniconfig +tqdm +requests-oauthlib +rpds-py +frozenlist +aiosignal +jsonschema-specifications +sqlalchemy +beautifulsoup4 +soupsieve +greenlet +tomli +psutil +scipy +propcache +pyparsing +lxml +referencing +rich +opentelemetry-semantic-conventions +grpcio +pathspec +pillow +tomlkit +oauthlib +markdown-it-py +aiohappyeyeballs +openpyxl +et-xmlfile +mdurl +distlib +exceptiongroup +grpcio-tools +requests-toolbelt +trove-classifiers +docutils +more-itertools +yandexcloud +pyopenssl +snowflake-connector-python +starlette +uvicorn +opentelemetry-sdk +werkzeug +isodate +opentelemetry-proto +regex +proto-plus +google-cloud-storage +msgpack +mypy-extensions +opentelemetry-api +flask +websocket-client +shellingham +decorator +psycopg2-binary +pynacl +fastapi +tenacity +opentelemetry-instrumentation +sortedcontainers +opentelemetry-exporter-otlp-proto-http +scikit-learn +async-timeout +poetry-core +coverage +azure-core +gitpython +opentelemetry-util-http +msal +asn1crypto +wcwidth +bcrypt +smmap +opentelemetry-exporter-otlp-proto-common +gitdb +opentelemetry-instrumentation-requests +dnspython +networkx +matplotlib +threadpoolctl +pexpect +ptyprocess +google-api-python-client +deprecated +google-cloud-core +azure-identity +joblib +fonttools +chardet +huggingface-hub +keyring +fastjsonschema +build +openai +pyproject-hooks +google-genai +websockets +itsdangerous +paramiko +opentelemetry-exporter-otlp-proto-grpc +zstandard +langsmith +blinker +tabulate +secretstorage +jaraco-classes +google-resumable-media +prompt-toolkit +ruamel-yaml +kiwisolver +jeepney +dill +rapidfuzz +backoff +distro +google-crc32c +defusedxml +transformers +google-auth-oauthlib +python-multipart +cycler +redis +pydantic-settings +uritemplate +google-cloud-bigquery +alembic +httplib2 +prometheus-client +msal-extensions +contourpy +ruff +typer +setuptools-scm +hatchling +xmltodict +ruamel-yaml-clib +google-auth-httplib2 +webencodings +importlib-resources +pytest-cov +jaraco-functools +tzlocal +opentelemetry-exporter-otlp +docker +orjson +py4j +types-requests +marshmallow +awswrangler +tokenizers +sqlparse +babel +ipython +jaraco-context +jiter +nest-asyncio +black +jsonpointer +langchain +typedload +pkginfo +azure-storage-blob +cachecontrol +jedi +cython +mako +parso +traitlets +sentry-sdk +toml +grpc-google-iam-v1 +kubernetes +cloudpickle +pymysql +installer +aliyun-python-sdk-core +tornado +torch +mypy +executing +google-cloud-secret-manager +aiofiles +matplotlib-inline +gunicorn +cloudflare +dulwich +poetry +crashtest +ply +types-python-dateutil +poetry-plugin-export +grpcio-health-checking +sympy +nodeenv +email-validator +asttokens +isort +acme +markdown +certbot-dns-cloudflare +mccabe +cleo +pycodestyle +uv +pyzmq +stack-data +pure-eval +termcolor +langchain-core +pymongo +future +uvloop +sphinx +mpmath +databricks-sdk +identify +pendulum +python-json-logger +pytest-xdist +pre-commit +asgiref +debugpy +smart-open +typing-inspect +cfgv +pytest-asyncio +jsonpatch +execnet +shapely +tiktoken +hf-xet +pycryptodome +watchfiles +jsonpath-ng +tinycss2 +httpx-sse +snowflake-sqlalchemy +msrest +google-cloud-batch +aioitertools +datadog +py +scramp +semver +watchdog +rfc3339-validator +httptools +backports-tarfile +azure-common +editables +arrow +lz4 +notebook +databricks-sql-connector +docstring-parser +redshift-connector +opensearch-py +jupyter-core +google-analytics-admin +pytest-mock +multiprocess +ipykernel +slack-sdk +pyflakes +argcomplete +dataclasses-json +jupyter-client +mistune +pbs-installer +mcp +findpython +pyrsistent +comm +google-cloud-aiplatform +pygithub +google-cloud-pubsub +xlsxwriter +requests-aws4auth +bleach +pyspark +durationpy +nvidia-nccl-cu12 +requests-file +invoke +nbformat +flake8 +text-unidecode +pycryptodomex +mysql-connector-python +zope-interface +jupyterlab +nbconvert +elasticsearch +python-slugify +sse-starlette +jupyter-server +pysocks +cattrs +simplejson +dbt-core +opentelemetry-exporter-prometheus +lark +nbclient +google-cloud-logging +google-cloud-compute +google-cloud-kms +aenum +nvidia-cublas-cu12 +humanfriendly +loguru +google-cloud-vision +argon2-cffi +argon2-cffi-bindings +safetensors +google-cloud-monitoring +h2 +google-cloud-tasks +dbt-adapters +google-cloud-dlp +typeguard +dbt-common +google-cloud-resource-manager +graphql-core +deepdiff +authlib +nvidia-cusparse-cu12 +google-cloud-speech +hyperframe +hpack +triton +wsproto +nvidia-cudnn-cu12 +xlrd +google-cloud-workflows +pg8000 +nvidia-nvjitlink-cu12 +selenium +pandocfilters +psycopg2 +jupyterlab-pygments +narwhals +nvidia-cusolver-cu12 +gcsfs +faker +nvidia-curand-cu12 +brotli +nvidia-cufft-cu12 +google-cloud-language +nvidia-cuda-nvrtc-cu12 +structlog +apache-airflow-providers-common-sql +json5 +google-cloud-dataform +google-cloud-appengine-logging +toolz +snowflake-snowpark-python +google-cloud-videointelligence +overrides +nltk +types-pyyaml +numba +altair +google-cloud-os-login +db-dtypes +html5lib +nvidia-cuda-cupti-cu12 +types-protobuf +confluent-kafka +portalocker +flatbuffers +google-cloud-redis +webcolors +inflection +nvidia-cuda-runtime-cu12 +astroid +croniter +google-pasta +tb-nightly +jupyterlab-server +azure-keyvault-secrets +colorlog +ordered-set +google-cloud-memcache +imageio +absl-py +fqdn +pylint +send2trash +antlr4-python3-runtime +responses +isoduration +plotly +setproctitle +uri-template +gevent +deprecation +llvmlite +xgboost +trio +nvidia-nvtx-cu12 +rfc3986-validator +lazy-object-proxy +kombu +async-lru +click-plugins +google-cloud-bigtable +rfc3986 +h5py +thrift +terminado +seaborn +ipywidgets +aws-lambda-powertools +pyodbc +widgetsnbextension +oauth2client +omegaconf +notebook-shim +xxhash +jupyterlab-widgets +sqlalchemy-bigquery +coloredlogs +delta-spark +progressbar2 +jupyter-events +ecdsa +botocore-stubs +adal +ipython-pygments-lexers +google-cloud-audit-log +litellm +langchain-community +sshtunnel +types-awscrt +django +tf-keras-nightly +opencv-python +jupyter-server-terminals +azure-mgmt-core +langchain-text-splitters +zeep +pipenv +pandas-stubs +datasets +python-utils +pandas-gbq +types-s3transfer +azure-storage-file-datalake +jupyter-lsp +outcome +retry +celery +pymssql +google-cloud-bigquery-datatransfer +sentencepiece +schema +playwright +fastavro +nvidia-cusparselt-cu12 +humanize +anthropic +langchain-openai +tensorboard +appdirs +pycountry +pytzdata +google-cloud-texttospeech +google-cloud-automl +sagemaker +ujson +tensorflow +google-cloud-dataproc +google-cloud-orchestration-airflow +billiard +gremlinpython +google-cloud-dataproc-metastore +amqp +trio-websocket +vine +pkgutil-resolve-name +google-ads +boto3-stubs +asyncpg +gspread +apache-beam +click-didyoumean +gcloud-aio-storage +rich-toolkit +nh3 +google-cloud-bigquery-storage +pyee +polars +pbr +freezegun +events +mock +pydata-google-auth +sendgrid +pathos +click-repl +tox +google-cloud-run +libcst +oss2 +opentelemetry-instrumentation-asgi +graphviz +types-pytz +psycopg +graphene +graphql-relay +langchain-google-vertexai +google-cloud-dataflow-client +mlflow +snowballstemmer +fastapi-cli +pox +entrypoints +ppft +opentelemetry-instrumentation-fastapi +markdownify +simple-salesforce +zope-event +mlflow-skinny +google-cloud-spanner +great-expectations +pywin32 +tblib +flask-cors +statsmodels +oscrypto +torchvision +smdebug-rulesconfig +retrying +mashumaro +duckdb +mergedeep +aws-requests-auth +readme-renderer +semantic-version +rfc3987-syntax +dacite +databricks-sqlalchemy +lockfile +ijson +hvac +agate +azure-cosmos +pytimeparse +ray +patsy +wandb +python-telegram-bot +tableauserverclient +prettytable +unidecode +ninja +moto +onnxruntime +azure-datalake-store +twine +pytest-metadata +pypdf +kafka-python +astronomer-cosmos +elastic-transport +bracex +yamllint +pybind11 +cron-descriptor +nvidia-cufile-cu12 +openapi-spec-validator +aiosqlite +sqlglot +gcloud-aio-auth +gcloud-aio-bigquery +cssselect2 +alabaster +pytest-timeout +ml-dtypes +looker-sdk +imagesize +sphinxcontrib-serializinghtml +pytest-rerunfailures +flask-appbuilder +pip-tools +mypy-boto3-s3 +cramjam +mdit-py-plugins +parsedatetime +msrestazure +hypothesis +dateparser +python-http-client +orderly-set +userpath +gast +parse +python-jose +jira +google-cloud-firestore +psycopg-binary +resolvelib +mysqlclient +azure-mgmt-resource +holidays +pypdf2 +sphinxcontrib-htmlhelp +apache-airflow-providers-cncf-kubernetes +pymupdf +sphinxcontrib-applehelp +semgrep +sphinxcontrib-qthelp +sphinxcontrib-devhelp +types-setuptools +bs4 +thriftpy2 +google-cloud-datacatalog +ddtrace +opencv-python-headless +leather +dask +openapi-schema-validator +apache-airflow-providers-snowflake +scikit-image +tensorboard-data-server +mmh3 +dbt-extractor +sphinxcontrib-jsmath +apache-airflow-providers-databricks +validators +google-cloud-translate +apache-airflow-providers-fab +google-cloud-container +opt-einsum +sqlalchemy-utils +texttable +python-magic +types-urllib3 +wcmatch +cached-property +tldextract +linkify-it-py +google-cloud-dataplex +flit-core +docker-pycreds +google-cloud-build +argparse +pyphen +pathable +dbt-semantic-interfaces +pydeequ +id +flask-sqlalchemy +hyperlink +opentelemetry-instrumentation-urllib3 +imbalanced-learn +requests-mock +bytecode +tomli-w +phonenumbers +jpype1 +uc-micro-py +grpcio-gcp +diskcache +stevedore +flask-login +streamlit +sentence-transformers +jsonpickle +azure-mgmt-storage +pytest-runner +posthog +grpc-interceptor +click-option-group +fastmcp +opencensus +azure-storage-queue +keras +inflect +envier +limits +universal-pathlib +jsonschema-path +azure-batch +opencensus-context +apache-airflow-providers-ssh +apache-airflow-providers-mysql +llama-parse +djangorestframework +python-docx +weasyprint +makefun +oracledb +bitarray +opentelemetry-instrumentation-wsgi +docopt +spacy +google-cloud-storage-transfer +apache-airflow-providers-google +python-daemon +pickleshare +bashlex +rich-click +weaviate-client +pyathena +accelerate +fasteners +pyright +msgspec +sqlalchemy-spanner +cloudpathlib +watchtower +protego +impyla +thinc +apscheduler +passlib +backcall +apache-airflow +aws-xray-sdk +statsd +filetype +py-cpuinfo +levenshtein +pyroaring +blis +dbt-protos +libclang +pyperclip +llama-cloud-services +flask-wtf +pyspnego +pydeck +apache-airflow-providers-sqlite +databricks-cli +asynctest +marisa-trie +time-machine +stripe +azure-servicebus +lazy-loader +readabilipy +datadog-api-client +wtforms +astunparse +apispec +boltons +pydot +griffe +langcodes +parameterized +jaydebeapi +aioboto3 +opentelemetry-instrumentation-dbapi +cymem +murmurhash +catalogue +uuid6 +srsly +pyproj +cssselect +zopfli +reportlab +emoji +pyproject-api +fuzzywuzzy +jsonref +pinotdb +fastparquet +fastapi-cloud-cli +azure-keyvault-keys +opentelemetry-instrumentation-logging +hydra-core +pytokens +frozendict +pydyf +pytest-html +cfn-lint +tifffile +opentelemetry-distro +wasabi +peewee +cmake +keyrings-google-artifactregistry-auth +mcp-server-fetch +azure-mgmt-containerregistry +einops +eval-type-backport +opentelemetry-instrumentation-urllib +natsort +flask-limiter +immutabledict +preshed +daff +python-gnupg +python-gitlab +fire +textual +unstructured-client +rich-argparse +opentelemetry-instrumentation-django +rignore +ciso8601 +thrift-sasl +language-data +kubernetes-asyncio +ua-parser +opentelemetry-instrumentation-flask +pdfminer-six +office365-rest-python-client +spacy-legacy +google-ai-generativelanguage +pytorch-lightning +azure-keyvault-certificates +jwcrypto +spacy-loggers +django-cors-headers +glom +azure-mgmt-cosmosdb +face +maxminddb +pymdown-extensions +confection +jupyter-console +pyotp +pydantic-extra-types +scp +tensorflow-estimator +jupyter +types-toml +cachelib +avro +jax +azure-mgmt-containerinstance +astor +aiohttp-retry +ipdb +datetime +truststore +pymilvus +google-generativeai +pysftp +xarray +curl-cffi +sh +opentelemetry-instrumentation-psycopg2 +pytest-env +langgraph +aws-sam-translator +kfp +azure-storage-file-share +socksio +azure-nspkg +configparser +geoip2 +azure-data-tables +bidict +azure-mgmt-containerservice +factory-boy +ratelimit +configargparse +tinyhtml5 +lightgbm +pydub +onnx +types-redis +hatch +azure-storage-common +azure-kusto-data +torchaudio +snowplow-tracker +sphinx-rtd-theme +partd +pywavelets +pywin32-ctypes +trino +jaxlib +types-pyopenssl +gql +py-spy +jsondiff +azure-mgmt-datalake-store +mypy-boto3-rds +ibmcloudant +types-cffi +apache-airflow-providers-http +monotonic +microsoft-kiota-authentication-azure +twilio +faiss-cpu +geopandas +bandit +mkdocstrings-python +mkdocs-material +html2text +torchmetrics +avro-python3 +pytest-django +requests-ntlm +swe-rex +locket +pinecone +python-levenshtein +atlassian-python-api +clickhouse-connect +azure-mgmt-compute +microsoft-kiota-http +strenum +python-pptx +types-aiofiles +types-paramiko +python-engineio +av +azure-synapse-artifacts +tree-sitter +dataclasses +wirerope +junitparser +python-socketio +applicationinsights +webdriver-manager +hiredis +shap +strictyaml +blessed +ansible-core +pycares +hishel +aiohttp-cors +azure-monitor-opentelemetry-exporter +mkdocs +asyncio +timm +html5validator +opentelemetry-instrumentation-threading +soundfile +azure-mgmt-keyvault +colorful +methodtools +gradio +azure-mgmt-datafactory +fabric +weasel +asyncssh +pytest-split +azure-mgmt-msi +simple-websocket +sagemaker-core +unearth +cmdstanpy +pycrypto +langgraph-sdk +slicer +ormsgpack +azure-mgmt-authorization +cohere +deltalake +pyiceberg +types-deprecated +marshmallow-sqlalchemy +azure-synapse-spark +geographiclib +pyyaml-env-tag +diff-cover +types-certifi +ghp-import +flask-jwt-extended +meson +dep-logic +eventlet +altgraph +flit +yapf +pyserial +flask-babel +pgvector +flask-session +netaddr +appnope +adlfs +databricks-connect +pydantic-ai-slim +incremental +geopy +ansible +google-re2 +pdm +ibm-cloud-sdk-core +langgraph-checkpoint +numexpr +inputimeout +django-filter +motor +azure-keyvault +segment-analytics-python +readchar +pipx +ua-parser-builtins +blobfile +jellyfish +lightning-utilities +py-partiql-parser +checkov +kazoo +aiodns +pika +apache-airflow-providers-slack +xmlsec +mkdocs-material-extensions +types-tabulate +microsoft-kiota-abstractions +protobuf3-to-dict +singer-sdk +pypdfium2 +pyelftools +openapi-pydantic +firebase-admin +prison +opencensus-ext-azure +genson +types-markdown +temporalio +sphinxcontrib-jquery +binaryornot +fakeredis +llama-index-core +connexion +flower +azure-monitor-query +sqlalchemy-jsonfield +django-extensions +nose +cfn-flip +amazon-ion +types-docutils +qrcode +azure-appconfiguration +iso8601 +openlineage-python +twisted +qdrant-client +aioresponses +optree +pydash +langdetect +contextlib2 +ftfy +databricks-labs-blueprint +questionary +microsoft-kiota-serialization-text +testcontainers +aniso8601 +cyclopts +azure-mgmt-monitor +pytest-json-report +pyhive +openlineage-integration-common +azure-mgmt-web +microsoft-kiota-serialization-json +locust +pytest-random-order +oci +enum34 +apache-airflow-providers-common-compat +mkdocs-get-deps +azure-mgmt-redis +msgraph-core +chromadb +mypy-boto3-sqs +ipython-genutils +junit-xml +feedparser +pure-sasl +langgraph-prebuilt +azure-mgmt-rdbms +azure-eventhub +boto +patchelf +azure-mgmt-sql +pyarrow-hotfix +types-cachetools +tensorflow-io-gcs-filesystem +psycopg-pool +minio +user-agents +protovalidate +azure-mgmt-trafficmanager +meson-python +azure-mgmt-loganalytics +autopep8 +azure-mgmt-servicebus +prek +pooch +backports-zoneinfo +fixedint +pydantic-ai +std-uritemplate +google-cloud-datastore +paginate +knack +azure-mgmt-eventhub +azure-mgmt-managementgroups +apache-airflow-providers-ftp +diagrams +flask-caching +azure-mgmt-cdn +cloudevents +json-repair +azure-mgmt-cognitiveservices +datamodel-code-generator +azure-mgmt-batch +typing +azure-mgmt-search +optuna +django-redis +slackclient +tensorflow-serving-api +crcmod +pymsteams +python-snappy +rich-rst +azure-mgmt-recoveryservices +azure-mgmt-marketplaceordering +azure-mgmt-recoveryservicesbackup +opentelemetry-instrumentation-httpx +waitress +h3 +elasticsearch-dsl +apache-airflow-providers-smtp +azure-mgmt-iothub +fastuuid +ultralytics +azure-mgmt-eventgrid +azure-mgmt-applicationinsights +distributed +pprintpp +azure-mgmt-advisor +namex +pinecone-plugin-assistant +azure-mgmt-policyinsights +ldap3 +jsonlines +azure-mgmt-billing +beartype +azure-mgmt-iothubprovisioningservices +awscrt +azure-mgmt-servicefabric +azure-mgmt-media +pyproject-metadata +schedule +smbprotocol +opentelemetry-instrumentation-botocore +azure-mgmt-batchai +types-boto3 +prophet +azure-mgmt-datamigration +teradatasql +azure-mgmt-maps +opentelemetry-instrumentation-grpc +azure-mgmt-iotcentral +azure-mgmt-signalr +opentelemetry-instrumentation-redis +bottle +openapi-core +apache-airflow-providers-common-io +pmdarima +ddsketch +aiomysql +pybase64 +pandera +ansible-lint +clickclick +requirements-parser +dask-expr +unidiff +fs +python-jenkins +curlify +pdfplumber +azure-monitor-opentelemetry +hatch-vcs +opentelemetry-instrumentation-sqlalchemy +automat +aws-cdk-asset-awscli-v1 +simpleeval +pep517 +rdflib +azure-cli-core +roman-numerals-py +azure-mgmt-nspkg +django-storages +pdf2image +backports-asyncio-runner +pyrfc3339 +pydantic-graph +license-expression +prefect +unicodecsv +types-croniter +whitenoise +pathvalidate +flaky +geomet +mypy-boto3-dynamodb +types-pymysql +service-identity +purecloudplatformclientv2 +apache-airflow-providers-imap +pathlib +mypy-protobuf +google-apitools +logbook +boolean-py +cligj +joserfc +gradio-client +dictdiffer +olefile +elementpath +sqlmodel +azure-mgmt-datalake-nspkg +python-decouple +langchain-aws +pyaml +azure-core-tracing-opentelemetry +mypy-boto3-lambda +pgpy +constantly +python3-saml +geventhttpclient +oldest-supported-numpy +dotenv +stringcase +peft +a2wsgi +querystring-parser +shortuuid +typed-ast +apache-airflow-providers-docker +stanio +pyogrio +biopython +llama-index-indices-managed-llama-cloud +opsgenie-sdk +dpath +javaproperties +nvidia-ml-py +azure-mgmt-privatedns +backrefs +yfinance +cbor2 +constructs +pytest-forked +python3-openid +memory-profiler +hdfs +influxdb-client +parse-type +inquirer +pyreadline3 +prometheus-fastapi-instrumentator +magicattr +grpclib +configobj +mypy-boto3-sts +keras-applications +pipdeptree +xmlschema +apache-airflow-microsoft-fabric-plugin +ndg-httpsclient +azure-cli +dbt-snowflake +starkbank-ecdsa +opentelemetry-resource-detector-azure +restructuredtext-lint +ipaddress +chevron +opentelemetry-propagator-aws-xray +json-merge-patch +xyzservices +azure-cli-telemetry +pytest-unordered +groq +pyzstd +langchain-google-community +clickhouse-driver +azure-mgmt-apimanagement +tensorflow-text +types-dataclasses +papermill +pytest-localserver +google-cloud-bigquery-biglake +cookiecutter +cssutils +codeowners +memray +neo4j +ollama +albumentations +pyhcl +django-debug-toolbar +airbyte-api +iso3166 +toposort +types-python-slugify +marshmallow-enum +alibabacloud-tea-openapi +minimal-snowplow-tracker +openlineage-sql +pycomposefile +azure-mgmt-hdinsight +backports-strenum +mlflow-tracing +langchain-google-genai +mypy-boto3-appflow +sqlfluff +yt-dlp +azure-multiapi-storage +drf-spectacular +pytest-custom-exit-code +grpcio-reflection +azure-mgmt-security +retryhttp +soxr +asana +librosa +zstd +marshmallow-oneofschema +azure-mgmt-network +azure-mgmt-synapse +pytesseract +facebook-business +orbax-checkpoint +icdiff +xmod +dependency-groups +cassandra-driver +editor +runs +microsoft-security-utilities-secret-masker +pkgconfig +webob +packageurl-python +modal +azure-mgmt-appconfiguration +mypy-boto3-cloudformation +cloudformation-cli +azure-mgmt-appcontainers +llama-index +click-default-group +azure-mgmt-sqlvirtualmachine +azure-synapse-accesscontrol +azure-mgmt-botservice +cloudformation-cli-python-plugin +reactivex +cloudformation-cli-java-plugin +cloudformation-cli-go-plugin +cloudformation-cli-typescript-plugin +azure-mgmt-redhatopenshift +azure-keyvault-administration +circuitbreaker +azure-synapse-managedprivateendpoints +apprise +azure-mgmt-extendedlocation +azure-mgmt-netapp +pip-api +pytest-repeat +azure-mgmt-imagebuilder +azure-mgmt-servicelinker +azure-mgmt-servicefabricmanagedclusters +azure-mgmt-postgresqlflexibleservers +fiona +uvicorn-worker +functions-framework +langfuse +cyclonedx-python-lib +kaleido +jsii +mypy-boto3-secretsmanager +imageio-ffmpeg +macholib +bokeh +azure-mgmt-mysqlflexibleservers +google-cloud-alloydb +cadwyn +mypy-boto3-ec2 +cx-oracle +uritools +mypy-boto3-redshift-data +mypy-boto3-glue +commonmark +biotite +checkdigit +hexbytes +python-on-whales +deepmerge +mixpanel +publication +pinecone-plugin-interface +aws-cdk-integ-tests-alpha +maturin +apache-airflow-providers-airbyte +sphinx-copybutton +py-deviceid +dunamai +fireworks-ai +python-box +pytest-instafail +hatch-fancy-pypi-readme +sphinx-autobuild +scikit-build-core +async-property +vcrpy +qtpy +jsonpath-python +ansicolors +azure-devops +dash +bitsandbytes +sigtools +expiringdict +google-cloud-managedkafka +typeid-python +pytest-icdiff +recordlinkage +sagemaker-studio +pyhumps +opentelemetry-instrumentation-aiohttp-client +unittest-xml-reporting +azure-graphrbac +lxml-html-clean +google-cloud-pubsublite +environs +gensim +pathlib2 +pytest-base-url +addict +ffmpeg-python +mistralai +bottleneck +biotraj +pyxlsb +types-retry +py-serializable +python-keycloak +google +pypika +langchain-anthropic +pyinstaller +convertdate +sqlalchemy-redshift +pytest-benchmark +realtime +lightning +sphinx-design +openlineage-airflow +tokenize-rt +objsize +diffusers +djangorestframework-simplejwt +azure-keyvault-securitydomain +allure-python-commons +mmcif +cel-python +kfp-pipeline-spec +python-bidi +atomicwrites +pyexasol +orderedmultidict +furl +pyinstaller-hooks-contrib +sphinx-autodoc-typehints +pyhmmer +qtconsole +cerberus +sqlparams +arro3-core +kgb +audioread +backports-datetime-fromisoformat +pulumi +ansible-compat +cairosvg +gymnasium +casefy +tensorboardx +azure-mgmt-resource-deploymentstacks +python-hcl2 +python-arango +python-crontab +funcsigs +azure-mgmt-resource-templatespecs +azure-mgmt-resource-deploymentscripts +azure-mgmt-resource-deployments +htmldate +voluptuous +delocate +opencv-contrib-python +supabase +pdbr +thefuzz +pyinstrument +arpeggio +synchronicity +num2words +xformers +storage3 +mkdocstrings +pikepdf +korean-lunar-calendar +svcs +launchdarkly-server-sdk +nexus-rpc +chroma-hnswlib +pytest-playwright +pydocstyle +milvus-lite +py7zr +paho-mqtt +pyzipper +autoflake +xlwt +moviepy +postgrest +pamqp +sql-metadata +aliyun-python-sdk-kms +flask-migrate +ydb +behave +multi-key-dict +types-jsonschema +google-cloud-recommendations-ai +types-html5lib +aio-pika +pip-system-certs +aiormq +myst-parser +allure-pytest +mkdocs-autorefs +farama-notifications +aws-cdk-lib +dbt-spark +django-timezone-field +django-environ +aiokafka +google-analytics-data +pytest-sugar +pywinrm +cairocffi +catboost +w3lib +google-cloud +dbt-postgres +boostedblob +polyfactory +configupdater +azure-kusto-ingest +enum-compat +snowflake-core +sgmllib3k +pytest-socket +instructor +dbt-databricks +unstructured +safety +dataclasses-avroschema +datasketch +tree-sitter-languages +futures +pypng +microsoft-kiota-serialization-multipart +marshmallow-dataclass +nbclassic +url-normalize +anytree +requests-kerberos +microsoft-kiota-serialization-form +parver +parsimonious +ultralytics-thop +vllm +asgi-lifespan +zenpy +multipart +dagster-webserver +pympler +gssapi +django-stubs-ext +swagger-ui-bundle +ffmpy +pynamodb +pyxdg +slack-bolt +dynamodb-json +eth-account +python-socks +pre-commit-uv +apache-airflow-providers-amazon +pyhocon +types-six +uamqp +django-model-utils +setuptools-rust +pathy +pyfakefs +geojson +autograd +fastcore +robotframework +fpdf +scrapbook +pyppmd +types-psutil +python-ldap +vertica-python +yq +markdown2 +kfp-server-api +aiosmtplib +mistral-common +sentinels +tensorflow-metadata +premailer +mongomock +blake3 +snowflake +trimesh +pypandoc +xgrammar +pdm-backend +django-celery-beat +pybcj +funcy +acryl-datahub +timezonefinder +tf-keras +dataclass-wizard +pynvml +multivolumefile +cytoolz +django-stubs +pytest-randomly +proglog +influxdb +evaluate +syrupy +tree-sitter-python +krb5 +compressed-tensors +databricks-labs-lsql +inflate64 +fake-useragent +cloud-sql-python-connector +flask-restful +pint +dj-database-url +bitstring +jq +pip-requirements-parser +llama-cloud +hjson +pymeeus +azure-cosmosdb-table +nox +channels +requests-cache +sseclient-py +azure-cosmosdb-nspkg +setuptools-git +sqlglotrs +pulumi-aws +teradatasqlalchemy +types-psycopg2 +rtree +pdpyras +async-generator +aiocache +poetry-dynamic-versioning +dynaconf +atpublic +cog +pagerduty +pytest-order +pyqt5-qt5 +aws-cdk-asset-node-proxy-agent-v6 +pytz-deprecation-shim +prefect-aws +youtube-transcript-api +hologram +pillow-heif +presto-python-client +jwt +locust-cloud +pygit2 +rq +retry2 +tensorflowjs +azure-search-documents +pyyaml-include +sphinx-argparse +dependency-injector +dirtyjson +aws-psycopg2 +pytest-subtests +pyenchant +msgraph-sdk +prometheus-flask-exporter +o365 +lupa +ydb-dbapi +snowflake-legacy +pillow-avif-plugin +azure-mgmt-dns +logfire-api +pulp +nvidia-cublas-cu11 +giturlparse +gprof2dot +gym-notices +editorconfig +pycurl +pytest-ordering +zict +mbstrdecoder +supervisor +pyclipper +plumbum +sounddevice +llama-index-llms-openai +typepy +apache-airflow-providers-microsoft-fabric +apache-airflow-providers-sftp +apache-airflow-providers-microsoft-mssql +terminaltables +kaitaistruct +gsutil +google-cloud-trace +llguidance +redis-py-cluster +dockerfile-parse +fasttext-wheel +python-frontmatter +fpdf2 +jsonconversion +s3path +ifaddr +tablib +gguf +django-celery-results +hmsclient +dm-tree +azure-functions +salesforce-bulk +boxsdk +web3 +keras-preprocessing +pathlib-abc +jsbeautifier +fastrlock +coolname +lm-format-enforcer +icalendar +launchdarkly-eventsource +nvidia-cudnn-cu11 +uv-build +pypsrp +subprocess-tee +python-ulid +opentelemetry-resourcedetector-gcp +eth-utils +multipledispatch +pyhanko +astropy +dbutils +appium-python-client +python-crfsuite +tweepy +base58 +mypy-boto3-iam +aws-cdk-cloud-assembly-schema +google-cloud-iam +dlt +haversine +pyodps +tensorstore +swifter +dagster +cupy-cuda12x +yandex-query-client +drf-yasg +open-clip-torch +rustworkx +opentelemetry-semantic-conventions-ai +pyfiglet +tensorboard-plugin-wit +etils +pyqt5 +tree-sitter-javascript +nested-lookup +multitasking +yaspin +pycocotools +opentelemetry-instrumentation-celery +gcloud-aio-pubsub +pipelinewise-singer-python +uuid +editdistance +dbt-bigquery +sphinx-autoapi +plette +comtypes +ghapi +dparse +eth-rlp +datefinder +multimethod +alibabacloud-adb20211201 +pulumi-command +dagster-graphql +hashin +construct +discord-py +sspilib +codespell +flexparser +pygame +django-phonenumber-field +outlines-core +ortools +flexcache +sqlalchemy-drill +google-cloud-recaptcha-enterprise +types-aiobotocore +pyqt5-sip +pdfkit +tzfpy +arviz +puremagic +azure-eventgrid +respx +aiolimiter +flake8-bugbear +parsel +simsimd +pygtrie +towncrier +hyperopt +dagster-pipes +pystache +opentelemetry-exporter-gcp-trace +nvidia-cuda-nvrtc-cu11 +django-appconf +types-beautifulsoup4 +notion-client +onnxruntime-gpu +databricks-api +diff-match-patch +keyrings-alt +pygsheets +strawberry-graphql +zope-deprecation +pyyaml-ft +rasterio +auth0-python +folium +gspread-dataframe +striprtf +python-iso639 +pylatexenc +elevenlabs +pydruid +dbl-tempo +hijri-converter +munch +spython +azure-storage-file +aiomultiprocess +pulumi-tls +partial-json-parser +pywinpty +nvidia-cuda-runtime-cu11 +colour +newrelic +logfire +imagehash +asteval +repoze-lru +pex +slowapi +social-auth-core +sphinxcontrib-httpdomain +accessible-pygments +flatten-dict +django-oauth-toolkit +python-vagrant +sacrebleu +pefile +legacy-cgi +types-pillow +zipfile38 +pyqt6 +rollbar +cleanco +txaio +github3-py +dbt-fabric +cdk-nag +findspark +pastedeploy +venusian +tld +autobahn +crossplane +pygeohash +pypiwin32 +ddapm-test-agent +priority +eth-hash +eth-typing +win32-setctime +checksumdir +pyqt6-qt6 +hypercorn +pytest-dotenv +pypandoc-binary +flask-openid +channels-redis +asyncer +sphinxcontrib-spelling +azure-ai-documentintelligence +formulaic +kornia +deptry +llama-index-readers-file +pydantic-evals +prance +banks +branca +gdown +social-auth-app-django +concurrent-log-handler +types-aiobotocore-s3 +flax +interegular +shtab +databricks-pypi1 +pyhamcrest +supabase-auth +torchsde +memoization +pytest-httpx +mongoengine +python-rapidjson +supabase-functions +rx +affine +llama-index-readers-llama-parse +pyqt6-sip +speechrecognition +pdfrw +jdcal +types-tqdm +azure-ai-ml +pylint-plugin-utils +sklearn +safehttpx +vertexai +plyvel +mypy-boto3-ssm +astropy-iers-data +arize-phoenix +port-for +depyf +trampoline +whenever +albucore +requests-futures +opentelemetry-sdk-extension-aws +cvxpy +injector +pyerfa +openinference-semantic-conventions +opentelemetry-instrumentation-system-metrics +urwid +kerberos +interface-meta +pastel +safety-schemas +mypy-boto3-batch +umap-learn +bc-detect-secrets +seleniumbase +clang-format +tox-uv +mangum +pypyp +strip-hints +anyascii +hupper +netcdf4 +python-can +opentelemetry-instrumentation-boto3sqs +commentjson +apache-airflow-providers-postgres +panel +azureml-core +pymemcache +azureml-dataprep +azure-mgmt-subscription +expandvars +eth-abi +vulture +groovy +polib +pymupdfb +mypy-boto3-athena +pykwalify +jsonargparse +eth-keys +msoffcrypto-tool +pymsgbox +marko +probableparsing +hijridate +pathlib-mate +pip-audit +click-spinner +openai-agents +alive-progress +types-pygments +netifaces +leb128 +azure-mgmt-reservations +ctranslate2 +hf-transfer +about-time +svgwrite +oras +lmdb +yamale +tritonclient +arabic-reshaper +usaddress +pytest-check +dagster-postgres +pyusb +python-oxmsg +translationstring +portpicker +stringzilla +oslo-utils +clickhouse-sqlalchemy +rlp +pytest-httpserver +types-simplejson +uuid7 +gotrue +ckzg +soda-core +sacremoses +llama-index-workflows +dogpile-cache +aws-cdk-aws-lambda-python-alpha +category-encoders +osqp +alibabacloud-credentials +python-editor +zarr +eth-keyfile +pyaes +llama-index-cli +cftime +wget +openxlab +mediapipe +hubspot-api-client +pyramid +dataproperty +azure-mgmt-devtestlabs +mypy-boto3-kinesis +timeout-decorator +immutables +regress +ip3country +chex +pyhanko-certvalidator +mirakuru +django-simple-history +pywinauto +aiofile +pygraphviz +caio +pytablewriter +pep8-naming +ec2-metadata +tabledata +opentelemetry-instrumentation-asyncio +plaster-pastedeploy +avro-gen3 +plaster +requests-aws-sign +plotnine +flask-compress +llama-index-embeddings-openai +lit +pyrate-limiter +pynndescent +blosc2 +pysmb +jproperties +intelhex +mypy-boto3-ecr +mizani +mypy-boto3-stepfunctions +opentelemetry-exporter-zipkin-json +ccxt +asynch +daphne +spdx-tools +crc32c +braceexpand +ephem +django-ipware +scapy +types-freezegun +ty +deepeval +undetected-chromedriver +pykerberos +bazel-runfiles +tcolorpy +json-log-formatter +ecs-logging +alibabacloud-tea +comfyui-workflow-templates +ebcdic +django-ratelimit +alibabacloud-tea-util +amazon-textract-response-parser +lakefs-sdk +alibabacloud-openapi-util +azureml-sdk +flask-restx +sphinx-jinja +nanoid +policy-sentry +langchain-experimental +numcodecs +numpy-financial +webargs +ulid-py +dbt-redshift +alibabacloud-endpoint-util +alibabacloud-gateway-spi +llama-index-agent-openai +nodejs-wheel-binaries +mammoth +grpc-stubs +pydata-sphinx-theme +cobble +kornia-rs +torchdata +whatthepatch +conan +mutagen +pytest-postgresql +analytics-python +sphinxcontrib-mermaid +sqlalchemy2-stubs +testfixtures +svglib +python-stdnum +gym +geocoder +scandir +eralchemy2 +django-crispy-forms +dspy +pusher +anndata +simple-gcp-object-downloader +phonenumberslite +ratelim +flask-socketio +b2luigi +azure-mgmt-datalake-analytics +poetry-plugin-pypi-mirror +boa-str +furo +scs +alibabacloud-tea-xml +bump2version +check-jsonschema +patch-ng +bc-python-hcl2 +databricks-pypi2 +sqlfluff-templater-dbt +statsig +cloudsplaining +h5netcdf +pycep-parser +aws-encryption-sdk +piexif +odfpy +libtmux +decord +rfc3987 +apache-sedona +opentelemetry-instrumentation-openai +scrapy +sarif-om +xhtml2pdf +airportsdata +apache-airflow-core +openai-harmony +python-xlib +easydict +dropbox +aioredis +optax +pyppeteer +databricks-agents +pyquery +igraph +pyramid-mako +line-profiler +dohq-artifactory +choreographer +rcssmin +cli-exit-tools +django-csp +awslambdaric +kylinpy +django-import-export +screeninfo +httmock +lib-detect-testenv +rank-bm25 +tfds-nightly +xsdata +statsig-python-core +ibm-db +djangorestframework-stubs +pytorch-metric-learning +types-mock +jaxtyping +promise +pyairtable +pyramid-debugtoolbar +easyocr +spandrel +gcovr +opentelemetry-instrumentation-sqlite3 +moreorless +algoliasearch +bc-jsonpath-ng +ansi2html +mypy-boto3-apigateway +kagglehub +pyramid-jinja2 +pysaml2 +sagemaker-mlflow +requests-sigv4 +cssbeautifier +lifelines +dagster-shared +wordcloud +sqlalchemy-stubs +nanobind +dash-bootstrap-components +boto3-type-annotations +bitstruct +django-js-asset +logistro +wmill +trl +inquirerpy +pandasql +pfzy +googlemaps +singer-python +tree-sitter-typescript +jsonpath-rw +mando +presidio-analyzer +modin +radon +geoalchemy2 +open-webui +grep-ast +wrapt-timeout-decorator +docx2txt +primp +pyluach +supafunc +azure-ai-inference +atlasclient +intervaltree +django-silk +outlines +pytest-timeouts +graphframes +djlint +jinja2-simple-tags +soda-core-spark +python-nvd3 +lru-dict +types-ujson +flask-httpauth +structlog-sentry +xattr +singledispatch +cucumber-tag-expressions +shareplum +flashtext +docxtpl +mkdocs-monorepo-plugin +pyunormalize +azure +simple-parsing +lark-parser +mypy-boto3-bedrock-runtime +swig +webdataset +llama-index-instrumentation +pytest-recording +pytest-retry +pdoc +versioneer +pycairo +betterproto +sphinx-basic-ng +flake8-docstrings +crewai +soda-core-spark-df +robotframework-pythonlibcore +django-allauth +mypy-boto3-xray +clarabel +dirhash +scantree +azureml-mlflow +keystoneauth1 +dash-core-components +backports-functools-lru-cache +wand +itemadapter +diff-parser +mysql-connector +poethepoet +opentelemetry-instrumentation-jinja2 +pyjson5 +tables +mypy-boto3-schemas +uuid-utils +mypy-boto3-signer +click-log +lakefs +zeroconf +jiwer +j2cli +azure-loganalytics +django-otp +azure-mgmt-consumption +uwsgi +sphinxcontrib-redoc +mypy-boto3-sns +optimum +cucumber-expressions +tree-sitter-yaml +pytimeparse2 +types-cryptography +oslo-config +jupyter-kernel-gateway +django-prometheus +dagster-cloud +lunarcalendar +aiogram +azure-mgmt-notificationhubs +diracx-core +dagster-aws +decli +databricks +browsergym-core +virtualenv-clone +koalas +google-cloud-artifact-registry +jupytext +icecream +testpath +sanic +node-semver +pyvis +tensorflow-hub +aiorwlock +databricks-labs-dqx +textwrap3 +django-health-check +queuelib +pyarrow-stubs +apache-airflow-providers-microsoft-azure +hashids +rdkit +openhands-aci +dash-table +starlette-context +trailrunner +azure-monitor-ingestion +clr-loader +dash-html-components +hdbcli +cheroot +country-converter +mypy-boto3-lakeformation +tensorflow-datasets +stdlibs +nothing +aws-secretsmanager-caching +dbfread +pydispatcher +jinja2-humanize-extension +mdx-truly-sane-lists +oslo-i18n +faster-whisper +langchain-groq +apache-airflow-client +pyexcel-io +aws-cdk-asset-kubectl-v20 +xarray-einstats +azure-communication-email +c7n +lml +asyncstdlib +azure-schemaregistry +pyvmomi +duckduckgo-search +pyvirtualdisplay +lxml-stubs +flask-bcrypt +exchangelib +azure-mgmt-logic +opentelemetry-instrumentation-asyncpg +pythonnet +thop +azure-mgmt-relay +audioop-lts +anybadge +oyaml +types-markupsafe +types-jinja2 +pyscreeze +alibabacloud-credentials-api +chispa +trafilatura +yacs +robotframework-seleniumlibrary +polling +usort +apache-airflow-providers-standard +tensorflow-cpu +hyperpyyaml +telethon +arxiv +speechbrain +resampy +robotframework-requests +opentelemetry-instrumentation-mysqlclient +pyside6 +rjsmin +tyro +validate-email +ptpython +django-waffle +rouge-score +flask-talisman +aiostream +progress +flask-oidc +basedpyright +zc-lockfile +filterpy +genai-prices +pylint-django +azure-servicefabric +pyautogui +xmlrunner +ufmt +ndindex +ndjson +e2b +pygetwindow +ibm-platform-services +array-record +pytweening +iopath +fastapi-pagination +qh3 +apsw +commitizen +azure-mgmt +python-pam +azure-mgmt-commerce +ansiwrap +logging-azure-rest +flask-admin +openinference-instrumentation +azure-mgmt-scheduler +httpx-ws +azure-mgmt-powerbiembedded +detect-secrets +mouseinfo +mss +azure-mgmt-machinelearningcompute +azure-mgmt-hanaonazure +pyrect +frida +azure-mgmt-managementpartner +sgqlc +pybuildkite +artifacts-keyring +pyloudnorm +lancedb +textblob +dominate +tentaclio +azure-servicemanagement-legacy +braintree +apache-airflow-providers-jdbc +google-cloud-discoveryengine +cloudscraper +itemloaders +pyandoc +pytest-snapshot +synapseml +pyquaternion +langchain-ollama +brotlipy +azure-mgmt-devspaces +gnupg +azure-mgmt-hybridcompute +wadler-lindig +rstr +confuse +click-help-colors +bson +jschema-to-python +pytest-messenger +open3d +troposphere +dnslib +braintrust +azure-applicationinsights +typeshed-client +smartsheet-python-sdk +datacompy +pondpond +oslo-serialization +grimp +sudachipy +autograd-gamma +mem0ai +aws-sam-cli +nulltype +mypy-boto3-logs +pyside6-essentials +types-boto3-s3 +transitions +nibabel +django-mysql +django-countries +nvidia-cusparse-cu11 +mmhash3 +itypes +mleap +cuda-python +coreapi +nvidia-cusolver-cu11 +opentelemetry-instrumentation-langchain +pytest-aiohttp +backports-weakref +apache-airflow-task-sdk +nvidia-curand-cu11 +nvidia-cufft-cu11 +apache-airflow-providers-oracle +spinners +log-symbols +param +puccinialin +junit2html +grpcio-testing +suds-community +stdlib-list +s3cmd +tentaclio-s3 +pytest-freezegun +elastic-apm +emr-notebooks-magics +cmd2 +pyahocorasick +dirty-equals +config +iterative-telemetry +s3pathlib +sanic-routing +flatten-json +azure-cognitiveservices-speech +josepy +plotly-express +pyexcel +boto-session-manager +connectorx +justext +django-compressor +libsass +jaconv +semantic-kernel +azure-ai-projects +uncertainties +result +mkdocs-macros-plugin +plac +types-openpyxl +luigi +opentelemetry-instrumentation-aws-lambda +pyside6-addons +docling +beanie +quart +scikit-base +lintrunner +freetype-py +pytest-dependency +pyudev +sphinxcontrib-websupport +aiohttp-socks +iterproxy +llama-index-multi-modal-llms-openai +apache-airflow-providers-openlineage +argparse-addons +nameparser +aiortc +jsonnet +urllib3-future +flatdict +triad +github-heatmap +jh2 +numpydoc +crewai-tools +fugue +elasticsearch-dbapi +webtest +python-lsp-jsonrpc +wassima +docformatter +jupyter-ydoc +niquests +pytest-bdd +adagio +vtk +pylance +jupyter-server-ydoc +asgi-correlation-id +amazon-textract-caller +latex2mathml +types-colorama +livekit-protocol +rope +types-pyserial +yappi +docker-compose +pytube +hatch-requirements-txt +dicttoxml +google-cloud-documentai +pyupgrade +customerio +colorclass +prometheus-api-client +courlan +shiboken6 +nvidia-cuda-cupti-cu11 +aws-lambda-builders +nvidia-nccl-cu11 +aiodocker +super-collections +collections-extended +livekit-api +brotlicffi +autofaiss +databricks-feature-engineering +textparser +pytest-assume +path +patchright +nvidia-cuda-nvcc-cu12 +jupyter-server-fileid +presidio-anonymizer +opentelemetry-instrumentation-bedrock +flake8-isort +google-adk +opentelemetry-instrumentation-vertexai +browser-use +snakeviz +python-ipware +valkey +requests-unixsocket +localstack-core +htmlmin +google-cloud-profiler +opentelemetry-propagator-b3 +update-checker +flask-shell-ipython +func-args +stone +opentelemetry-instrumentation-cohere +tree-sitter-c-sharp +firecrawl-py +grandalf +pymatting +tinydb +nvidia-nvtx-cu11 +tqdm-multiprocess +types-regex +llama-index-program-openai +rembg +azure-mgmt-databoxedge +backports-tempfile +flake8-comprehensions +embedding-reader +zthreading +opentelemetry-instrumentation-starlette +pymupdf4llm +mypy-boto3-dataexchange +cerberus-python-client +opentelemetry-instrumentation-kafka-python +ydata-profiling +treescope +django-formtools +sly +opentelemetry-test-utils +types-click +adapters +pytest-factoryboy +func-timeout +django-anymail +pretty-html-table +mkdocs-literate-nav +snapshot-restore-py +cliff +fasttext +visions +pinecone-client +tecton +import-linter +dify-plugin +pytest-github-actions-annotate-failures +raven +selenium-wire +tensorflow-probability +types-pyasn1 +opentelemetry-instrumentation-replicate +opencv-contrib-python-headless +django-mathfilters +pyannote-core +apache-airflow-providers-apache-kafka +y-py +dagster-k8s +azure-ai-formrecognizer +dotmap +amazon-textract-textractor +httpx-aiohttp +tree-sitter-language-pack +json-delta +magika +exa-py +opentelemetry-instrumentation-pymongo +aioice +pylink-square +pylibsrtp +tracerite +opentelemetry-instrumentation-llamaindex +html5tagger +envyaml +pre-commit-hooks +opentelemetry-instrumentation-chromadb +python-calamine +splunk-handler +dotty-dict +mypy-boto3-kms +array-api-compat +janus +llama-index-question-gen-openai +graphemeu +prefect-gcp +polyline +daytona-api-client +alembic-postgresql-enum +opentelemetry-instrumentation-haystack +clandestined +textdistance +pyannote-database +aaaaaaaaa +opentelemetry-instrumentation-transformers +coremltools +pytest-lazy-fixture +banal +tree-sitter-embedded-template +opentelemetry-instrumentation-qdrant +apache-airflow-providers-mongo +ddt +pysmi +mkdocs-git-revision-date-localized-plugin +modelscope +cuda-bindings +ypy-websocket +opentelemetry-instrumentation-pinecone +opentelemetry-instrumentation-weaviate +shillelagh +python-certifi-win32 +opentelemetry-instrumentation-watsonx +strands-agents +opentelemetry-instrumentation-ollama +numdifftools +pyqt6-webengine-qt6 +opentelemetry-instrumentation-alephalpha +mkdocs-redirects +opentelemetry-instrumentation-milvus +sudachidict-core +html-text +utilsforecast +django-polymorphic +pymongo-auth-aws +opentelemetry-instrumentation-mistralai +newrelic-telemetry-sdk +ag-ui-protocol +drf-nested-routers +yarg +docling-core +coveralls +opentelemetry-instrumentation-lancedb +oci-cli +jsonmerge +avalara +fluent-logger +django-taggit +opentelemetry-instrumentation-marqo +langgraph-checkpoint-postgres +sasl +celery-types +opentelemetry-instrumentation-together +peppercorn +lunardate +z3-solver +mypy-boto3-ses +types-aiobotocore-sqs +opentelemetry-instrumentation-sagemaker +west +python-geohash +pymongocrypt +bootstrap-flask +dvc +fido2 +camel-converter +python-keystoneclient +pyqt6-webengine +guppy3 +rfc3339 +breathe +databricks-vectorsearch +openai-whisper +pymatgen +cron-converter +django-picklefield +anki +lintrunner-adapters +pyannote-metrics +opentelemetry-instrumentation-crewai +flask-marshmallow +pytoolconfig +pyopengl +parsy +dagster-slack +duckdb-engine +docstring-to-markdown +together +tavily-python +jupyter-packaging +jsonschema-rs +types-lxml +mypy-boto3-cloudwatch +selectolax +python-tds +types-httplib2 +openstacksdk +autovizwidget +hdijupyterutils +marshmallow-jsonschema +sagemaker-data-insights +runloop-api-client +sagemaker-datawrangler +comfyui-embedded-docs +stepfunctions +aqt +embedchain +kconfiglib +os-service-types +facexlib +python-lsp-server +opentelemetry-instrumentation-pika +mypy-boto3-events +pysam +pytest-flask +k8 +pytest-memray +mypy-boto3-ecs +dataset +dict2xml +django-reversion +aws-msk-iam-sasl-signer-python +pi-heif +deepspeed +logging +devtools +azure-ai-agents +hdbscan +spark-nlp +debtcollector +opentelemetry-instrumentation-mcp +opencc-python-reimplemented +azure-mgmt-resourcegraph +autopage +expecttest +dagster-cloud-cli +datadog-lambda +extract-msg +word2number +flake8-pyproject +colored +tink +kivy +comfyui-frontend-package +magic-filter +sphinx-tabs +pytest-profiling +azure-containerregistry +livekit-agents +import-deps +django-structlog +idf-component-manager +langgraph-cli +yattag +anki-release +lazy-model +tableau-api-lib +pytest-homeassistant-custom-component +grpc-google-logging-v2 +model-bakery +treelib +flask-mail +anki-audio +publicsuffix2 +pip-licenses +fastprogress +azure-storage +tableauhyperapi +pynose +pudb +opentelemetry-propagator-gcp +codetiming +pem +sktime +azureml-pipeline +textstat +apache-airflow-providers-datadog +jieba +madoka +c7n-org +opentelemetry-instrumentation-tortoiseorm +prefect-docker +plux +statsforecast +pyshark +weread2notionpro +dvc-data +elementary-data +initools +lmfit +credstash +apache-airflow-providers-pagerduty +python-consul +meltano +mapbox-earcut +pybytebuffer +polling2 +emmet-core +supervision +shyaml +pyshp +future-fstrings +bumpversion +opentelemetry-resourcedetector-kubernetes +idna-ssl +pkce +opentelemetry-exporter-jaeger-thrift +langchain-mcp-adapters +evergreen-py +fastexcel +deep-translator +fhir-resources +scmrepo +certbot +itables +contextvars +typish +airbyte-cdk +docusign-esign +docker-image-py +inflector +apache-airflow-providers-odbc +ariadne-codegen +plantuml-markdown +untokenize +pipreqs +apache-airflow-providers-dbt-cloud +databases +versioningit +waxtablet +types-aioboto3 +mypy-boto3-sagemaker +traceloop-sdk +tree-sitter-go +businesstimedelta +backports-cached-property +oslo-log +webvtt-py +pylcs +django-widget-tweaks +inline-snapshot +eradicate +restrictedpython +django-axes +opentelemetry-resourcedetector-docker +avro-gen +jsonschema-spec +phik +darabonba-core +google-cloud-pipeline-components +ml-collections +langgraph-api +mdformat +google-api-python-client-stubs +gtts +torchdiffeq +types-chardet +cloup +linecache2 +clang +azure-schemaregistry-avroserializer +stamina +traceback2 +django-querycount +daytona-sdk +pytest-vcr +dockerpty +djangorestframework-api-key +mpire +crccheck +pylsqpack +starlette-exporter +miscreant +watchgod +pyspark-dist-explore +opentelemetry-propagator-jaeger +browserbase +flake8-import-order +tree-sitter-ruby +asyncache +types-oauthlib +pyviz-comms +mkdocs-section-index +iterfzf +colorcet +pysbd +django-choices +requests-auth-aws-sigv4 +wikipedia +pybloom-live +pep8 +langchain-chroma +pdbp +esp-idf-size +mkdocs-techdocs-core +celery-redbeat +coreforecast +httpretty +pgeocode +livekit +apache-airflow-providers-apache-impala +ibm-watsonx-ai +wasmer +tree-sitter-rust +plaid-python +types-werkzeug +apache-airflow-providers-salesforce +workalendar +setuptools-git-versioning +cron-validator +aioquic +apache-airflow-providers-celery +tabcompleter +dagster-dbt +hashring +pyglet +sshpubkeys +pydantic-yaml +opentelemetry-instrumentation-anthropic +fastapi-utils +scikit-optimize +python-binance +pyjsparser +enrich +flake8-polyfill +memcache +androguard +annoy +cibuildwheel +pytest-split-tests +asteroid-filterbanks +uszipcode +mypy-boto3-emr +opentelemetry-instrumentation-psycopg +molecule +pybtex +latexcodec +django-ses +dateutils +geonames +torchtune +pyannote-pipeline +mxnet +mplcursors +tensorflow-addons +flufl-lock +capstone +mistletoe +rpyc +js2py +pyexcel-xls +torch-geometric +skl2onnx +pynput +dvc-objects +liblinear-multicore +django-mptt +jsons +workos +decopatch +dvc-studio-client +nebius +sparqlwrapper +pycollada +gpustat +coreschema +envs +tinynetrc +evidently +pytest-celery +nbsphinx +apkinspector +primepy +python-etcd +torch-audiomentations +markitdown +shrub-py +django-admin-sortable2 +pulsar-client +red-discordbot +webauthn +tach +sampleproject +sqlalchemy-mate +torch-pitch-shift +stable-baselines3 +scipy-stubs +flask-smorest +svg-path +types-ipaddress +easygui +honcho +mutf8 +manifold3d +pyre-extensions +langchain-huggingface +langchain-cohere +docling-parse +mypy-boto3-elbv2 +pytelegrambotapi +numpy-quaternion +fastdiff +autocommand +opentelemetry-exporter-prometheus-remote-write +requestsexceptions +glob2 +gluonts +sanic-ext +django-hijack +roman +sbvirtualdisplay +hypothesis-jsonschema +sqlalchemy-trino +types-xmltodict +pytest-test-groups +clean-fid +msbench-utils +pyexcel-xlsx +lsprotocol +optype +mitmproxy +pylev +python3-xlib +apache-airflow-providers-apache-spark +pluginbase +pydrive2 +mail-parser +buildkite-test-collector +scikit-build +habluetooth +python-barcode +dtlpymetrics +gcloud-rest-auth +wurlitzer +pymc +django-ninja +traittypes +shandy-sqlfmt +sqlitedict +python-whois +compressed-rtf +ariadne +aiocsv +django-admin-rangefilter +django-object-actions +dvc-render +patool +crontab +urllib3-secure-extra +praw +pypinyin +types-decorator +flake8-print +codemagic-cli-tools +replicate +sqllineage +scons +mimesis +copier +typing-utils +jenkinsapi +djangorestframework-csv +types-passlib +apache-airflow-providers-trino +jinja2-cli +markdown-pdf +flask-debugtoolbar +opentelemetry-instrumentation-google-generativeai +mypy-boto3-route53 +pyocd +pydantic-xml +python3-logstash +qiskit +lingua-language-detector +zipfile36 +holoviews +pytest-watcher +recurring-ical-events +einx +rouge +swagger-spec-validator +html-testrunner +flask-dance +gender-guesser +geohash2 +blockbuster +gto +wasmer-compiler-cranelift +viztracer +absolufy-imports +pyfarmhash +tree-sitter-bash +publish-event-sns +exchange-calendars +assemblyai +scooby +lmnr +ruyaml +stomp-py +adyen +flake8-plugin-utils +trafaret +opentelemetry-instrumentation-tornado +logzero +rust-just +mypy-boto3-cognito-idp +blessings +gitdb2 +shellescape +watchdog-gevent +usd-core +pyzbar +pytest-durations +livy +pycognito +segment-anything +crowdstrike-falconpy +openvino +aws-cdk-aws-glue-alpha +paste +prawcore +pytest-testmon +dvc-task +vhacdx +pyomo +pyvista +objgraph +sqltrie +tree-sitter-java +mypy-boto3-scheduler +turbopuffer +meilisearch +flake8-noqa +frictionless +grapheme +opencensus-ext-logging +nats-py +paddlepaddle +natto-py +objprint +casbin +subprocess32 +semchunk +mlxtend +art +ntlm-auth +django-two-factor-auth +pydicom +certifi-linux +quantlib +fancycompleter +drf-spectacular-sidecar +pdbpp +formic2 +dvc-http +us +pyngrok +awesomeversion +oslo-db +authentik-client +scanpy +docling-ibm-models +requests-pkcs12 +amplitude-analytics +docxcompose +pygls +opentelemetry-exporter-jaeger-proto-grpc +jaraco-text +python-redis-lock +flake8-builtins +keras-tuner +rpaframework +imblearn +apache-airflow-providers-alibaba +google-cloud-scheduler +uv-dynamic-versioning +pylint-pydantic +strands-agents-tools +donfig +pyobjc-core +unittest2 +fasta2a +warcio +snapshottest +mypy-boto3-emr-serverless +mailchimp-transactional +pyannote-audio +petl +neptune-fetcher +tatsu +streamlit-aggrid +newspaper3k +dagster-pandas +azureml-train +unsloth +crawl4ai +awacs +strawberry-graphql-django +opentelemetry-instrumentation-elasticsearch +imgaug +javaobj-py3 +stanza +pyod +canopen +pyexecmd +office365 +signxml +roundrobin +mypy-boto3-bedrock +types-confluent-kafka +sphinx-book-theme +oslo-service +apache-airflow-providers-redis +mecab-python3 +flupy +amqpstorm +pytest-codspeed +django-deprecate-fields +bubus +reliability +crayons +py-ecc +opentelemetry-instrumentation-groq +inject +pysnmp +naked +esp-idf-kconfig +python-fsutil +types-flask +django-pgactivity +legacy-api-wrap +testtools +pytest-azurepipelines +django-pglock +braintrust-core +mailchimp-marketing +tabula-py +mypy-boto3-eks +awscli-local +types-aiobotocore-dynamodb +voyageai +hypothesis-graphql +verspec +crispy-bootstrap5 +litestar +mypy-boto3-codeartifact +click-creds +mypy-boto3-cloudfront +rlpycairo +pyintelowl +gspread-formatting +ibm-cos-sdk +looseversion +oletools +mypy-boto3-codebuild +ibm-cos-sdk-core +google-cloud-error-reporting +google-search-results +find-libpython +pyroute2 +sparse +mypy-boto3-textract +test-results-parser +schemathesis +opentelemetry-instrumentation-confluent-kafka +kt-legacy +ibm-cos-sdk-s3transfer +jinjasql +urlextract +gputil +pcodedmp +python-semantic-release +pynetbox +bigframes +dbt-duckdb +mltable +mypy-boto3-firehose +dirac +ratelimiter +p4python +darglint +claude-code-sdk +isal +hellosign-python-sdk +latex2sympy2-extended +djangorestframework-dataclasses +scenedetect +pyscaffold +git-filter-repo +types-bleach +jsmin +flask-basicauth +defusedcsv +fredapi +django-linear-migrations +types-maxminddb +oslo-context +tensordict +onfido-python +rarfile +opentracing +arize-phoenix-otel +dbus-fast +fluent-syntax +pymodbus +python-memcached +eralchemy +readability-lxml +mycdp +aws-lambda-typing +python-cinderclient +tensorflow-model-optimization +pandas-market-calendars +x-wr-timezone +starlette-testclient +openapi-schema-pydantic +julius +minify-html +django-treebeard +delta +cartopy +mailjet-rest +types-aiobotocore-lambda +ragas +cuda-pathfinder +autoevals +keyrings-codeartifact +lpips +ldapdomaindump +opentelemetry-instrumentation-boto +suds-py3 +appengine-python-standard +litellm-proxy-extras +cvss +sphinxcontrib-bibtex +kaggle +ruptures +math-verify +readerwriterlock +git-python +mockito +domdf-python-tools +python-logging-loki +types-boto3-full +pygerduty +configcat-client +whoosh +firecrawl +apache-airflow-providers-tableau +telnetlib3 +pytest-flakefinder +lazy-imports +mne +git-remote-codecommit +python-openstackclient +protoc-gen-openapiv2 +json-logging +mercantile +fastapi-cache2 +xdoctest +embreex +types-aiobotocore-ec2 +strict-rfc3339 +pyobjc-framework-cocoa +xmljson +onnxconverter-common +pymediainfo +pycrdt +ibis-framework +mkdocs-click +opentelemetry-instrumentation-pymysql +aiodataloader +submitit +jsonfield +minidump +hnswlib +types-tzlocal +sagemaker-feature-store-pyspark-3-1 +random-user-agent +ecos +livekit-plugins-silero +imapclient +htmlmin2 +pytest-cases +bigquery-schema-generator +graphene-django +schwifty +fixedwidth +ilcdirac +roboflow +pyjks +pyyml +bce-python-sdk +deepgram-sdk +recommonmark +mypy-boto3-cognito-identity +mitmproxy-rs +parsley +rtfde +googleads +falcon +chargebee +pytest-docker-tools +jinja2-ansible-filters +standard-chunk +pebble +rioxarray +flash-attn +mkdocs-glightbox +rstcheck +unpaddedbase64 +pygobject +aiogoogle +jsonpath-rw-ext +types-dateparser +django-webpack-loader +polyleven +fastai +dag-factory +types-python-jose +litestar-htmx +xopen +homeassistant +osc-lib +tensorflow-io +azureml-dataset-runtime +opentelemetry-exporter-gcp-monitoring +intuit-oauth +treepoem +click-aliases +zxcvbn +srt +glfw +django-guardian +flake8-black +disposable-email-domains +fal-client +torchtext +django-pgtrigger +coredis +anki-mac-helper +pglast +manhole +cmaes +transaction +typer-slim +standard-aifc +agno +sqlalchemy-adapter +fastembed +types-boto +mypy-boto3-autoscaling +reductoai +reedsolo +dagster-docker +textfsm +singleton-decorator +fvcore +pysubs2 +pyfaidx +googletrans +opentelemetry-instrumentation-aio-pika +python-baseconv +attrdict +wordfreq +psycogreen +docker-py +opik +psygnal +maison +spacy-language-detection +mypy-boto3-elasticache +splunk-sdk +py-moneyed +nutree +mypy-boto3-bedrock-agent-runtime +pykakasi +dramatiq +oslo-messaging +pytest-testinfra +gherkin-official +gcloud +types-aiobotocore-dataexchange +pyapns-client +lia-web +mujoco +vobject +pyrepl +resend +asyncpg-stubs +easyprocess +harfile +lm-eval +onnxscript +streamsets +httpie +pytest-docker +mypy-boto3-kafka +hunter +json2html +azureml-fsspec +json-stream-rs-tokenizer +gepa +yamlfix +py-markdown-table +case-conversion +argh +pytest-datadir +mkdocs-panzoom-plugin +cyclonedx-bom +fastapi-sso +mkdocs-awesome-pages-plugin +tangled-up-in-unicode +optuna-integration +mpi4py +solders +infi-systray +timing-asgi +locate +alembic-utils +opentok +ansible-runner +ruamel-yaml-jinja2 +mike +flake8-eradicate +logzio-python-handler +maybe-else +pysubtypes +cvxopt +pymiscutils +prettierfier +pyte +jinja2-time +opentelemetry-resourcedetector-process +paddleocr +pathmagic +pyiotools +dbl-discoverx +django-auditlog +logz +reportportal-client +opentelemetry-container-distro +google-cloud-dialogflow-cx +azure-storage-nspkg +username +astral +inference-gpu +solana +jaro-winkler +imagecodecs +lib4sbom +dbt-athena-community +quinn +cli-helpers +okta +ipympl +drf-extensions +scylla-driver +sqlalchemy-json +fortifyapi +crypto +diceware +msgpack-python +granian +mypy-boto3-efs +verboselogs +cma +uhashring +nc-py-api +libusb-package +matrix-nio +vector-quantize-pytorch +sharepy +myst-nb +mypy-boto3-application-autoscaling +type-enforced +labelbox +mypy-boto3-config +sparkmeasure +plum-dispatch +taskgroup +dagster-celery +couchbase +pytest-watch +django-localflavor +versioneer-518 +arch +django-admin-list-filter-dropdown +hstspreload +jupyter-highlight-selected-word +types-authlib +docarray +kneed +inspect-ai +delta-sharing +pytest-opentelemetry +htmldocx +jupyter-nbextensions-configurator +seeuletter +setuptools-golang +flask-threads +notion +mypy-boto3-acm +awscliv2 +asciitree +ensure +simpleflow +slackify-markdown +pycasbin +codecov +gurobipy +langchain-postgres +mypy-boto3-s3control +fastapi-mail +xmlunittest +azureml-dataprep-rslex +jax-cuda12-plugin +google-cloud-ndb +adjusttext +mypy-boto3-bedrock-agent +fairscale +descartes +kafe2 +keyboard +plpygis +dashscope +mkdocs-gen-files +mypy-boto3-imagebuilder +pybtex-docutils +fcache +palettable +columnar +flake8-junit-report-basic +mypy-boto3-pricing +bugsnag +mkdocs-meta-manager +torchlibrosa +salesforce-fuelsdk-sans +cached-path +boto3-stubs-lite +python-benedict +pytest-archon +eyes-common +django-migration-linter +opentelemetry-instrumentation-falcon +msgpack-numpy +django-tables2 +mariadb +mypy-boto3-transfer +duo-client +jsonalias +sphinx-reredirects +mkdocs-link-marker +pytensor +mypy-boto3-mwaa +torchao +openvino-telemetry +asyncio-throttle +mypy-boto3-pinpoint +halo +tinysegmenter +robotframework-pabot +aws-embedded-metrics +pytest-find-dependencies +pytest-pylint +google-cloud-asset +uncalled +autofaker +tbats +yaml-config +json-stream +mkdocs-auto-tag-plugin +eyes-selenium +matrix-client +mypy-boto3-es +jax-cuda12-pjrt +lief +google-cloud-os-config +usaddress-scourgify +robotframework-stacktrace +markuppy +tentaclio-postgres +mypy-boto3-sagemaker-runtime +m3u8 +returns +mypy-boto3-resourcegroupstaggingapi +pytest-shard +mypy-boto3-polly +mypy-boto3-sesv2 +google-cloud-org-policy +jsonpath +angr +mypy-boto3-organizations +mypy-boto3-qbusiness +tensorflow-decision-forests +boost-histogram +markdown-graphviz-inline +mypy-boto3-quicksight +mypy-boto3-medialive +lorem +jupyter-cache +pandarallel +py-rust-stemmers +sphinx-notfound-page +mypy-boto3-ce +langchain-mistralai +django-rest-polymorphic +chdb +django-htmx +mypy-boto3-synthetics +growthbook +rapidocr-onnxruntime +methoddispatch +flake8-quotes +yara-python +pytest-ansible +mypy-boto3-s3tables +gitlint-core +mypy-boto3-workspaces +zizmor +pyroscope-io +cheetah3 +netsuitesdk +apify-client +mypy-boto3-cloudtrail +requests-html +semantic-link-sempy +python-jsonpath +django-safedelete +language-tags +aioconsole +construct-typing +aws-cdk-asset-node-proxy-agent-v5 +docx2pdf +mypy-boto3-mediaconvert +mypy-boto3-mediatailor +pyqtgraph +docstring-parser-fork +bleak +pybase62 +volcengine-python-sdk +asyncclick +pymorphy3-dicts-ru +pymorphy3 +tree-sitter-cpp +prefixed +clikit +mypy-boto3-timestream-query +gitlint +xlutils +azureml-dataprep-native +tbb +ably +mypy-boto3-connect +mypy-boto3-kendra +mypy-boto3-iot +django-ckeditor +databricks-feature-store +msgpack-types +mwparserfromhell +shopifyapi +pytest-plus +stix2-patterns +tree-sitter-xml +ajsonrpc +pyaudio +ada-url +luqum +mkdocs-minify-plugin +livereload +graphiti-core +tox-gh-actions +livekit-plugins-openai +awsiotsdk +google-python-cloud-debugger +google-cloud-access-context-manager +pyrogram +robocorp-vault +mypy-boto3-sso-admin +django-dotenv +fabric-analytics-notebook-plugin +fabric-analytics-sdk +mypy-boto3-sso +setuptools-scm-git-archive +cdp-use +mypy-boto3-transcribe +enlighten +emails +mypy-boto3-license-manager +mypy-boto3-guardduty +mypy-boto3-redshift +ipyparallel +hachoir +pyspellchecker +fiddle +exit-codes +cosmic-ray +fast-depends +opentelemetry-instrumentation-mysql +openinference-instrumentation-langchain +feu +mypy-boto3-dms +cantools +h3-pyspark +smmap2 +openapi-python-client +urlobject +pyactiveresource +insightface +databricks-labs-remorph +simplefix +azureml-train-core +clipboard +uhi +email-reply-parser +pymisp +localstack-client +img2pdf +litellm-enterprise +mypy-boto3-ecr-public +fake-http-header +distro2sbom +kedro +mypy-boto3-apigatewaymanagementapi +opentelemetry-instrumentation-pyramid +flask-testing +mypy-boto3-appconfig +tls-client +dagster-gcp +mypy-boto3-codepipeline +pydantic-to-typescript +hurry-filesize +testrail-api +oslo-concurrency +mypy-boto3-datazone +mypy-boto3-apigatewayv2 +gnureadline +unleashclient +fastapi-users +mypy-boto3-gamelift +brickflows +mypy-boto3-timestream-write +adbc-driver-postgresql +dpkt +langgraph-checkpoint-sqlite +tf-playwright-stealth +mypy-boto3-storagegateway +mypy-boto3-devicefarm +mypy-boto3-deadline +mypy-boto3-opensearch +mypy-boto3-route53resolver +platformio +kestra +dawg-python +mypy-boto3-aiops +mypy-boto3-ivs-realtime +mypy-boto3-identitystore +mypy-boto3-ebs +mapclassify +snuggs +geckodriver-autoinstaller +tempora +svix +cchardet +selinux +starrocks +zope-schema +slacker +hidapi +mypy-boto3-budgets +sgp4 +aiopg +pystan +redditwarp +aiosonic +onecache +check-manifest +dbt-athena +mypy-boto3-ram +mypy-boto3-marketplace-entitlement +flasgger +mypy-boto3-service-quotas +python-novaclient +mypy-boto3-fsx +onnx-ir +mypy-boto3-discovery +mkdocs-mermaid2-plugin +mypy-boto3-securityhub +mypy-boto3-mgh +apify-shared +tf-nightly +mypy-boto3-route53domains +clize +mypy-boto3-rds-data +quantulum3 +mypy-boto3-acm-pca +awkward +mypy-boto3-bedrock-data-automation +mypy-boto3-connectcases +mypy-boto3-meteringmarketplace +mypy-boto3-appsync +mypy-boto3-servicecatalog +chromedriver-autoinstaller +mypy-boto3-iot-data +apache-airflow-providers-opsgenie +kafka-python-ng +mypy-boto3-taxsettings +types-enum34 +pyfzf +cw-rpa +pytest-freezer +mypy-boto3-supplychain +asn1 +tgcrypto +darkdetect +nbstripout +mypy-boto3-dsql +mypy-boto3-comprehendmedical +mypy-boto3-translate +zope-deferredimport +wfdb +mypy-boto3-iotsecuretunneling +pismosendlogs +python-graphql-client +langgraph-runtime-inmem +mypy-boto3-compute-optimizer +pwdlib +numpy-typing-compat +mypy-boto3-mediaconnect +sqlean-py +inference-cli +stix2 +mypy-boto3-lightsail +mypy-boto3-wafv2 +treq +sqlite-vec +mypy-boto3-chime +od +mypy-boto3-ds +django-colorfield +opentelemetry-propagator-ot-trace +mypy-boto3-pi +chainlit +mypy-boto3-dlm +mypy-boto3-cloudhsmv2 +aiomqtt +a2a-sdk +kedro-datasets +bittensor-cli +hist +mypy-boto3-elb +zigpy +types-emoji +pydeseq2 +mypy-boto3-fms +types-networkx +torchinfo +mypy-boto3-comprehend +mypy-boto3-iotsitewise +geomdl +sagemaker-scikit-learn-extension +types-boto3-dynamodb +ascii-magic +effdet +langchain-pinecone +mypy-boto3-detective +lkml +mypy-boto3-pcs +netmiko +types-boto3-sqs +acryl-datahub-airflow-plugin +mypy-boto3-sso-oidc +mypy-boto3-directconnect +archinfo +mypy-boto3-cleanrooms +scrapli +elasticsearch8 +mypy-boto3-payment-cryptography +pytest-nunit +assertpy +apache-airflow-providers-apache-beam +mypy-boto3-dynamodbstreams +langchainhub +pennylane-lightning +adbc-driver-manager +elasticsearch7 +mypy-boto3-customer-profiles +histoprint +sqlite-utils +mypy-boto3-auditmanager +mypy-boto3-account +importlab +anywidget +python-amazon-sp-api +mypy-boto3-pinpoint-sms-voice-v2 +mypy-boto3-ec2-instance-connect +types-boto3-ec2 +keplergl +mypy-boto3-codedeploy +pycarlo +azure-mgmt-kusto +python-swiftclient +mypy-boto3-apprunner +pyct +mypy-boto3-verifiedpermissions +better-profanity +publicsuffixlist +cxxfilt +pymannkendall +pypeln +mypy-boto3-support +polars-lts-cpu +cloudwatch +pyvisa +types-boto3-rds +yarn-api-client +types-futures +forbiddenfruit +mypy-boto3-keyspaces +opentelemetry-instrumentation-click +mypy-boto3-redshift-serverless +core-universal +arize +django-pgmigrate +pytest-reportlog +mypy-boto3-payment-cryptography-data +mypy-boto3-kafkaconnect +mypy-boto3-arc-zonal-shift +zipfile-deflate64 +mypy-boto3-grafana +mypy-boto3-rekognition +simple-pid +cmsis-pack-manager +mypy-boto3-cleanroomsml +mypy-boto3-application-signals +mypy-boto3-backup +hl7 +mypy-boto3-connectparticipant +mypy-boto3-billingconductor +mypy-boto3-workspaces-thin-client +mypy-boto3-controltower +ntplib +aws-kinesis-agg +lcov-cobertura +lomond +mypy-boto3-resource-groups +mypy-boto3-neptune +mypy-boto3-cost-optimization-hub +mypy-boto3-artifact +mypy-boto3-codeconnections +mypy-boto3-sagemaker-metrics +mypy-boto3-mediapackagev2 +mypy-boto3-iotfleetwise +impacket +rpaframework-core +mypy-boto3-qconnect +mypy-boto3-cloudsearch +types-aiobotocore-rds +jplephem +mypy-boto3-emr-containers +flpc +mypy-boto3-managedblockchain-query +mypy-boto3-servicediscovery +mypy-boto3-notifications +mypy-boto3-resource-explorer-2 +mypy-boto3-securitylake +mypy-boto3-cloudsearchdomain +mypy-boto3-oam +geojson-pydantic +claripy +mypy-boto3-marketplace-deployment +mypy-boto3-mailmanager +mypy-boto3-marketplace-reporting +mypy-boto3-geo-places +mypy-boto3-elasticbeanstalk +aioesphomeapi +mypy-boto3-observabilityadmin +mypy-boto3-codecommit +django-modeltranslation +mypy-boto3-appstream +pytest-parallel +mypy-boto3-appconfigdata +pemja +mypy-boto3-pinpoint-email +mypy-boto3-waf +mypy-boto3-docdb +mypy-boto3-sdb +mypy-boto3-workmail +cle +mypy-boto3-amplify +lance-namespace-urllib3-client +mypy-boto3-shield +mypy-boto3-serverlessrepo +mypy-boto3-workmailmessageflow +mypy-boto3-glacier +mypy-boto3-cognito-sync +mypy-boto3-location +mypy-boto3-waf-regional +mypy-boto3-mediastore +mypy-boto3-mq +mypy-boto3-appmesh +mypy-boto3-kinesisanalyticsv2 +mypy-boto3-lex-models +mypy-boto3-swf +mypy-boto3-inspector +airflow-clickhouse-plugin +mypy-boto3-personalize-events +mypy-boto3-qldb-session +mypy-boto3-pinpoint-sms-voice +mypy-boto3-application-insights +mypy-boto3-workdocs +mypy-boto3-braket +mypy-boto3-chime-sdk-meetings +mypy-boto3-health +mypy-boto3-autoscaling-plans +mypy-boto3-marketplace-catalog +mypy-boto3-datasync +mypy-boto3-kinesis-video-media +mypy-boto3-cur +mypy-boto3-marketplacecommerceanalytics +mypy-boto3-mediapackage-vod +mypy-boto3-personalize +mypy-boto3-codestar-notifications +mypy-boto3-iotevents +mypy-boto3-iotanalytics +mypy-boto3-mediastore-data +mypy-boto3-dax +mypy-boto3-accessanalyzer +mypy-boto3-managedblockchain +mypy-boto3-globalaccelerator +mypy-boto3-savingsplans +mypy-boto3-lex-runtime +mypy-boto3-outposts +mypy-boto3-kinesisanalytics +unstructured-inference +mypy-boto3-qldb +pvlib +mypy-boto3-mturk +mypy-boto3-clouddirectory +mypy-boto3-forecast +pystac +mypy-boto3-iotthingsgraph +mypy-boto3-migrationhub-config +mypy-boto3-robomaker +mypy-boto3-kinesisvideo +mypy-boto3-iotevents-data +mypy-boto3-elastictranscoder +mypy-boto3-personalize-runtime +pythainlp +mypy-boto3-ssm-sap +mypy-boto3-cloud9 +mypy-boto3-mediapackage +mypy-boto3-machinelearning +mypy-boto3-groundstation +mypy-boto3-snowball +mypy-boto3-datapipeline +google-cloud-functions +mypy-boto3-greengrass +mypy-boto3-kinesis-video-signaling +mypy-boto3-kinesis-video-archived-media +mypy-boto3-iot-jobs-data +mypy-boto3-sagemaker-a2i-runtime +segno +mypy-boto3-importexport +mypy-boto3-macie2 +mypy-boto3-codestar-connections +mypy-boto3-codeguru-reviewer +mypy-boto3-networkmanager +mypy-boto3-cloudhsm +mypy-boto3-frauddetector +pyro-ppl +mypy-boto3-forecastquery +mypy-boto3-codeguruprofiler +json-schema-for-humans +mypy-boto3-network-firewall +mypy-boto3-iot-managed-integrations +apipkg +ntc-templates +apeye-core +sphinx-gallery +rauth +binapy +mypy-boto3-evs +scalecodec +mypy-boto3-healthlake +faststream +mypy-boto3-sagemaker-featurestore-runtime +extra-streamlit-components +opentelemetry-instrumentation-pymemcache +chess +types-boto3-cloudformation +mypy-boto3-wellarchitected +pydotplus +mypy-boto3-ivs +murmurhash2 +mypy-boto3-amp +ta +tonyg-rfc3339 +mypy-boto3-amplifybackend +mypy-boto3-iotwireless +mypy-boto3-greengrassv2 +mypy-boto3-s3outposts +django-libsass +mypy-boto3-lexv2-runtime +forex-python +mypy-boto3-appintegrations +wmi +mypy-boto3-devops-guru +az-cli +mypy-boto3-sagemaker-edge +apache-flink +mypy-boto3-databrew +types-boto3-lambda +sql-formatter +rebulk +google-cloud-appengine-admin +mypy-boto3-servicecatalog-appregistry +mypy-boto3-iotdeviceadvisor +mypy-boto3-lookoutvision +mypy-boto3-connect-contact-lens +mdutils +mypy-boto3-iotfleethub +mergepythonclient +flask-apscheduler +mypy-boto3-lexv2-models +babelfish +meraki +asdf +mypy-boto3-fis +mypy-boto3-opensearchserverless +mypy-boto3-lookoutequipment +guessit +ldaptor +mypy-boto3-vpc-lattice +mediapy +mypy-boto3-lookoutmetrics +mypy-boto3-finspace-data +mypy-boto3-finspace +mypy-boto3-applicationcostprofiler +opentelemetry-instrumentation-aiopg +mypy-boto3-ssm-contacts +mypy-boto3-cloudcontrol +sklearn2pmml +secure +codecov-cli +cbor +mypy-boto3-chime-sdk-messaging +python-fcl +mypy-boto3-mgn +mypy-boto3-memorydb +backports-ssl-match-hostname +mypy-boto3-ssm-incidents +neptune-client +dagster-spark +mypy-boto3-proton +dagster-celery-k8s +opentelemetry-processor-baggage +simplegeneric +types-aiobotocore-cloudformation +mypy-boto3-geo-routes +mypy-boto3-omics +prefect-kubernetes +mypy-boto3-chime-sdk-identity +progressbar +langid +mypy-boto3-bedrock-agentcore +django-fernet-fields-v2 +mypy-boto3-inspector2 +mypy-boto3-wisdom +mypy-boto3-voice-id +shellcheck-py +mypy-boto3-route53-recovery-cluster +mypy-boto3-marketplace-agreement +pandoc +mypy-boto3-workspaces-web +bezier +mypy-boto3-route53-recovery-readiness +plyfile +mypy-boto3-rum +mypy-boto3-chime-sdk-voice +mypy-boto3-route53-recovery-control-config +mypy-boto3-snow-device-management +webrtcvad-wheels +mcp-server-odoo +robocorp-storage +mypy-boto3-amplifyuibuilder +mypy-boto3-appfabric +llama-index-llms-azure-openai +mypy-boto3-backup-gateway +mypy-boto3-support-app +django-coverage-plugin +mypy-boto3-evidently +mypy-boto3-cloudtrail-data +mypy-boto3-panorama +mypy-boto3-tnb +mypy-boto3-osis +biothings-client +mypy-boto3-chime-sdk-media-pipelines +mypy-boto3-rolesanywhere +mypy-boto3-migration-hub-refactor-spaces +nose2 +pytest-wake +zake +mypy-boto3-rbin +mypy-boto3-b2bi +python-json-config +mypy-boto3-migrationhubstrategy +mypy-boto3-drs +mypy-boto3-medical-imaging +mypy-boto3-kendra-ranking +mypy-boto3-codecatalyst +mypy-boto3-iottwinmaker +mypy-boto3-resiliencehub +mypy-boto3-license-manager-user-subscriptions +mypy-boto3-bcm-data-exports +mypy-boto3-pipes +python-debian +mypy-boto3-connectcampaigns +mypy-boto3-codeguru-security +mypy-boto3-entityresolution +mypy-boto3-timestream-influxdb +mypy-boto3-chatbot +gptcache +mypy-boto3-trustedadvisor +mypy-boto3-bcm-pricing-calculator +customtkinter +mypy-boto3-ivschat +mypy-boto3-neptune-graph +mypy-boto3-m2 +mypy-boto3-bedrock-data-automation-runtime +mypy-boto3-simspaceweaver +mypy-boto3-cloudfront-keyvaluestore +mypy-boto3-license-manager-linux-subscriptions +www-authenticate +mypy-boto3-freetier +mypy-boto3-sagemaker-geospatial +pyobjc-framework-quartz +mypy-boto3-apptest +mypy-boto3-internetmonitor +mypy-boto3-backupsearch +mypy-boto3-docdb-elastic +mypy-boto3-billing +mypy-boto3-controlcatalog +jwskate +mypy-boto3-geo-maps +mypy-boto3-socialmessaging +mypy-boto3-pca-connector-ad +mypy-boto3-eks-auth +mypy-boto3-kinesis-video-webrtc-storage +mypy-boto3-inspector-scan +mypy-boto3-migrationhuborchestrator +mypy-boto3-invoicing +mypy-boto3-launch-wizard +vadersentiment +coola +mypy-boto3-workspaces-instances +mypy-boto3-route53profiles +mypy-boto3-networkmonitor +mypy-boto3-qapps +mypy-boto3-neptunedata +mypy-boto3-networkflowmonitor +mypy-boto3-connectcampaignsv2 +mypy-boto3-repostspace +mypy-boto3-ssm-quicksetup +mypy-boto3-pca-connector-scep +gevent-websocket +yapsy +mypy-boto3-security-ir +abnf +mypy-boto3-ds-data +mypy-boto3-partnercentral-selling +pykmip +mypy-boto3-notificationscontacts +proxy-protocol +segmentation-models-pytorch +validator-collection +greenback +dbt-clickhouse +twirp +mypy-boto3-keyspacesstreams +mygene +nutter +python-prctl +uproot +anyscale +jaraco-collections +django-nested-admin +pycocoevalcap +sqlalchemy-continuum +crc +cement +datadog-checks-base +mypy-boto3-gameliftstreams +pyserial-asyncio +zope-proxy +tcod +cerebras-cloud-sdk +notifiers +molecule-plugins +highspy +curatorbin +ocspbuilder +apache-airflow-providers-apache-livy +opentelemetry-instrumentation-openai-agents +pyserde +mypy-boto3-ssm-guiconnect +evergreen-lint +mautrix +mypy-boto3 +cachy +pytest-mypy +asyncio-mqtt +ocspresponder +coincurve +pycld2 +types-termcolor +currencyconverter +seqeval +lance-namespace +token-bucket +requests-oauth +py-vapid +scim2-filter-parser +mdformat-tables +types-flask-cors +sphinx-prompt +cloudinary +typesense +mypy-boto3-mpa +testing-common-database +spacy-curated-transformers +meteostat +python-monkey-business +bravado +pip-hello-world +pinecone-plugin-inference +cursor +application-properties +python-liquid +cmakelang +bump-my-version +textile +python-statemachine +ddgs +morefs +logger +bio +jsonformatter +grain +feast +opentelemetry-instrumentation-remoulade +tencentcloud-sdk-python +opentelemetry-instrumentation-cassandra +qudida +mozilla-django-oidc +intel-openmp +opentelemetry-instrumentation-aiohttp-server +java-manifest +pyseccomp +blendmodes +apache-airflow-providers-atlassian-jira +pyttsx3 +x-transformers +certvalidator +rpaframework-pdf +redlock-py +prefect-dbt +tos +libhoney +unicodedata2 +fastapi-slim +pymatreader +djhtml +pytest-reportportal +pyjarowinkler +pymeta3 +postmarker +skyfield +mcp-server-time +aiotask-context +patch +bleak-retry-connector +dghs-imgutils +numpyro +fasttext-langdetect +simpleitk +perlin-noise +autogen-agentchat +flake8-bandit +pyvim +ta-lib +mypy-boto3-arc-region-switch +resize-right +mypy-boto3-bedrock-agentcore-control +mypy-boto3-s3vectors +django-fake-model +django-rq +boto3-stubs-full +types-defusedxml +mypy-boto3-odb +gprofiler-official +catkin-pkg +target-hotglue +django-admin-inline-paginator +hyperbrowser +influxdb3-python +subliminal +django-fsm +apache-flink-libraries +qiskit-aer +doit +enzyme +aws-assume-role-lib +csscompressor +pydivert +isoweek +botorch +flake8-broken-line +testing-postgresql +tree-sitter-html +django-dirtyfields +remote-pdb +python-logstash +jupyter-contrib-nbextensions +liccheck +imap-tools +taskflow +azureml-telemetry +tree-sitter-css +gpytorch +pymarkdownlnt +rerun-sdk +locust-plugins +cherrypy +prefect-sqlalchemy +esptool +django-cleanup +streamlit-extras +gdbmongo +mne-bids +django-constance +unsloth-zoo +pytest-sftpserver +edgegrid-python +ordereddict +stemming +connect-python +casadi +apeye +imath +knowit +hepunits +tree-sitter-json +quart-cors +smbus2 +trakit +sqlite-fts4 +xmltojson +cons +equinox +particle +django-json-widget +easing-functions +htmltools +tree-sitter-markdown +spark-expectations +etuples +tree-sitter-sql +httpx-retries +tree-sitter-regex +qwen-vl-utils +policyuniverse +pathtools +gcloud-aio-datastore +openevals +pylint-gitlab +mpld3 +ibm-db-sa +dagster-dg-cli +fickling +pywebpush +alexapy +pyrfc6266 +pytype +landlock +favicon +ytsaurus-client +names +pydantic-avro +azureml-pipeline-core +schematics +property-manager +tree-sitter-toml +salt-lint +mmengine +django-loginas +traits +gcloud-rest-datastore +atproto +mcap +grequests +sqlakeyset +azure-eventhub-checkpointstoreblob-aio +libretranslatepy +python-interface +airflow-dbt +livekit-plugins-deepgram +jieba3k +serverless-wsgi +text2digits +auditwheel +splink +pyminizip +pylru +pyspark-hnsw +arnparse +fixtures +django-postgres-copy +mnemonic +py-grpc-prometheus +translate +bert-score +great-expectations-experimental +evdev +cint +faust-streaming +flake8-annotations +types-jmespath +ipyevents +hl7apy +wagtail +metaflow +gcloud-aio-taskqueue +rstcheck-core +feedfinder2 +distrax +asdf-standard +python-miio +wincertstore +pyro-api +e2b-code-interpreter +robotframework-assertion-engine +importlib +econml +boruta +libipld +keybert +teamcity-messages +asammdf +ase +opentelemetry-exporter-jaeger +sodapy +portion +odxtools +utm +executor +functools32 +fastapi-users-db-sqlalchemy +python-string-utils +zope-component +symengine +ytsaurus-yson +plexapi +kr8s +gcloud-rest-taskqueue +transliterate +css-inline +flask-oauthlib +rpy2 +dataclasses-json-speakeasy +pydevd-pycharm +bayesian-optimization +dspy-ai +portend +autoray +autogen-core +hbutils +pybreaker +opentelemetry-instrumentation-aiokafka +google-reauth +pandas-datareader +unstructured-pytesseract +psycopg-c +oic +dm-control +edfio +prov +clearml +spotipy +filesplit +schema-salad +pybit +minikanren +jupyter-contrib-core +databricks-dlt +gcloud-rest-bigquery +linear-operator +zope-hookable +py-machineid +bindep +qdldl +esp-idf-monitor +sparkdantic +torch-model-archiver +entrypoint2 +django-cte +ast-grep-cli +urwid-readline +pytd +sqlalchemy-cockroachdb +pandas-flavor +inscriptis +eeglabio +graphlib-backport +algoliasearch-django +cmake-build-extension +imgtool +pysnooper +robotframework-seleniumtestability +ping3 +marimo +localstack-ext +ast-grep-py +pythran +interpret-core +sqlalchemy-databricks +line-bot-sdk +dagster-snowflake +tf2onnx +pybv +pytest-eventlet +pymap3d +camelot-py +transformers-stream-generator +osmnx +django-money +pyarmor-cli-core +torch-tb-profiler +robotframework-browser +imutils +pilmoji +pyarmor +pgsanity +moderngl +dagster-dg-core +delighted +django-adminplus +torch-xla +mode-streaming +python-gflags +routes +bchlib +arize-phoenix-evals +stream-zip +flask-sock +dvc-s3 +flameprof +axiom-py +autodocsumm +pyxirr +py-consul +hfutils +robotframework-robocop +aiologic +paypalrestsdk +types-greenlet +g2p-en +sqloxide +simple-term-menu +yellowbrick +pystoi +java-access-bridge-wrapper +octodns +pytest-parametrized +sqlmesh +fluent-runtime +edge-tts +setuptools-download +measurement +dvclive +pyjokes +httpx-oauth +taskipy +requests-oauth2client +stream-python +python-statsd +segtok +sdcclient +pynput-robocorp-fork +nemo-toolkit +eccodes +pysimdjson +acres +llama-index-embeddings-azure-openai +awsebcli +pytest-lazy-fixtures +comet-ml +wtforms-json +robotframework-jsonlibrary +sphinx-togglebutton +cpplint +nacos-sdk-python +django-multiselectfield +tk +sshconf +pytest-alembic +pyrefly +tensorboard-plugin-profile +pytest-qt +prospector +pytest-doctestplus +prisma +databricks-pypi-extras +transforms3d +nicegui +aws-opentelemetry-distro +extension-helpers +pydevd +kopf +pygrib +tox-ansible +azure-mgmt-managedservices +jcs +escapism +captum +glcontext +xlwings +requirements-detector +kafka +torch-fidelity +anyconfig +azureml-train-automl-client +python-neutronclient +types-boto3-ses +dodgy +pygltflib +llama-cpp-python +rangehttpserver +gcs-oauth2-boto-plugin +databricks-ai-bridge +google-cloud-dns +scikeras +markdown-exec +curated-tokenizers +asdf-transform-schemas +bedrock-agentcore +opensearch-dsl +pysfeel +python-dynamodb-lock +ics +googlesearch-python +esprima +lilcom +types-aiobotocore-sns +vl-convert-python +types-boto3-iam +nipype +google-cloud-bigquery-connection +rio-cogeo +types-google-cloud-ndb +sqlalchemy-pytds +notify-run +xlsx2csv +djangorestframework-role-filters +obstore +maya +typos +pylint-quotes +devicecheck +datadog-logger +blingfire +maggma +django-deprecation +lhotse +akshare +jinjanator-plugins +jinjanator +django-tinymce +adbc-driver-sqlite +tensorflow-intel +google-cloud-billing +lazy +mailgun +openmeteo-requests +pyaml-env +mcpo +django-rest-swagger +onnxsim +django-vite +cdk-ecr-deployment +apache-airflow-providers-elasticsearch +ibm-vpc +pyzabbix +robotframework-databaselibrary +awslabs-aws-documentation-mcp-server +airtable +pytest-pretty +openmeteo-sdk +paypalhttp +td-client +ibm-secrets-manager-sdk +dissect-target +alpaca-trade-api +sarif-tools +types-pexpect +ezdxf +tlslite-ng +types-pycurl +codeguru-profiler-agent +stagehand +silero-vad +pyobjc-framework-applicationservices +mp-api +tika +draftjs-exporter +beniget +aiohttp-jinja2 +colourmap +sphinxext-opengraph +langchain-ibm +libtpu +ipy +curated-transformers +pyobjc-framework-coretext +loro +pockets +suds +mobly +authcaptureproxy +tomesd +mlforecast +doc8 +nbdime +stdeb +pytest-flake8 +types-mypy-extensions +types-docker +pycron +dj-rest-auth +testcontainers-core +optbinning +sqladmin +livekit-plugins-turn-detector +cowsay +rply +keystonemiddleware +findlibs +nuitka +langgraph-supervisor +quadprog +githubpy +retry-decorator +streamlit-keyup +google-ads-admanager +mdformat-gfm +django-user-agents +flask-swagger-ui +django-autocomplete-light +pyvers +ansible-base +autogluon-core +pytest-deadfixtures +openshift +apache-airflow-providers-papermill +mongomock-motor +meltanolabs-target-snowflake +sorl-thumbnail +ci-info +etelemetry +azure-schemaregistry-avroencoder +mysql-replication +prettyprinter +databricks-langchain +datazets +django-select2 +singlestoredb +gin-config +pyapacheatlas +apache-airflow-providers-samba +aiosmtpd +pykalman +meshio +arize-phoenix-client +polygon-api-client +monai +mplfinance +gemmi +ftputil +sphinxcontrib-napoleon +btrees +mmdet +tslearn +cvdupdate +jupyter-server-mathjax +spotinst-agent +daft +python-quickbooks +hl7parser +paypal-checkout-serversdk +secure-smtplib +seqio-nightly +apache-airflow-providers-hashicorp +mozfile +redfish +sphinx-click +extras +skops +langchain-tests +datarobot +python-jwt +mkdocs-include-markdown-plugin +playwright-stealth +splinebox +pygments-ansi-color +gmpy2 +mkl +delta-kernel-rust-sharing-wrapper +tzwhere +twofish +plotext +langchain-perplexity +pact-python +dagster-azure +clu +warp-lang +types-stripe +openinference-instrumentation-openai +mjml-python +sudachidict-full +flaml +chalice +oslo-policy +dbt-exasol +pypydispatcher +pyqrcode +pyvalid +ipydagred3 +aws-cdk-core +pyartifactory +pydrive +rosbags +databind-core +mlserver +pgspecial +pyagrum-nightly +httpagentparser +red-black-tree-mod +django-modelcluster +autogluon-features +awslabs-aws-api-mcp-server +streamlit-image-coordinates +mypy-baseline +azureml-pipeline-steps +squarify +databind-json +speedtest-cli +pybind11-stubgen +alphashape +netapp-ontap +rotary-embedding-torch +ringcentral +crhelper +bravado-core +ailment +ocrmypdf +pyautogen +requests-unixsocket2 +pydeprecate +pydomo +apache-airflow-providers-sendgrid +kedro-telemetry +pyathenajdbc +mypy-boto3-sms +aws-error-utils +inngest +graypy +spandrel-extra-arches +pysnmpcrypto +johnnydep +empy +ropwr +mux-python +djangorestframework-camel-case +pyfunctional +pysnyk +paddlex +hydra-colorlog +betterproto-fw +pyawscron +asyncstdlib-fw +aioodbc +controlnet-aux +pyobjc-framework-applescriptkit +pytest-clarity +pytest-embedded-serial-esp +weave +tools +compress-pickle +zope-i18n +mypy-boto3-opsworks +py-sr25519-bindings +pyobjc-framework-corebluetooth +jstyleson +wasmtime +http-ece +waiting +pyecharts +lizard +mypy-boto3-opsworkscm +kuzu +pdfrw2 +bzt +langchain-mongodb +willow +lap +tortoise-orm +apache-airflow-providers-github +visitor +pyobjc-framework-libdispatch +allure-behave +koheesio +langchain-deepseek +st-annotated-text +pyreadstat +cognite-sdk +scatterd +toml-sort +csv23 +django-jazzmin +pyiqa +django-auth-ldap +libusb1 +binary +coralogix-logger +requests-ratelimiter +attr +pyston +pca +oslo-privsep +azure-ai-textanalytics +backports-entry-points-selectable +github-action-utils +csv-diff +html-sanitizer +deap +cwcwidth +dash-ag-grid +optimizely-sdk +dynamic-yaml +everett +pyston-autoload +proto-google-cloud-datastore-v1 +stumpy +os-client-config +pytest-mock-resources +cirq-core +prefect-github +yggdrasil-engine +base64io +aiodogstatsd +implicit +jaxopt +sphinxcontrib-plantuml +wimpy +poster3 +mdformat-frontmatter +pystemmer +pgcli +bingads +fastcrc +pyobjc +python-glanceclient +types-gevent +oslo-middleware +livekit-blingfire +lakefs-client +sglang +edn-format +dash-mantine-components +iterators +tsdownsample +b2sdk +torchcodec +cython-lint +toronado +rio-tiler +missingpy +djangorestframework-xml +adjust-precision-for-schema +fastdownload +fillpdf +lpc-checksum +pymonetdb +powerline-shell +xmodem +pysqlite3-binary +latex2sympy2 +zope-i18nmessageid +unitycatalog-ai +azure-mgmt-databricks +timedelta +keras-nightly +flake8-commas +futurist +clickhouse-pool +jamo +number-parser +fugashi +elasticsearch-curator +wordninja +autogluon-tabular +django-recaptcha +target-jsonl +onnxmltools +pytest-embedded-serial +pandasai +django-mcp-server +azureml-train-restclients-hyperdrive +pytest-explicit +pandas-ta +pypika-tortoise +aerospike +backports-shutil-get-terminal-size +stpyv8 +persistent +dash-extensions +streamlit-card +tm1py +hogql-parser +scikit-plot +google-play-scraper +pdf2docx +simple-ddl-parser +getschema +collate-sqllineage +tailer +flask-pydantic +spglib +baron +logical-unification +fastdtw +unitycatalog-client +jsonschema2md +mdxpy +patchy +redbaron +mf2py +esp-idf-nvs-partition-gen +mygeotab +pyobjc-framework-security +django-log-request-id +icmplib +robocorp-log +mozterm +laspy +webexteamssdk +better-exceptions +graphql-server-core +tcmlib +pyobjc-framework-systemconfiguration +posthoganalytics +contextily +apispec-webframeworks +distance +hera +extruct +djangosaml2 +kaldiio +csvw +pydot-ng +botbuilder-schema +pycnite +plotly-resampler +easy-thumbnails +python-subunit +airflow-provider-lakefs +fal +pyobjc-framework-webkit +pot +streamlit-condition-tree +django-statsd +trustme +pytest-cover +pydevicetree +cinemagoer +braintrust-langchain +azure-iot-hub +aiomonitor +schemdraw +pyobjc-framework-fsevents +pgqueuer +gliner +sphinxcontrib-svg2pdfconverter +httpstan +pyflux +loman +python-snap7 +ncclient +streamlit-faker +hass-web-proxy-lib +intel-cmplr-lib-ur +aiodynamo +fuzzysearch +flask-flatpages +semantic-link-labs +pandas-read-xml +pyrdfa3 +pyobjc-framework-coreaudio +python-mimeparse +coffea +django-types +pandas-profiling +openfeature-sdk +pyobjc-framework-coreservices +localstack +pyu2f +sunshine-conversations-client +drf-exceptions-hog +botframework-connector +pytest-coverage +correctionlib +pygal +waao +nbqa +httpx-auth +pyobjc-framework-coremedia +pyvisa-py +zope-exceptions +brotli-asgi +livekit-plugins-noise-cancellation +llama-index-vector-stores-postgres +neptune-api +pyobjc-framework-coreml +statshog +prefect-shell +py-automapper +docx +dagit +deepl +openvino-dev +django-admin-autocomplete-filter +cfgrib +markdownlit +nvitop +pymc3 +backoff-utils +cppy +pyobjc-framework-corelocation +py-money +pedalboard +aistudio-sdk +pulumi-gcp +marshmallow-union +lzfse +circular-dict +pyobjc-framework-avfoundation +pyobjc-framework-vision +reward-kit +pyobjc-framework-contacts +pyobjc-framework-cfnetwork +autogen-ext +bpyutils +rospkg +path-py +zope-security +webrtcvad +conditional-cache +pyatlan +email-to +streamlit-embedcode +pipupgrade +pyobjc-framework-addressbook +json-spec +mido +pytest-variables +pyobjc-framework-coredata +nbval +pyobjc-framework-automator +pylint-celery +bz2file +pyobjc-framework-photos +pyobjc-framework-syncservices +pyobjc-framework-screensaver +vcver +telepath +pyobjc-framework-localauthentication +livekit-plugins-elevenlabs +pyobjc-framework-discrecording +fastapi-limiter +lapx +discord +pyobjc-framework-metal +ipex-llm +pyobjc-framework-coreaudiokit +wheel-filename +unicorn +creosote +pyobjc-framework-securityinterface +zope-configuration +pyjavaproperties3 +pyobjc-framework-spritekit +streamlit-camera-input-live +segments +pyobjc-framework-corewlan +pymoo +sccache +pyobjc-framework-avkit +imscore +timeago +pyobjc-framework-imagecapturecore +streamlit-toggle-switch +runpod +finnhub-python +valkey-glide +pyobjc-framework-mapkit +pyobjc-framework-cryptotokenkit +pyobjc-framework-gamecenter +pyobjc-framework-multipeerconnectivity +prompty +pyobjc-framework-storekit +pyobjc-framework-scenekit +pyobjc-framework-corespotlight +pyobjc-framework-modelio +pyobjc-framework-intents +pyobjc-framework-contactsui +yesqa +pyobjc-framework-networkextension +pyobjc-framework-photosui +pyobjc-framework-notificationcenter +pyobjc-framework-coremediaio +pyobjc-framework-gameplaykit +pyobjc-framework-gamekit +pyobjc-framework-externalaccessory +pyobjc-framework-safariservices +zope-testing +tsfresh +rules +tpu-info +flake8-debugger +traceback-with-variables +pyobjc-framework-mediatoolbox +pyobjc-framework-videotoolbox +pyobjc-framework-coremidi +pyobjc-framework-gamecontroller +unlzw3 +redisvl +flask-graphql +i18nice +djangoql +pyobjc-framework-fileprovider +pyobjc-framework-network +faust-cchardet +pyobjc-framework-usernotifications +pyobjc-framework-coremotion +swebench +pytest-trio +aws-cdk-cx-api +pyobjc-framework-launchservices +pyobjc-framework-metalperformanceshaders +fusepy +async-substrate-interface +types-babel +langchain-nvidia-ai-endpoints +types-aws-xray-sdk +pyobjc-framework-authenticationservices +pyobjc-framework-metalkit +pybind11-global +pyobjc-framework-automaticassessmentconfiguration +types-aiobotocore-elbv2 +allennlp +pyobjc-framework-speech +pyobjc-framework-oslog +pykcs11 +pyobjc-framework-pushkit +streamlit-vertical-slider +pycountry-convert +session-info +pyobjc-framework-systemextensions +pyobjc-framework-audiovideobridging +pyobjc-framework-accessibility +pyobjc-framework-applescriptobjc +monty +dukpy +botbuilder-core +unitycatalog-langchain +pyobjc-framework-classkit +pyobjc-framework-callkit +rejson +rocksdict +pyobjc-framework-passkit +property-cached +pyobjc-framework-replaykit +panns-inference +pyobjc-framework-virtualization +flake8-variables-names +haystack-ai +ffmpeg +telesign +pyobjc-framework-exceptionhandling +pyobjc-framework-libxpc +pyexcelerate +pyobjc-framework-iobluetooth +pyobjc-framework-scriptingbridge +pyobjc-framework-installerplugins +pyobjc-framework-latentsemanticmapping +sip +pyobjc-framework-preferencepanes +pyobjc-framework-intentsui +pdoc3 +pyobjc-framework-metrickit +pyobjc-framework-diskarbitration +pycln +pyobjc-framework-opendirectory +pyobjc-framework-securityfoundation +pyobjc-framework-shazamkit +pyobjc-framework-uniformtypeidentifiers +pyobjc-framework-searchkit +botframework-streaming +azureml-automl-core +pyobjc-framework-screencapturekit +pyobjc-framework-servicemanagement +sox +pyobjc-framework-discrecordingui +pyobjc-framework-dvdplayback +pyobjc-framework-osakit +pypd +pyobjc-framework-colorsync +python-igraph +django-test-migrations +hvplot +pyftdi +pyobjc-framework-eventkit +wagon +pylsp-mypy +hud-python +pycobertura +flake8-return +pyobjc-framework-accounts +json-flatten +tqdm-loggable +pyobjc-framework-cloudkit +pyobjc-framework-social +munkres +pyobjc-framework-iosurface +trustcall +pyobjc-framework-mediaplayer +pyobjc-framework-netfs +zope-location +pyobjc-framework-findersync +pyobjc-framework-mediaaccessibility +pyobjc-framework-medialibrary +pyobjc-framework-ituneslibrary +arq +pyxnat +pyobjc-framework-adsupport +pyobjc-framework-businesschat +xvfbwrapper +pyobjc-framework-naturallanguage +pysolr +pymacaroons +python-redmine +pyobjc-framework-videosubscriberaccount +pip-install-test +promptflow-tracing +pyobjc-framework-healthkit +ddtrace-api +asyncua +pyobjc-framework-avrouting +pyghmi +pyobjc-framework-corehaptics +llama-index-legacy +pyobjc-framework-sharedwithyoucore +pyobjc-framework-backgroundassets +django-permissionedforms +pyobjc-framework-executionpolicy +pyobjc-framework-fileproviderui +pyobjc-framework-metalfx +pyobjc-framework-extensionkit +pyobjc-framework-linkpresentation +pyobjc-framework-devicecheck +pyobjc-framework-pencilkit +pyobjc-framework-quicklookthumbnailing +certbot-dns-multi +pyobjc-framework-safetykit +pyobjc-framework-sharedwithyou +pyobjc-framework-soundanalysis +drf-jwt +sqlvalidator +telesignenterprise +objectpath +pyobjc-framework-apptrackingtransparency +pyobjc-framework-adservices +types-fpdf2 +faiss-gpu +pyobjc-framework-kernelmanagement +promptflow-devkit +pyobjc-framework-mlcompute +promptflow-core +pyobjc-framework-metalperformanceshadersgraph +pyobjc-framework-screentime +pyobjc-framework-usernotificationsui +django-cacheops +youtube-dl +pyobjc-framework-inputmethodkit +pytest-spark +zope-publisher +pyobjc-framework-iobluetoothui +pyjwkest +tempita +pyobjc-framework-datadetection +pyobjc-framework-mailkit +pyobjc-framework-localauthenticationembeddedui +nvidia-cudnn-frontend +testscenarios +autogluon-common +dydantic +pyicu-binary +pytest-race +internetarchive +telebot +mysql +py-bip39-bindings +pyreadline +ipaddr +dlinfo +pyobjc-framework-phase +django-allow-cidr +nvidia-nvshmem-cu12 +pyiso8583 +oauth2 +multiprocessing-logging +pyspark-pandas +semantic-link +semantic-link-functions-validators +semantic-link-functions-meteostat +semantic-link-functions-holidays +semantic-link-functions-phonenumbers +awkward-cpp +veracode-api-signing +semantic-link-functions-geopandas +feature-engine +pygam +aqtinstall +pyobjc-framework-threadnetwork +sphinx-toolbox +ipycanvas +google-cloud-monitoring-dashboards +serpent +aiven-client +zulip +vector +icalevents +yagmail +jupyterlab-git +redis-sentinel-url +pyobjc-framework-dictionaryservices +jsonslicer +pythran-openblas +bibtexparser +pyicu +pyobjc-framework-collaboration +awscli-plugin-s3-proxy +mteb +pyobjc-framework-instantmessage +pyobjc-framework-calendarstore +drf-writable-nested +backports-abc +pyobjc-framework-carbon +zope-browser +mmtf-python +prefect-slack +interrogate +aws-cdk-aws-iam +zope-contenttype +bm25s +spanishconjugator +qpd +types-orjson +pyobjc-framework-browserenginekit +autologging +panda3d +django-crum +swapper +pygount +mplhep +autogluon +saxonche +bloomfilter-py +lazify +textract +robotframework-retryfailed +clip-interrogator +impit +onnxslim +nmslib +haystack-experimental +nbmake +djangorestframework-jwt +py-ed25519-zebra-bindings +django-rest-knox +tts +flask-apispec +portkey-ai +nvidia-ml-py3 +gdal +jsonseq +rq-dashboard +duplocloud-client +arm-pyart +color-matcher +flask-moment +colorzero +liger-kernel +pyobjc-framework-cinematic +dagster-pyspark +pyobjc-framework-sensitivecontentanalysis +docopt-ng +pyobjc-framework-symbols +cpuset-py3 +logfury +clearml-agent +brazilnum +statistics +texterrors +nptyping +sec-api +geonamescache +pytrends +gpiozero +teradataml +yoyo-migrations +deprecat diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/internal/__init__.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/internal/__init__.py new file mode 100644 index 000000000..04f8b7b76 --- /dev/null +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/internal/__init__.py @@ -0,0 +1,2 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/internal/packages_resolver.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/internal/packages_resolver.py new file mode 100644 index 000000000..209dfa485 --- /dev/null +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/internal/packages_resolver.py @@ -0,0 +1,382 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Package discovery and classification module for OpenTelemetry code correlation. + +This module provides utilities to: +- Classify Python code as standard library, third-party, or user code +- Map file paths to their corresponding Python packages +- Cache package metadata for performance optimization +- Support code correlation features in AWS OpenTelemetry distribution +""" + +import logging +import sys +import sysconfig +import threading +from functools import lru_cache, wraps +from inspect import FullArgSpec, getfullargspec, isgeneratorfunction +from pathlib import Path +from typing import Any, Callable, Dict, List, NamedTuple, Optional, Set, Union + +from ..config import AwsCodeCorrelationConfig + +# Module-level constants +_logger = logging.getLogger(__name__) + +# Configuration +_code_attributes_config = AwsCodeCorrelationConfig.from_env() + +# Global caching variables +_sys_path_hash: Optional[int] = None +_resolved_sys_path: List[Path] = [] +_sys_path_lock = threading.Lock() + +# Standard library paths (computed once at module load) +_STDLIB_PATH = Path(sysconfig.get_path("stdlib")).resolve() +_PLATSTDLIB_PATH = Path(sysconfig.get_path("platstdlib")).resolve() +_PURELIB_PATH = Path(sysconfig.get_path("purelib")).resolve() +_PLATLIB_PATH = Path(sysconfig.get_path("platlib")).resolve() + + +class Distribution(NamedTuple): + """Represents a Python distribution with name and version.""" + + name: str + version: str + + +def _validate_void_function(func: Callable, argspec: FullArgSpec) -> bool: + """Check if a function has no arguments or special characteristics.""" + return not ( + argspec.args + or argspec.varargs + or argspec.varkw + or argspec.defaults + or argspec.kwonlyargs + or argspec.kwonlydefaults + or isgeneratorfunction(func) + ) + + +def execute_once(func: Callable) -> Callable: + """ + Decorator that ensures a function is executed only once. + + Args: + func: Function to be decorated (must have no arguments) + + Returns: + Wrapped function that caches its result + + Raises: + ValueError: If the function has arguments + """ + argspec = getfullargspec(func) + if not _validate_void_function(func, argspec): + raise ValueError("The execute_once decorator can only be applied to functions with no arguments") + + @wraps(func) + def wrapper() -> Any: + try: + result, exception = func.__execute_once_result__ # type: ignore[attr-defined] + except AttributeError: + try: + result = func() + exception = None + except Exception as error: # pylint: disable=broad-exception-caught + result = None + exception = error + func.__execute_once_result__ = result, exception # type: ignore[attr-defined] + + if exception is not None: + raise exception + + return result + + return wrapper + + +def _determine_effective_root(relative_path: Path, parent_path: Path) -> str: + """ + Determine the effective root module for a given path. + + Args: + relative_path: Path relative to the parent + parent_path: Parent directory path + + Returns: + Root module name + """ + base_name = relative_path.parts[0] + root_dir = parent_path / base_name + + if root_dir.is_dir() and (root_dir / "__init__.py").exists(): + return base_name + return str(Path(*relative_path.parts[:2])) + + +def _resolve_system_paths() -> List[Path]: + """ + Resolve and cache system paths from sys.path. + + Uses double-checked locking for thread safety while maintaining performance. + + Returns: + List of resolved Path objects from sys.path + """ + global _sys_path_hash, _resolved_sys_path # pylint: disable=global-statement + + current_hash = hash(tuple(sys.path)) + + # Fast path: check without lock (common case when no update needed) + if current_hash == _sys_path_hash: + return _resolved_sys_path + + # Slow path: acquire lock and double-check + with _sys_path_lock: + # Double-check inside lock in case another thread already updated + if current_hash != _sys_path_hash: + _sys_path_hash = current_hash + _resolved_sys_path = [Path(path).resolve() for path in sys.path] + + return _resolved_sys_path + + +@lru_cache(maxsize=256) +def _extract_root_module_name(file_path: Path) -> str: + """ + Extract the root module name from a file path. + + Args: + file_path: Path to the Python file + + Returns: + Root module name + + Raises: + ValueError: If root module cannot be determined + """ + # Try standard library paths first (most common case) + for parent_path in (_PURELIB_PATH, _PLATLIB_PATH): + try: + relative_path = file_path.resolve().relative_to(parent_path) + return _determine_effective_root(relative_path, parent_path) + except ValueError: + continue + + # Try sys.path resolution with shortest relative path priority + shortest_relative = None + best_parent = None + + for parent_path in _resolve_system_paths(): + try: + relative_path = file_path.relative_to(parent_path) + if shortest_relative is None or len(relative_path.parents) < len(shortest_relative.parents): + shortest_relative = relative_path + best_parent = parent_path + except ValueError: + continue + + if shortest_relative is not None and best_parent is not None: + try: + return _determine_effective_root(shortest_relative, best_parent) + except IndexError: + pass + + raise ValueError(f"Could not determine root module for path: {file_path}") + + +@execute_once +def _build_package_mapping() -> Optional[Dict[str, Distribution]]: + """ + Build mapping from root modules to their distributions. + + Returns: + Dictionary mapping module names to Distribution objects, or None if failed + """ + try: + import importlib.metadata as importlib_metadata # pylint: disable=import-outside-toplevel + + # Cache for namespace package detection + namespace_cache: Dict[str, bool] = {} + + def is_namespace_package(package_file: importlib_metadata.PackagePath) -> bool: + """Check if a package file belongs to a namespace package.""" + root = package_file.parts[0] + + if root in namespace_cache: + return namespace_cache[root] + + if len(package_file.parts) < 2: + namespace_cache[root] = False + return False + + located_file = package_file.locate() + if located_file is None: + namespace_cache[root] = False + return False + + parent_dir = Path(located_file).parents[len(package_file.parts) - 2] + is_namespace = parent_dir.is_dir() and not (parent_dir / "__init__.py").exists() + + namespace_cache[root] = is_namespace + return is_namespace + + package_mapping = {} + + for distribution in importlib_metadata.distributions(): + files = distribution.files + if not files: + continue + + metadata = distribution.metadata + dist_info = Distribution(name=metadata["name"], version=metadata["version"]) + + for file_path in files: + root_module = file_path.parts[0] + + # Skip metadata directories + if root_module.endswith((".dist-info", ".egg-info")) or root_module == "..": + continue + + # Handle namespace packages + if is_namespace_package(file_path): + root_module = "/".join(file_path.parts[:2]) + + # Only add if not already present (first distribution wins) + if root_module not in package_mapping: + package_mapping[root_module] = dist_info + + return package_mapping + + except Exception: # pylint: disable=broad-exception-caught + _logger.warning( + "Failed to build package mapping. Please report this issue to " + "https://github.com/aws/aws-otel-python-instrumentation/issues", + exc_info=True, + ) + return None + + +@execute_once +def _load_third_party_packages() -> Set[str]: + """ + Load the set of third-party package names from configuration. + + Returns: + Set of third-party package names + """ + try: + from importlib.resources import read_text # pylint: disable=import-outside-toplevel + + # Load package list from text file + content = read_text("amazon.opentelemetry.distro.code_correlation.internal", "3rd.txt") + package_names = set(content.splitlines()) + + # Apply configuration overrides + configured_packages = (package_names | set(_code_attributes_config.include)) - set( + _code_attributes_config.exclude + ) + + return configured_packages + + except Exception: # pylint: disable=broad-exception-caught + _logger.warning("Failed to load third-party packages configuration", exc_info=True) + return set() + + +@lru_cache(maxsize=20000) +def resolve_package_from_filename(filename: Union[str, Path]) -> Optional[Distribution]: + """ + Resolve a Python distribution from a file path. + + Args: + filename: Path to the Python file (string or Path object) + + Returns: + Distribution object if found, None otherwise + """ + package_mapping = _build_package_mapping() + if package_mapping is None: + return None + + try: + file_path = Path(filename) if isinstance(filename, str) else filename + root_module = _extract_root_module_name(file_path) + + # Try exact module match first + if root_module in package_mapping: + _logger.debug("Found distribution by exact match: %s", root_module) + return package_mapping[root_module] + + # Try distribution name match as fallback + for distribution in package_mapping.values(): + if distribution.name == root_module: + _logger.debug("Found distribution by name match: %s", distribution.name) + return distribution + + _logger.debug("No distribution found for module: %s", root_module) + return None + + except (ValueError, OSError) as error: + _logger.debug("Error resolving package for %s: %s", filename, error) + return None + + +@lru_cache(maxsize=256) +def is_standard_library(file_path: Path) -> bool: + """ + Check if a file path belongs to the Python standard library. + + Args: + file_path: Path to check + + Returns: + True if the path is in the standard library, False otherwise + """ + resolved_path = file_path + if not resolved_path.is_absolute() or resolved_path.is_symlink(): + resolved_path = resolved_path.resolve() + + # Check if in standard library paths but not in site-packages + is_in_stdlib = resolved_path.is_relative_to(_STDLIB_PATH) or resolved_path.is_relative_to(_PLATSTDLIB_PATH) + + is_in_site_packages = resolved_path.is_relative_to(_PURELIB_PATH) or resolved_path.is_relative_to(_PLATLIB_PATH) + + return is_in_stdlib and not is_in_site_packages + + +@lru_cache(maxsize=256) +def is_third_party_package(file_path: Path) -> bool: + """ + Check if a file path belongs to a third-party package. + + Args: + file_path: Path to check + + Returns: + True if the path belongs to a third-party package, False otherwise + """ + distribution = resolve_package_from_filename(file_path) + if distribution is None: + return False + + third_party_packages = _load_third_party_packages() + return distribution.name in third_party_packages + + +@lru_cache(maxsize=1024) +def is_user_code(file_path: str) -> bool: + """ + Check if a file path represents user code (not stdlib or third-party). + + Args: + file_path: Path to check as string + + Returns: + True if the path represents user code, False otherwise + """ + path_obj = Path(file_path) + return not (is_standard_library(path_obj) or is_third_party_package(path_obj)) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/utils.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/utils.py new file mode 100644 index 000000000..e6a88f8f4 --- /dev/null +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/utils.py @@ -0,0 +1,274 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Utility functions for code correlation in AWS OpenTelemetry Python Instrumentation. + +This module contains the core functionality for extracting and correlating +code metadata with telemetry data. +""" + +import functools +import inspect +from functools import wraps +from types import FrameType, FunctionType, MethodType +from typing import Any, Callable + +from opentelemetry import trace +from opentelemetry.semconv.attributes.code_attributes import CODE_FILE_PATH, CODE_FUNCTION_NAME, CODE_LINE_NUMBER + + +def get_callable_fullname(obj) -> str: # pylint: disable=too-many-return-statements + """ + Return the fully qualified name of any callable (module + qualname), + safely handling functions, methods, classes, partials, built-ins, etc. + + Examples: + >>> get_callable_fullname(len) + 'builtins.len' + >>> get_callable_fullname(math.sqrt) + 'math.sqrt' + >>> get_callable_fullname(MyClass.method) + '__main__.MyClass.method' + >>> get_callable_fullname(functools.partial(func)) + '__main__.func' + """ + try: + # functools.partial objects + if isinstance(obj, functools.partial): + target = get_callable_fullname(obj.func) + return target + + # Classes + if inspect.isclass(obj): + module = getattr(obj, "__module__", "") + name = getattr(obj, "__qualname__", getattr(obj, "__name__", "")) + return _construct_qualified_name(module, None, name) + + # Bound or unbound methods + if isinstance(obj, MethodType): + func = obj.__func__ + cls = getattr(obj, "__self__", None) + if cls: + cls_name = cls.__class__.__name__ if not inspect.isclass(cls) else cls.__name__ + module = getattr(func, "__module__", "") + name = getattr(func, "__name__", "") + return _construct_qualified_name(module, cls_name, name) + + # Regular Python functions, lambdas, static/class methods + if isinstance(obj, (FunctionType, staticmethod, classmethod)): + func = inspect.unwrap(obj) + module = getattr(func, "__module__", "") + qualname = getattr(func, "__qualname__", getattr(func, "__name__", repr(func))) + return _construct_qualified_name(module, None, qualname) + + # Built-in or C extension functions (e.g., len, numpy.add) + module = getattr(obj, "__module__", None) + name = getattr(obj, "__qualname__", None) or getattr(obj, "__name__", None) + if name: + return _construct_qualified_name(module or "", None, name) + + # Fallback for unknown callables + return repr(obj) + + except Exception: # pylint: disable=broad-exception-caught + return "" + + +def get_function_fullname_from_frame(frame: FrameType) -> str: + """ + Extract a fully qualified function name from a frame, similar to get_callable_fullname. + + This attempts to construct a full name including module and class information + when possible, falling back to just the function name if needed. + + Args: + frame: The Python frame object to extract name from + + Returns: + The fully qualified function name if possible, otherwise just the function name + """ + code = frame.f_code + func_name = code.co_name + + try: + # Try to get module name from frame globals + module_name = frame.f_globals.get("__name__", "") + + # Try to determine if this is a method by looking for 'self' or 'cls' in locals + locals_dict = frame.f_locals + + # Check for bound method (has 'self') + if "self" in locals_dict: + try: + cls_name = locals_dict["self"].__class__.__name__ + return _construct_qualified_name(module_name, cls_name, func_name) + except (AttributeError, KeyError): + pass + + # Check for class method (has 'cls') + elif "cls" in locals_dict: + try: + cls_name = locals_dict["cls"].__name__ + return _construct_qualified_name(module_name, cls_name, func_name) + except (AttributeError, KeyError): + pass + + # For regular functions or fallback + return _construct_qualified_name(module_name, None, func_name) + + except Exception: # pylint: disable=broad-exception-caught + # If anything goes wrong, fallback to simple function name + return func_name + + +def add_code_attributes_to_span_from_frame(frame: FrameType, span) -> None: + """ + Add code-related attributes to a span based on a Python frame object. + + This utility method extracts metadata from a frame and adds the following span attributes: + - CODE_FUNCTION_NAME: The fully qualified function name from the frame + - CODE_FILE_PATH: The file path where the code is defined + - CODE_LINE_NUMBER: The line number where the function is defined + + Args: + frame: The Python frame object to extract metadata from + span: The OpenTelemetry span to add attributes to + """ + if not span.is_recording(): + return + + try: + # Set function name using full qualified name (consistent with add_code_attributes_to_span) + span.set_attribute(CODE_FUNCTION_NAME, get_function_fullname_from_frame(frame)) + + # Set file path from code object + span.set_attribute(CODE_FILE_PATH, frame.f_code.co_filename) + + # Set line number from code object + span.set_attribute(CODE_LINE_NUMBER, frame.f_lineno) + + except Exception: # pylint: disable=broad-exception-caught + pass + + +def add_code_attributes_to_span(span, func_or_class: Callable[..., Any]) -> None: + """ + Add code-related attributes to a span based on a Python function or class. + + This utility method extracts metadata and adds the following span attributes: + - CODE_FUNCTION_NAME: The fully qualified name of the function/class + - CODE_FILE_PATH: The file path where the function/class is defined + - CODE_LINE_NUMBER: The line number where the function is defined (if available) + + Args: + span: The OpenTelemetry span to add attributes to + func_or_class: The Python function or class to extract metadata from + """ + if not span.is_recording(): + return + + try: + # Always set the function name using our robust helper + span.set_attribute(CODE_FUNCTION_NAME, get_callable_fullname(func_or_class)) + + # Try to get file path using inspect.getfile (works for both classes and functions) + try: + file_path = inspect.getfile(func_or_class) + span.set_attribute(CODE_FILE_PATH, file_path) + except (OSError, TypeError): + # Built-ins and some other callables don't have source files + pass + + # Try to get line number from __code__ attribute (only available for functions) + code = getattr(func_or_class, "__code__", None) + if code: + span.set_attribute(CODE_LINE_NUMBER, code.co_firstlineno) + + except Exception: # pylint: disable=broad-exception-caught + pass + + +def record_code_attributes(func: Callable[..., Any]) -> Callable[..., Any]: + """ + Decorator to automatically add code attributes to the current OpenTelemetry span. + + This decorator extracts metadata from the decorated function and adds it as + attributes to the current active span. The attributes added are: + - code.function.name: The name of the function + - code.file.path: The file path where the function is defined + - code.line.number: The line number where the function is defined + + This decorator supports both synchronous and asynchronous functions. + + Usage: + @record_code_attributes + def my_sync_function(): + # Sync function implementation + pass + + @record_code_attributes + async def my_async_function(): + # Async function implementation + pass + + Args: + func: The function to be decorated + + Returns: + The wrapped function with current span code attributes tracing + """ + # Detect async functions + is_async = inspect.iscoroutinefunction(func) + + if is_async: + # Async function wrapper + @wraps(func) + async def async_wrapper(*args, **kwargs): + # Add code attributes to current span + try: + current_span = trace.get_current_span() + if current_span: + add_code_attributes_to_span(current_span, func) + except Exception: # pylint: disable=broad-exception-caught + pass + + # Call and await the original async function + return await func(*args, **kwargs) + + return async_wrapper + + # Sync function wrapper + @wraps(func) + def sync_wrapper(*args, **kwargs): + # Add code attributes to current span + try: + current_span = trace.get_current_span() + if current_span: + add_code_attributes_to_span(current_span, func) + except Exception: # pylint: disable=broad-exception-caught + pass + + # Call the original sync function + return func(*args, **kwargs) + + return sync_wrapper + + +def _construct_qualified_name(module_name: str, class_name: str = None, func_name: str = "") -> str: + """ + Construct a fully qualified name from module, class, and function components. + + Args: + module_name: The module name + class_name: The class name (optional) + func_name: The function name + + Returns: + The fully qualified name in the format module.Class.function or module.function + """ + if class_name: + return f"{module_name}.{class_name}.{func_name}" + if module_name and module_name not in ("", None): + return f"{module_name}.{func_name}" + return func_name diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/internal/__init__.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/internal/__init__.py new file mode 100644 index 000000000..04f8b7b76 --- /dev/null +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/internal/__init__.py @@ -0,0 +1,2 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/internal/test_packages_resolver.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/internal/test_packages_resolver.py new file mode 100644 index 000000000..d39806a6f --- /dev/null +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/internal/test_packages_resolver.py @@ -0,0 +1,683 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import sysconfig +from pathlib import Path +from unittest import TestCase +from unittest.mock import Mock, patch + +from amazon.opentelemetry.distro.code_correlation.internal.packages_resolver import ( + Distribution, + _build_package_mapping, + _determine_effective_root, + _extract_root_module_name, + _load_third_party_packages, + _resolve_system_paths, + _validate_void_function, + execute_once, + is_standard_library, + is_third_party_package, + is_user_code, + resolve_package_from_filename, +) + + +class TestDistribution(TestCase): + """Test the Distribution NamedTuple.""" + + def test_distribution_initialization(self): + """Test Distribution initialization with name and version.""" + dist = Distribution(name="test-package", version="1.0.0") + + self.assertEqual(dist.name, "test-package") + self.assertEqual(dist.version, "1.0.0") + + def test_distribution_equality(self): + """Test Distribution equality comparison.""" + dist1 = Distribution(name="package", version="1.0") + dist2 = Distribution(name="package", version="1.0") + dist3 = Distribution(name="package", version="2.0") + + self.assertEqual(dist1, dist2) + self.assertNotEqual(dist1, dist3) + + def test_distribution_tuple_behavior(self): + """Test Distribution behaves as a tuple.""" + dist = Distribution(name="pkg", version="1.0") + + # Can be unpacked like a tuple + name, version = dist + self.assertEqual(name, "pkg") + self.assertEqual(version, "1.0") + + # Can be indexed like a tuple + self.assertEqual(dist[0], "pkg") + self.assertEqual(dist[1], "1.0") + + def test_distribution_repr(self): + """Test Distribution string representation.""" + dist = Distribution(name="test-pkg", version="2.5.1") + repr_str = repr(dist) + + self.assertIn("test-pkg", repr_str) + self.assertIn("2.5.1", repr_str) + + +class TestValidateVoidFunction(TestCase): + """Test the _validate_void_function helper.""" + + def test_validate_void_function_valid(self): + """Test validation of function with no arguments.""" + + def valid_func(): + return "test" + + from inspect import getfullargspec + + argspec = getfullargspec(valid_func) + result = _validate_void_function(valid_func, argspec) + + self.assertTrue(result) + + def test_validate_void_function_with_args(self): + """Test validation fails for function with positional arguments.""" + + def func_with_args(arg): + return arg + + from inspect import getfullargspec + + argspec = getfullargspec(func_with_args) + result = _validate_void_function(func_with_args, argspec) + + self.assertFalse(result) + + def test_validate_void_function_with_kwargs(self): + """Test validation fails for function with keyword arguments.""" + + def func_with_kwargs(**kwargs): + return kwargs + + from inspect import getfullargspec + + argspec = getfullargspec(func_with_kwargs) + result = _validate_void_function(func_with_kwargs, argspec) + + self.assertFalse(result) + + def test_validate_void_function_with_varargs(self): + """Test validation fails for function with varargs.""" + + def func_with_varargs(*args): + return args + + from inspect import getfullargspec + + argspec = getfullargspec(func_with_varargs) + result = _validate_void_function(func_with_varargs, argspec) + + self.assertFalse(result) + + def test_validate_void_function_with_defaults(self): + """Test validation fails for function with default arguments.""" + + def func_with_defaults(arg="default"): + return arg + + from inspect import getfullargspec + + argspec = getfullargspec(func_with_defaults) + result = _validate_void_function(func_with_defaults, argspec) + + self.assertFalse(result) + + def test_validate_void_function_generator(self): + """Test validation fails for generator function.""" + + def generator_func(): + yield 1 + + from inspect import getfullargspec + + argspec = getfullargspec(generator_func) + result = _validate_void_function(generator_func, argspec) + + self.assertFalse(result) + + +class TestExecuteOnce(TestCase): + """Test the execute_once decorator.""" + + def test_execute_once_valid_function(self): + """Test execute_once decorator on valid function.""" + call_count = 0 + + @execute_once + def test_func(): + nonlocal call_count + call_count += 1 + return "result" + + # First call + result1 = test_func() + self.assertEqual(result1, "result") + self.assertEqual(call_count, 1) + + # Second call should return cached result + result2 = test_func() + self.assertEqual(result2, "result") + self.assertEqual(call_count, 1) # Should not increment + + def test_execute_once_with_exception(self): + """Test execute_once decorator when function raises exception.""" + call_count = 0 + + @execute_once + def failing_func(): + nonlocal call_count + call_count += 1 + raise ValueError("test error") + + # First call should raise exception + with self.assertRaises(ValueError): + failing_func() + self.assertEqual(call_count, 1) + + # Second call should raise the same exception without re-executing + with self.assertRaises(ValueError): + failing_func() + self.assertEqual(call_count, 1) # Should not increment + + def test_execute_once_invalid_function(self): + """Test execute_once decorator rejects function with arguments.""" + with self.assertRaises(ValueError) as context: + + @execute_once + def invalid_func(arg): + return arg + + self.assertIn("no arguments", str(context.exception)) + + def test_execute_once_function_attributes(self): + """Test execute_once preserves function attributes.""" + + @execute_once + def test_func(): + """Test docstring.""" + return "result" + + self.assertEqual(test_func.__name__, "test_func") + self.assertEqual(test_func.__doc__, "Test docstring.") + + +class TestDetermineEffectiveRoot(TestCase): + """Test the _determine_effective_root function.""" + + def test_determine_effective_root_package_with_init(self): + """Test determining root for package with __init__.py.""" + with patch("pathlib.Path.is_dir", return_value=True), patch("pathlib.Path.exists", return_value=True): + + relative_path = Path("mypackage/submodule.py") + parent_path = Path("/usr/lib/python3.9/site-packages") + + result = _determine_effective_root(relative_path, parent_path) + self.assertEqual(result, "mypackage") + + def test_determine_effective_root_no_init(self): + """Test determining root for module without __init__.py.""" + with patch("pathlib.Path.is_dir", return_value=False): + relative_path = Path("module/file.py") + parent_path = Path("/usr/lib/python3.9/site-packages") + + result = _determine_effective_root(relative_path, parent_path) + self.assertEqual(result, "module/file.py") + + def test_determine_effective_root_directory_no_init(self): + """Test determining root for directory without __init__.py.""" + with patch("pathlib.Path.is_dir", return_value=True), patch("pathlib.Path.exists", return_value=False): + + relative_path = Path("namespace/subpackage.py") + parent_path = Path("/usr/lib/python3.9/site-packages") + + result = _determine_effective_root(relative_path, parent_path) + self.assertEqual(result, "namespace/subpackage.py") + + +class TestResolveSystemPaths(TestCase): + """Test the _resolve_system_paths function.""" + + def test_resolve_system_paths_caching(self): + """Test that system paths are cached properly.""" + with patch( + "amazon.opentelemetry.distro.code_correlation.internal.packages_resolver.sys.path", ["/path1", "/path2"] + ): + + # Clear cache by modifying global variables + import amazon.opentelemetry.distro.code_correlation.internal.packages_resolver as pkg_module + + pkg_module._sys_path_hash = None + pkg_module._resolved_sys_path = [] + + # First call + result1 = _resolve_system_paths() + + # Second call should return cached result + result2 = _resolve_system_paths() + + self.assertEqual(result1, result2) + self.assertEqual(len(result1), 2) + + def test_resolve_system_paths_cache_invalidation(self): + """Test that cache is invalidated when sys.path changes.""" + import amazon.opentelemetry.distro.code_correlation.internal.packages_resolver as pkg_module + + with patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver.sys.path", ["/path1"]): + pkg_module._sys_path_hash = None # Clear cache + result1 = _resolve_system_paths() + + with patch( + "amazon.opentelemetry.distro.code_correlation.internal.packages_resolver.sys.path", ["/path1", "/path2"] + ): + result2 = _resolve_system_paths() + + self.assertNotEqual(len(result1), len(result2)) + + +class TestExtractRootModuleName(TestCase): + """Test the _extract_root_module_name function.""" + + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._PURELIB_PATH") + def test_extract_root_module_name_purelib(self, mock_purelib): + """Test extracting root module name from purelib path.""" + mock_purelib.return_value = Path("/usr/lib/python3.9/site-packages") + + with patch.object(Path, "resolve") as mock_resolve, patch.object( + Path, "relative_to" + ) as mock_relative_to, patch( + "amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._determine_effective_root" + ) as mock_determine: + + mock_resolve.return_value = Path("/usr/lib/python3.9/site-packages/mypackage/module.py") + mock_relative_to.return_value = Path("mypackage/module.py") + mock_determine.return_value = "mypackage" + + file_path = Path("/usr/lib/python3.9/site-packages/mypackage/module.py") + result = _extract_root_module_name(file_path) + + self.assertEqual(result, "mypackage") + + def test_extract_root_module_name_value_error(self): + """Test _extract_root_module_name raises ValueError when module cannot be determined.""" + with patch.object(Path, "relative_to", side_effect=ValueError("not relative")), patch( + "amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._resolve_system_paths", + return_value=[], + ): + + file_path = Path("/unknown/path/module.py") + + with self.assertRaises(ValueError) as context: + _extract_root_module_name(file_path) + + self.assertIn("Could not determine root module", str(context.exception)) + + +class TestBuildPackageMapping(TestCase): + """Test the _build_package_mapping function.""" + + @patch("importlib.metadata.distributions") + def test_build_package_mapping_success(self, mock_distributions): + """Test successful package mapping build.""" + # Mock distribution + mock_dist = Mock() + mock_dist.metadata = {"name": "test-package", "version": "1.0.0"} + mock_dist.files = [ + Mock(parts=["testpkg", "module.py"], locate=lambda: Path("/site-packages/testpkg/module.py")) + ] + + mock_distributions.return_value = [mock_dist] + + # Clear the cache by calling the function directly + result = _build_package_mapping.__wrapped__() + + self.assertIsInstance(result, dict) + self.assertIn("testpkg", result) + self.assertEqual(result["testpkg"].name, "test-package") + self.assertEqual(result["testpkg"].version, "1.0.0") + + @patch("importlib.metadata.distributions") + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._logger") + def test_build_package_mapping_exception(self, mock_logger, mock_distributions): + """Test package mapping build handles exceptions.""" + mock_distributions.side_effect = Exception("Import error") + + result = _build_package_mapping.__wrapped__() + + self.assertIsNone(result) + mock_logger.warning.assert_called_once() + + @patch("importlib.metadata.distributions") + def test_build_package_mapping_namespace_packages(self, mock_distributions): + """Test package mapping handles namespace packages.""" + # Mock distribution with namespace package + mock_dist = Mock() + mock_dist.metadata = {"name": "namespace-pkg", "version": "1.0.0"} + + # Mock file path that represents a namespace package + mock_file = Mock() + mock_file.parts = ["namespace", "subpkg", "module.py"] + mock_file.locate.return_value = Path("/site-packages/namespace/subpkg/module.py") + + mock_dist.files = [mock_file] + mock_distributions.return_value = [mock_dist] + + # Mock Path methods to simulate namespace package (no __init__.py) + with patch.object(Path, "is_dir", return_value=True), patch.object( + Path, "exists", return_value=False + ): # No __init__.py + + result = _build_package_mapping.__wrapped__() + + # Should create mapping for namespace/subpkg + self.assertIn("namespace/subpkg", result) + + @patch("importlib.metadata.distributions") + def test_build_package_mapping_skip_metadata(self, mock_distributions): + """Test package mapping skips metadata directories.""" + mock_dist = Mock() + mock_dist.metadata = {"name": "test-pkg", "version": "1.0.0"} + mock_dist.files = [ + Mock(parts=["test_pkg-1.0.0.dist-info", "METADATA"]), + Mock(parts=["test_pkg-1.0.0.egg-info", "PKG-INFO"]), + Mock(parts=["..", "something"]), + Mock(parts=["testpkg", "module.py"], locate=lambda: Path("/site-packages/testpkg/module.py")), + ] + + mock_distributions.return_value = [mock_dist] + + result = _build_package_mapping.__wrapped__() + + # Should only have the actual package, not metadata directories + self.assertEqual(len(result), 1) + self.assertIn("testpkg", result) + + +class TestLoadThirdPartyPackages(TestCase): + """Test the _load_third_party_packages function.""" + + @patch("importlib.resources.read_text") + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._code_attributes_config") + def test_load_third_party_packages_success(self, mock_config, mock_read_text): + """Test successful loading of third-party packages.""" + # Mock text file content + mock_read_text.return_value = "package1\npackage2\npackage3" + + # Mock configuration + mock_config.include = ["extra_package"] + mock_config.exclude = ["package2"] + + result = _load_third_party_packages.__wrapped__() + + expected = {"package1", "package3", "extra_package"} # package2 excluded + self.assertEqual(result, expected) + + @patch("importlib.resources.read_text") + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._logger") + def test_load_third_party_packages_exception(self, mock_logger, mock_read_text): + """Test loading third-party packages handles exceptions.""" + mock_read_text.side_effect = Exception("Read error") + + result = _load_third_party_packages.__wrapped__() + + self.assertEqual(result, set()) + mock_logger.warning.assert_called_once() + + +class TestResolvePackageFromFilename(TestCase): + """Test the resolve_package_from_filename function.""" + + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._build_package_mapping") + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._extract_root_module_name") + def test_resolve_package_from_filename_exact_match(self, mock_extract, mock_build): + """Test resolving package with exact module name match.""" + # Mock package mapping + test_dist = Distribution(name="test-package", version="1.0.0") + mock_build.return_value = {"testpkg": test_dist} + mock_extract.return_value = "testpkg" + + result = resolve_package_from_filename("/path/to/testpkg/module.py") + + self.assertEqual(result, test_dist) + + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._build_package_mapping") + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._extract_root_module_name") + def test_resolve_package_from_filename_name_match(self, mock_extract, mock_build): + """Test resolving package with distribution name match.""" + test_dist = Distribution(name="test-package", version="1.0.0") + mock_build.return_value = {"otherpkg": test_dist} + mock_extract.return_value = "test-package" # Matches distribution name + + result = resolve_package_from_filename("/path/to/test-package/module.py") + + self.assertEqual(result, test_dist) + + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._build_package_mapping") + def test_resolve_package_from_filename_no_mapping(self, mock_build): + """Test resolving package when no mapping is available.""" + mock_build.return_value = None + + result = resolve_package_from_filename("/path/to/unknown/module.py") + + self.assertIsNone(result) + + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._build_package_mapping") + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._extract_root_module_name") + def test_resolve_package_from_filename_no_match(self, mock_extract, mock_build): + """Test resolving package when no match is found.""" + mock_build.return_value = {"otherpkg": Distribution("other", "1.0")} + mock_extract.return_value = "unknown" + + result = resolve_package_from_filename("/path/to/unknown/module.py") + + self.assertIsNone(result) + + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._build_package_mapping") + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._extract_root_module_name") + def test_resolve_package_from_filename_path_object(self, mock_extract, mock_build): + """Test resolving package with Path object input.""" + test_dist = Distribution(name="test-package", version="1.0.0") + mock_build.return_value = {"testpkg": test_dist} + mock_extract.return_value = "testpkg" + + path_obj = Path("/path/to/testpkg/module.py") + result = resolve_package_from_filename(path_obj) + + self.assertEqual(result, test_dist) + + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._build_package_mapping") + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._extract_root_module_name") + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._logger") + def test_resolve_package_from_filename_exception(self, mock_logger, mock_extract, mock_build): + """Test resolving package handles exceptions.""" + mock_build.return_value = {"testpkg": Distribution("test", "1.0")} + mock_extract.side_effect = ValueError("Extract error") + + # Clear the LRU cache for this test + resolve_package_from_filename.cache_clear() + + result = resolve_package_from_filename("/path/to/unknown/module.py") + + self.assertIsNone(result) + mock_logger.debug.assert_called() + + +class TestIsStandardLibrary(TestCase): + """Test the is_standard_library function.""" + + def test_is_standard_library_site_packages(self): + """Test detection of site-packages (not stdlib).""" + purelib_path = Path(sysconfig.get_path("purelib")) + test_path = purelib_path / "requests" / "__init__.py" + + with patch.object(Path, "is_relative_to") as mock_is_relative: + # Mock to return False for stdlib, True for site-packages + def side_effect(path): + return path == purelib_path + + mock_is_relative.side_effect = side_effect + + result = is_standard_library(test_path) + self.assertFalse(result) + + def test_is_standard_library_symlink_resolution(self): + """Test standard library detection resolves symlinks.""" + test_path = Path("/some/symlink/os.py") + + with patch.object(Path, "is_absolute", return_value=False), patch.object( + Path, "is_symlink", return_value=True + ), patch.object(Path, "resolve") as mock_resolve: + + mock_resolve.return_value = Path(sysconfig.get_path("stdlib")) / "os.py" + + # The function should call resolve() and then check the resolved path + is_standard_library(test_path) + mock_resolve.assert_called_once() + + +class TestIsThirdPartyPackage(TestCase): + """Test the is_third_party_package function.""" + + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver.resolve_package_from_filename") + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._load_third_party_packages") + def test_is_third_party_package_true(self, mock_load_packages, mock_resolve): + """Test detection of third-party package.""" + mock_resolve.return_value = Distribution(name="requests", version="2.25.1") + mock_load_packages.return_value = {"requests", "urllib3"} + + result = is_third_party_package(Path("/path/to/requests/__init__.py")) + + self.assertTrue(result) + + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver.resolve_package_from_filename") + def test_is_third_party_package_no_distribution(self, mock_resolve): + """Test detection when no distribution is found.""" + mock_resolve.return_value = None + + result = is_third_party_package(Path("/path/to/unknown/__init__.py")) + + self.assertFalse(result) + + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver.resolve_package_from_filename") + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._load_third_party_packages") + def test_is_third_party_package_not_in_list(self, mock_load_packages, mock_resolve): + """Test detection when package is not in third-party list.""" + mock_resolve.return_value = Distribution(name="myapp", version="1.0.0") + mock_load_packages.return_value = {"requests", "urllib3"} + + result = is_third_party_package(Path("/path/to/myapp/__init__.py")) + + self.assertFalse(result) + + +class TestIsUserCode(TestCase): + """Test the is_user_code function.""" + + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver.is_standard_library") + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver.is_third_party_package") + def test_is_user_code_true(self, mock_is_third_party, mock_is_stdlib): + """Test detection of user code.""" + mock_is_stdlib.return_value = False + mock_is_third_party.return_value = False + + result = is_user_code("/path/to/myapp/module.py") + + self.assertTrue(result) + + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver.is_standard_library") + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver.is_third_party_package") + def test_is_user_code_stdlib(self, mock_is_third_party, mock_is_stdlib): + """Test detection when file is standard library.""" + mock_is_stdlib.return_value = True + mock_is_third_party.return_value = False + + result = is_user_code("/usr/lib/python3.9/os.py") + + self.assertFalse(result) + + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver.is_standard_library") + @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver.is_third_party_package") + def test_is_user_code_third_party(self, mock_is_third_party, mock_is_stdlib): + """Test detection when file is third-party package.""" + mock_is_stdlib.return_value = False + mock_is_third_party.return_value = True + + result = is_user_code("/path/to/site-packages/requests/__init__.py") + + self.assertFalse(result) + + +class TestPackagesIntegration(TestCase): + """Integration tests for packages module.""" + + def test_distribution_in_resolve_package_workflow(self): + """Test Distribution is properly used in the resolution workflow.""" + with patch( + "amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._build_package_mapping" + ) as mock_build, patch( + "amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._extract_root_module_name" + ) as mock_extract: + + test_dist = Distribution(name="test-pkg", version="1.2.3") + mock_build.return_value = {"testpkg": test_dist} + mock_extract.return_value = "testpkg" + + result = resolve_package_from_filename("/path/to/testpkg/module.py") + + self.assertIsInstance(result, Distribution) + self.assertEqual(result.name, "test-pkg") + self.assertEqual(result.version, "1.2.3") + + def test_caching_behavior(self): + """Test that caching functions work correctly.""" + # Test that resolve_package_from_filename uses caching + with patch( + "amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._build_package_mapping" + ) as mock_build, patch( + "amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._extract_root_module_name" + ) as mock_extract: + + test_dist = Distribution(name="cached-pkg", version="1.0.0") + mock_build.return_value = {"cachedpkg": test_dist} + mock_extract.return_value = "cachedpkg" + + # First call + result1 = resolve_package_from_filename("/path/to/cachedpkg/module.py") + + # Second call with same path should use cache + result2 = resolve_package_from_filename("/path/to/cachedpkg/module.py") + + self.assertEqual(result1, result2) + self.assertEqual(result1.name, "cached-pkg") + + # Should have been called only once due to LRU cache + self.assertEqual(mock_extract.call_count, 1) + + def test_module_constants(self): + """Test that module constants are properly initialized.""" + from amazon.opentelemetry.distro.code_correlation.internal.packages_resolver import ( + _PLATLIB_PATH, + _PLATSTDLIB_PATH, + _PURELIB_PATH, + _STDLIB_PATH, + ) + + # All paths should be Path objects + self.assertIsInstance(_STDLIB_PATH, Path) + self.assertIsInstance(_PLATSTDLIB_PATH, Path) + self.assertIsInstance(_PURELIB_PATH, Path) + self.assertIsInstance(_PLATLIB_PATH, Path) + + # All paths should be absolute and resolved + self.assertTrue(_STDLIB_PATH.is_absolute()) + self.assertTrue(_PLATSTDLIB_PATH.is_absolute()) + self.assertTrue(_PURELIB_PATH.is_absolute()) + self.assertTrue(_PLATLIB_PATH.is_absolute()) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/test_code_attributes_span_processor.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/test_code_attributes_span_processor.py new file mode 100644 index 000000000..cbaf71620 --- /dev/null +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/test_code_attributes_span_processor.py @@ -0,0 +1,463 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from types import FrameType +from unittest import TestCase +from unittest.mock import MagicMock, patch + +from amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor import CodeAttributesSpanProcessor +from opentelemetry.context import Context +from opentelemetry.sdk.trace import ReadableSpan, Span +from opentelemetry.semconv.attributes.code_attributes import CODE_FUNCTION_NAME +from opentelemetry.trace import SpanKind + + +class TestIterStackFrames(TestCase): + """Test the _iter_stack_frames private class method.""" + + def test_iter_stack_frames_single_frame(self): + """Test iterating over a single frame.""" + mock_frame = MagicMock(spec=FrameType) + mock_frame.f_back = None + + frames = list(CodeAttributesSpanProcessor._iter_stack_frames(mock_frame)) + + self.assertEqual(len(frames), 1) + self.assertEqual(frames[0], mock_frame) + + def test_iter_stack_frames_multiple_frames(self): + """Test iterating over multiple frames.""" + # Create a chain of frames + frame3 = MagicMock(spec=FrameType) + frame3.f_back = None + + frame2 = MagicMock(spec=FrameType) + frame2.f_back = frame3 + + frame1 = MagicMock(spec=FrameType) + frame1.f_back = frame2 + + frames = list(CodeAttributesSpanProcessor._iter_stack_frames(frame1)) + + self.assertEqual(len(frames), 3) + self.assertEqual(frames[0], frame1) + self.assertEqual(frames[1], frame2) + self.assertEqual(frames[2], frame3) + + def test_iter_stack_frames_empty_when_none(self): + """Test that no frames are yielded when starting with None.""" + frames = list(CodeAttributesSpanProcessor._iter_stack_frames(None)) + self.assertEqual(len(frames), 0) + + +class TestCodeAttributesSpanProcessor(TestCase): + """Test the CodeAttributesSpanProcessor class.""" + + def setUp(self): + """Set up test fixtures.""" + # Patch the initialization calls to avoid side effects + self.build_package_mapping_patcher = patch( + "amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor._build_package_mapping" + ) + self.load_third_party_packages_patcher = patch( + "amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor._load_third_party_packages" + ) + + self.mock_build_package_mapping = self.build_package_mapping_patcher.start() + self.mock_load_third_party_packages = self.load_third_party_packages_patcher.start() + + self.processor = CodeAttributesSpanProcessor() + + def tearDown(self): + """Clean up test fixtures.""" + self.build_package_mapping_patcher.stop() + self.load_third_party_packages_patcher.stop() + + def test_initialization_calls_setup_functions(self): + """Test that initialization calls the package mapping functions.""" + self.mock_build_package_mapping.assert_called_once() + self.mock_load_third_party_packages.assert_called_once() + + def test_max_stack_frames_constant(self): + """Test that MAX_STACK_FRAMES is set to expected value.""" + self.assertEqual(CodeAttributesSpanProcessor.MAX_STACK_FRAMES, 50) + + def create_mock_span(self, span_kind=SpanKind.CLIENT, attributes=None): + """Helper to create a mock span with specified properties.""" + mock_span = MagicMock(spec=Span) + mock_span.kind = span_kind + mock_span.attributes = attributes + return mock_span + + def test_should_process_span_client_span_without_attributes(self): + """Test that CLIENT spans without code attributes should be processed.""" + span = self.create_mock_span(SpanKind.CLIENT, attributes=None) + result = CodeAttributesSpanProcessor._should_process_span(span) + self.assertTrue(result) + + def test_should_process_span_client_span_with_empty_attributes(self): + """Test that CLIENT spans with empty attributes should be processed.""" + span = self.create_mock_span(SpanKind.CLIENT, attributes={}) + result = CodeAttributesSpanProcessor._should_process_span(span) + self.assertTrue(result) + + def test_should_process_span_client_span_with_existing_code_attributes(self): + """Test that CLIENT spans with existing code attributes should not be processed.""" + attributes = {CODE_FUNCTION_NAME: "existing.function"} + span = self.create_mock_span(SpanKind.CLIENT, attributes=attributes) + result = CodeAttributesSpanProcessor._should_process_span(span) + self.assertFalse(result) + + def test_should_process_span_server_and_internal_spans(self): + """Test that SERVER and INTERNAL spans should not be processed.""" + test_cases = [ + SpanKind.SERVER, + SpanKind.INTERNAL, + ] + + for span_kind in test_cases: + with self.subTest(span_kind=span_kind): + span = self.create_mock_span(span_kind, attributes=None) + result = CodeAttributesSpanProcessor._should_process_span(span) + self.assertFalse(result) + + def test_should_process_span_client_producer_consumer_spans(self): + """Test that CLIENT, PRODUCER, and CONSUMER spans should be processed.""" + test_cases = [ + SpanKind.CLIENT, + SpanKind.PRODUCER, + SpanKind.CONSUMER, + ] + + for span_kind in test_cases: + with self.subTest(span_kind=span_kind): + span = self.create_mock_span(span_kind, attributes=None) + result = CodeAttributesSpanProcessor._should_process_span(span) + self.assertTrue(result) + + @patch("amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor.sys._getframe") + @patch("amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor.is_user_code") + @patch( + "amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor." + "add_code_attributes_to_span_from_frame" + ) + def test_capture_code_attributes_with_user_code(self, mock_add_attributes, mock_is_user_code, mock_getframe): + """Test capturing code attributes when user code is found.""" + # Create mock frames + mock_code = MagicMock() + mock_code.co_filename = "/user/code.py" + + mock_frame = MagicMock(spec=FrameType) + mock_frame.f_code = mock_code + mock_frame.f_back = None + + mock_getframe.return_value = mock_frame + mock_is_user_code.return_value = True + + span = self.create_mock_span() + + self.processor._capture_code_attributes(span) + + mock_getframe.assert_called_once_with(1) + mock_is_user_code.assert_called_once_with("/user/code.py") + mock_add_attributes.assert_called_once_with(mock_frame, span) + + @patch("amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor.sys._getframe") + @patch("amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor.is_user_code") + @patch( + "amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor." + "add_code_attributes_to_span_from_frame" + ) + def test_capture_code_attributes_no_user_code(self, mock_add_attributes, mock_is_user_code, mock_getframe): + """Test capturing code attributes when no user code is found.""" + # Create mock frames - all library code + mock_code1 = MagicMock() + mock_code1.co_filename = "/lib/code1.py" + + mock_code2 = MagicMock() + mock_code2.co_filename = "/lib/code2.py" + + mock_frame2 = MagicMock(spec=FrameType) + mock_frame2.f_code = mock_code2 + mock_frame2.f_back = None + + mock_frame1 = MagicMock(spec=FrameType) + mock_frame1.f_code = mock_code1 + mock_frame1.f_back = mock_frame2 + + mock_getframe.return_value = mock_frame1 + mock_is_user_code.return_value = False + + span = self.create_mock_span() + + self.processor._capture_code_attributes(span) + + mock_getframe.assert_called_once_with(1) + # is_user_code should be called for both frames + self.assertEqual(mock_is_user_code.call_count, 2) + mock_add_attributes.assert_not_called() + + @patch("amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor.sys._getframe") + @patch("amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor.is_user_code") + @patch( + "amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor." + "add_code_attributes_to_span_from_frame" + ) + def test_capture_code_attributes_max_frames_limit(self, mock_add_attributes, mock_is_user_code, mock_getframe): + """Test that frame iteration respects MAX_STACK_FRAMES limit.""" + # Create a deep stack that exceeds MAX_STACK_FRAMES + frames = [] + for i in range(CodeAttributesSpanProcessor.MAX_STACK_FRAMES + 1): # More than MAX_STACK_FRAMES + mock_code = MagicMock() + mock_code.co_filename = f"/frame{i}.py" + + mock_frame = MagicMock(spec=FrameType) + mock_frame.f_code = mock_code + frames.append(mock_frame) + + # Link frames together + for i in range(len(frames) - 1): + frames[i].f_back = frames[i + 1] + frames[-1].f_back = None + + mock_getframe.return_value = frames[0] + mock_is_user_code.return_value = False # No user code found + + span = self.create_mock_span() + + self.processor._capture_code_attributes(span) + + # Should only check up to MAX_STACK_FRAMES + self.assertEqual(mock_is_user_code.call_count, CodeAttributesSpanProcessor.MAX_STACK_FRAMES) + mock_add_attributes.assert_not_called() + + @patch("amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor.sys._getframe") + def test_capture_code_attributes_getframe_oserror(self, mock_getframe): + """Test handling of OSError when sys._getframe is not available.""" + mock_getframe.side_effect = OSError("sys._getframe not available") + + span = self.create_mock_span() + + # Should not raise exception + self.processor._capture_code_attributes(span) + + @patch("amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor.sys._getframe") + def test_capture_code_attributes_getframe_valueerror(self, mock_getframe): + """Test handling of ValueError when sys._getframe is called with invalid argument.""" + mock_getframe.side_effect = ValueError("invalid frame") + + span = self.create_mock_span() + + # Should not raise exception + self.processor._capture_code_attributes(span) + + @patch("amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor.sys._getframe") + @patch("amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor.is_user_code") + @patch( + "amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor." + "add_code_attributes_to_span_from_frame" + ) + def test_capture_code_attributes_stops_at_first_user_code( + self, mock_add_attributes, mock_is_user_code, mock_getframe + ): + """Test that processing stops at the first user code frame.""" + # Create mock frames where second frame is user code + mock_code1 = MagicMock() + mock_code1.co_filename = "/lib/code1.py" + + mock_code2 = MagicMock() + mock_code2.co_filename = "/user/code2.py" + + mock_code3 = MagicMock() + mock_code3.co_filename = "/user/code3.py" + + mock_frame3 = MagicMock(spec=FrameType) + mock_frame3.f_code = mock_code3 + mock_frame3.f_back = None + + mock_frame2 = MagicMock(spec=FrameType) + mock_frame2.f_code = mock_code2 + mock_frame2.f_back = mock_frame3 + + mock_frame1 = MagicMock(spec=FrameType) + mock_frame1.f_code = mock_code1 + mock_frame1.f_back = mock_frame2 + + mock_getframe.return_value = mock_frame1 + + def is_user_code_side_effect(filename): + return filename in ["/user/code2.py", "/user/code3.py"] + + mock_is_user_code.side_effect = is_user_code_side_effect + + span = self.create_mock_span() + + self.processor._capture_code_attributes(span) + + # Should check first two frames, then stop at first user code + self.assertEqual(mock_is_user_code.call_count, 2) + mock_add_attributes.assert_called_once_with(mock_frame2, span) + + def test_on_start_should_not_process_span(self): + """Test on_start when span should not be processed.""" + span = self.create_mock_span(SpanKind.SERVER) # Non-client span + + with patch.object(self.processor, "_capture_code_attributes") as mock_capture: + self.processor.on_start(span) + mock_capture.assert_not_called() + + def test_on_start_should_process_span(self): + """Test on_start when span should be processed.""" + span = self.create_mock_span(SpanKind.CLIENT) # Client span without code attributes + + with patch.object(self.processor, "_capture_code_attributes") as mock_capture: + self.processor.on_start(span) + mock_capture.assert_called_once_with(span) + + def test_on_start_with_parent_context(self): + """Test on_start with parent context parameter.""" + span = self.create_mock_span(SpanKind.CLIENT) + parent_context = MagicMock(spec=Context) + + with patch.object(self.processor, "_capture_code_attributes") as mock_capture: + self.processor.on_start(span, parent_context) + mock_capture.assert_called_once_with(span) + + def test_on_end(self): + """Test on_end method (empty implementation).""" + mock_span = MagicMock(spec=ReadableSpan) + + # Should not raise exception + self.processor.on_end(mock_span) + + def test_shutdown(self): + """Test shutdown method (empty implementation).""" + # Should not raise exception + self.processor.shutdown() + + def test_force_flush_returns_true(self): + """Test that force_flush always returns True.""" + result = self.processor.force_flush() + self.assertTrue(result) + + def test_force_flush_with_timeout(self): + """Test that force_flush accepts timeout parameter and returns True.""" + result = self.processor.force_flush(timeout_millis=5000) + self.assertTrue(result) + + def test_force_flush_with_default_timeout(self): + """Test that force_flush uses default timeout and returns True.""" + result = self.processor.force_flush() + self.assertTrue(result) + + +class TestCodeAttributesSpanProcessorIntegration(TestCase): + """Integration tests for CodeAttributesSpanProcessor.""" + + def setUp(self): + """Set up test fixtures.""" + # Patch the initialization calls + with patch( + "amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor._build_package_mapping" + ), patch( + "amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor._load_third_party_packages" + ): + self.processor = CodeAttributesSpanProcessor() + + def create_real_span_mock(self, span_kind=SpanKind.CLIENT, attributes=None): + """Create a more realistic span mock.""" + span = MagicMock(spec=Span) + span.kind = span_kind + span.attributes = attributes + span.is_recording.return_value = True + return span + + @patch("amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor.sys._getframe") + @patch("amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor.is_user_code") + @patch( + "amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor." + "add_code_attributes_to_span_from_frame" + ) + def test_full_workflow_with_user_code(self, mock_add_attributes, mock_is_user_code, mock_getframe): + """Test the complete workflow when user code is found.""" + # Setup mock frame + mock_code = MagicMock() + mock_code.co_filename = "/app/user_code.py" + + mock_frame = MagicMock(spec=FrameType) + mock_frame.f_code = mock_code + mock_frame.f_back = None + + mock_getframe.return_value = mock_frame + mock_is_user_code.return_value = True + + span = self.create_real_span_mock(SpanKind.CLIENT) + + # Execute the full workflow + self.processor.on_start(span) + + # Verify all components were called correctly + mock_getframe.assert_called_once_with(1) + mock_is_user_code.assert_called_once_with("/app/user_code.py") + mock_add_attributes.assert_called_once_with(mock_frame, span) + + @patch("amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor.sys._getframe") + @patch("amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor.is_user_code") + @patch( + "amazon.opentelemetry.distro.code_correlation.code_attributes_span_processor." + "add_code_attributes_to_span_from_frame" + ) + def test_full_workflow_no_user_code_found(self, mock_add_attributes, mock_is_user_code, mock_getframe): + """Test the complete workflow when no user code is found.""" + # Setup mock frames - all library code + frames_data = [ + ("/lib/instrumentation.py", False), + ("/lib/framework.py", False), + ("/lib/stdlib.py", False), + ] + + frames = [] + for i, (filename, _) in enumerate(frames_data): + mock_code = MagicMock() + mock_code.co_filename = filename + + mock_frame = MagicMock(spec=FrameType) + mock_frame.f_code = mock_code + frames.append(mock_frame) + + # Link frames + for i in range(len(frames) - 1): + frames[i].f_back = frames[i + 1] + frames[-1].f_back = None + + mock_getframe.return_value = frames[0] + mock_is_user_code.return_value = False + + span = self.create_real_span_mock(SpanKind.CLIENT) + + # Execute the full workflow + self.processor.on_start(span) + + # Verify components were called + mock_getframe.assert_called_once_with(1) + self.assertEqual(mock_is_user_code.call_count, len(frames_data)) + mock_add_attributes.assert_not_called() + + def test_processor_lifecycle(self): + """Test the complete processor lifecycle.""" + span = self.create_real_span_mock(SpanKind.CLIENT) + + # Start processing + with patch.object(self.processor, "_capture_code_attributes") as mock_capture: + self.processor.on_start(span) + mock_capture.assert_called_once() + + # End processing + self.processor.on_end(span) # Should not raise + + # Force flush + result = self.processor.force_flush() + self.assertTrue(result) + + # Shutdown + self.processor.shutdown() # Should not raise diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/test_code_correlation.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/test_code_correlation.py deleted file mode 100644 index a09964375..000000000 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/test_code_correlation.py +++ /dev/null @@ -1,326 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 - -import asyncio -from unittest import TestCase -from unittest.mock import MagicMock, patch - -from amazon.opentelemetry.distro.code_correlation import ( - CODE_FILE_PATH, - CODE_FUNCTION_NAME, - CODE_LINE_NUMBER, - add_code_attributes_to_span, - record_code_attributes, -) -from opentelemetry.trace import Span - - -class TestCodeCorrelationConstants(TestCase): - """Test code correlation attribute constants.""" - - def test_constants_values(self): - """Test that constants have the expected values.""" - self.assertEqual(CODE_FUNCTION_NAME, "code.function.name") - self.assertEqual(CODE_FILE_PATH, "code.file.path") - self.assertEqual(CODE_LINE_NUMBER, "code.line.number") - - -class TestAddCodeAttributesToSpan(TestCase): - """Test the add_code_attributes_to_span function.""" - - def test_add_code_attributes_to_recording_span_with_function(self): - """Test adding code attributes to a recording span with a regular function.""" - # Create independent mock_span for this test - mock_span = MagicMock(spec=Span) - mock_span.is_recording.return_value = True - - def test_function(): - pass - - add_code_attributes_to_span(mock_span, test_function) - - # Verify function name attribute is set - expected_function_name = f"{test_function.__module__}.{test_function.__qualname__}" - mock_span.set_attribute.assert_any_call(CODE_FUNCTION_NAME, expected_function_name) - - # Verify file path attribute is set - expected_file_path = test_function.__code__.co_filename - mock_span.set_attribute.assert_any_call(CODE_FILE_PATH, expected_file_path) - - # Verify line number attribute is set - expected_line_number = test_function.__code__.co_firstlineno - mock_span.set_attribute.assert_any_call(CODE_LINE_NUMBER, expected_line_number) - - def test_add_code_attributes_to_recording_span_with_class(self): - """Test adding code attributes to a recording span with a class.""" - # Create independent mock_span for this test - mock_span = MagicMock(spec=Span) - mock_span.is_recording.return_value = True - - class TestClass: - pass - - add_code_attributes_to_span(mock_span, TestClass) - - # Verify class name attribute is set - expected_class_name = f"{TestClass.__module__}.{TestClass.__qualname__}" - mock_span.set_attribute.assert_any_call(CODE_FUNCTION_NAME, expected_class_name) - - # Verify file path attribute is set (classes have file paths too) - mock_span.set_attribute.assert_any_call(CODE_FILE_PATH, __file__) - - def test_add_code_attributes_to_non_recording_span(self): - """Test that no attributes are added to a non-recording span.""" - # Create independent mock_span for this test - mock_span = MagicMock(spec=Span) - mock_span.is_recording.return_value = False - - def test_function(): - pass - - add_code_attributes_to_span(mock_span, test_function) - - # Verify no attributes are set - mock_span.set_attribute.assert_not_called() - - def test_add_code_attributes_function_without_code(self): - """Test handling of functions without __code__ attribute.""" - # Create independent mock_span for this test - mock_span = MagicMock(spec=Span) - mock_span.is_recording.return_value = True - - # Create a mock function without __code__ attribute - mock_func = MagicMock() - mock_func.__name__ = "mock_function" - delattr(mock_func, "__code__") - - add_code_attributes_to_span(mock_span, mock_func) - - # Functions without __code__ attribute don't get any attributes set - mock_span.set_attribute.assert_not_called() - - def test_add_code_attributes_builtin_function(self): - """Test handling of built-in functions.""" - # Create independent mock_span for this test - mock_span = MagicMock(spec=Span) - mock_span.is_recording.return_value = True - - # Use a built-in function like len - add_code_attributes_to_span(mock_span, len) - - # Built-in functions don't have __code__ attribute, so no attributes are set - mock_span.set_attribute.assert_not_called() - - def test_add_code_attributes_exception_handling(self): - """Test that exceptions are handled gracefully.""" - # Create independent mock_span for this test - mock_span = MagicMock(spec=Span) - mock_span.is_recording.return_value = True - - # Create a function that will cause an exception when accessing __name__ - mock_func = MagicMock() - mock_func.__name__ = MagicMock(side_effect=Exception("Test exception")) - - # This should not raise an exception - add_code_attributes_to_span(mock_span, mock_func) - - # No attributes should be set due to exception - mock_span.set_attribute.assert_not_called() - - -class TestRecordCodeAttributesDecorator(TestCase): - """Test the record_code_attributes decorator.""" - - def setUp(self): - """Set up test fixtures.""" - self.mock_span = MagicMock(spec=Span) - self.mock_span.is_recording.return_value = True - - @patch("amazon.opentelemetry.distro.code_correlation.trace.get_current_span") - def test_decorator_sync_function(self, mock_get_current_span): - """Test decorator with synchronous function.""" - mock_get_current_span.return_value = self.mock_span - - @record_code_attributes - def test_sync_function(arg1, arg2=None): - return f"sync result: {arg1}, {arg2}" - - # Call the decorated function - result = test_sync_function("test_arg", arg2="test_kwarg") - - # Verify the function still works correctly - self.assertEqual(result, "sync result: test_arg, test_kwarg") - - # Verify span attributes were set - expected_function_name = f"{test_sync_function.__module__}.{test_sync_function.__qualname__}" - self.mock_span.set_attribute.assert_any_call(CODE_FUNCTION_NAME, expected_function_name) - - @patch("amazon.opentelemetry.distro.code_correlation.trace.get_current_span") - def test_decorator_async_function(self, mock_get_current_span): - """Test decorator with asynchronous function.""" - mock_get_current_span.return_value = self.mock_span - - @record_code_attributes - async def test_async_function(arg1, arg2=None): - return f"async result: {arg1}, {arg2}" - - # Call the decorated async function - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - result = loop.run_until_complete(test_async_function("test_arg", arg2="test_kwarg")) - finally: - loop.close() - - # Verify the function still works correctly - self.assertEqual(result, "async result: test_arg, test_kwarg") - - # Verify span attributes were set - expected_function_name = f"{test_async_function.__module__}.{test_async_function.__qualname__}" - self.mock_span.set_attribute.assert_any_call(CODE_FUNCTION_NAME, expected_function_name) - - @patch("amazon.opentelemetry.distro.code_correlation.trace.get_current_span") - def test_decorator_no_current_span(self, mock_get_current_span): - """Test decorator when there's no current span.""" - mock_get_current_span.return_value = None - - @record_code_attributes - def test_function(): - return "test result" - - # Call the decorated function - result = test_function() - - # Verify the function still works correctly - self.assertEqual(result, "test result") - - # Verify no span attributes were set since there's no span - self.mock_span.set_attribute.assert_not_called() - - @patch("amazon.opentelemetry.distro.code_correlation.trace.get_current_span") - def test_decorator_exception_handling(self, mock_get_current_span): - """Test decorator handles exceptions gracefully.""" - mock_get_current_span.side_effect = Exception("Test exception") - - @record_code_attributes - def test_function(): - return "test result" - - # Call the decorated function - should not raise exception - result = test_function() - - # Verify the function still works correctly - self.assertEqual(result, "test result") - - def test_decorator_preserves_function_metadata(self): - """Test that decorator preserves original function metadata.""" - - @record_code_attributes - def test_function(): - """Test function docstring.""" - return "test result" - - # Verify function metadata is preserved - self.assertEqual(test_function.__name__, "test_function") - self.assertEqual(test_function.__doc__, "Test function docstring.") - - def test_async_function_detection(self): - """Test that async functions are properly detected.""" - - # Create a regular function - def sync_func(): - pass - - # Create an async function - async def async_func(): - pass - - # Apply decorator to both - decorated_sync = record_code_attributes(sync_func) - decorated_async = record_code_attributes(async_func) - - # Check that sync function returns a regular function - self.assertFalse(asyncio.iscoroutinefunction(decorated_sync)) - - # Check that async function returns a coroutine function - self.assertTrue(asyncio.iscoroutinefunction(decorated_async)) - - @patch("amazon.opentelemetry.distro.code_correlation.trace.get_current_span") - def test_decorator_with_function_that_raises_exception(self, mock_get_current_span): - """Test decorator with function that raises exception.""" - mock_get_current_span.return_value = self.mock_span - - @record_code_attributes - def test_function(): - raise ValueError("Test function exception") - - # Verify exception is still raised - with self.assertRaises(ValueError): - test_function() - - # Verify span attributes were still set before exception - expected_function_name = f"{test_function.__module__}.{test_function.__qualname__}" - self.mock_span.set_attribute.assert_any_call(CODE_FUNCTION_NAME, expected_function_name) - - @patch("amazon.opentelemetry.distro.code_correlation.trace.get_current_span") - def test_decorator_with_async_function_that_raises_exception(self, mock_get_current_span): - """Test decorator with async function that raises exception.""" - mock_get_current_span.return_value = self.mock_span - - @record_code_attributes - async def test_async_function(): - raise ValueError("Test async function exception") - - # Verify exception is still raised - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - with self.assertRaises(ValueError): - loop.run_until_complete(test_async_function()) - finally: - loop.close() - - # Verify span attributes were still set before exception - expected_function_name = f"{test_async_function.__module__}.{test_async_function.__qualname__}" - self.mock_span.set_attribute.assert_any_call(CODE_FUNCTION_NAME, expected_function_name) - - @patch("amazon.opentelemetry.distro.code_correlation.add_code_attributes_to_span") - @patch("amazon.opentelemetry.distro.code_correlation.trace.get_current_span") - def test_decorator_internal_exception_handling_sync(self, mock_get_current_span, mock_add_attributes): - """Test that decorator handles internal exceptions gracefully in sync function.""" - mock_get_current_span.return_value = self.mock_span - # Make add_code_attributes_to_span raise an exception - mock_add_attributes.side_effect = Exception("Internal exception") - - @record_code_attributes - def test_function(): - return "test result" - - # Call the decorated function - should not raise exception - result = test_function() - - # Verify the function still works correctly despite internal exception - self.assertEqual(result, "test result") - - @patch("amazon.opentelemetry.distro.code_correlation.add_code_attributes_to_span") - @patch("amazon.opentelemetry.distro.code_correlation.trace.get_current_span") - def test_decorator_internal_exception_handling_async(self, mock_get_current_span, mock_add_attributes): - """Test that decorator handles internal exceptions gracefully in async function.""" - mock_get_current_span.return_value = self.mock_span - # Make add_code_attributes_to_span raise an exception - mock_add_attributes.side_effect = Exception("Internal exception") - - @record_code_attributes - async def test_async_function(): - return "async test result" - - # Call the decorated async function - should not raise exception - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - result = loop.run_until_complete(test_async_function()) - finally: - loop.close() - - # Verify the function still works correctly despite internal exception - self.assertEqual(result, "async test result") diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/test_code_correlation_utils.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/test_code_correlation_utils.py new file mode 100644 index 000000000..3122f2e49 --- /dev/null +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/test_code_correlation_utils.py @@ -0,0 +1,736 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import asyncio +import functools +import inspect +import types +from types import FrameType +from unittest import TestCase +from unittest.mock import Mock, patch + +from amazon.opentelemetry.distro.code_correlation.utils import ( + _construct_qualified_name, + add_code_attributes_to_span, + add_code_attributes_to_span_from_frame, + get_callable_fullname, + get_function_fullname_from_frame, + record_code_attributes, +) +from opentelemetry.semconv.attributes.code_attributes import ( # noqa: F401 + CODE_FILE_PATH, + CODE_FUNCTION_NAME, + CODE_LINE_NUMBER, +) + + +# Test classes and functions for testing +class TestClass: + """Test class for testing callable name extraction.""" + + def instance_method(self): + """Instance method for testing.""" + pass + + @classmethod + def class_method(cls): + """Class method for testing.""" + pass + + @staticmethod + def static_method(): + """Static method for testing.""" + pass + + +def test_function(): + """Test function for testing.""" + pass + + +def test_function_with_locals(): + """Test function that creates a frame with locals.""" + local_var = "test" # noqa: F841 + return inspect.currentframe() + + +class TestGetCallableFullname(TestCase): + """Test the get_callable_fullname function.""" + + def test_regular_function(self): + """Test with regular Python function.""" + result = get_callable_fullname(test_function) + expected = f"{__name__}.test_function" + self.assertEqual(result, expected) + + def test_builtin_function(self): + """Test with built-in function.""" + result = get_callable_fullname(len) + self.assertEqual(result, "builtins.len") + + def test_lambda_function(self): + """Test with lambda function.""" + + def lambda_func(x): + return x + 1 + + # Properly simulate a lambda function by setting both __name__ and __qualname__ + lambda_func.__name__ = "" + lambda_func.__qualname__ = "" + result = get_callable_fullname(lambda_func) + # Lambda functions include the full qualname which includes the test method + self.assertTrue(result.endswith("")) + self.assertTrue(result.startswith(__name__)) + + def test_class(self): + """Test with class.""" + result = get_callable_fullname(TestClass) + expected = f"{__name__}.TestClass" + self.assertEqual(result, expected) + + def test_instance_method_unbound(self): + """Test with unbound instance method.""" + result = get_callable_fullname(TestClass.instance_method) + expected = f"{__name__}.TestClass.instance_method" + self.assertEqual(result, expected) + + def test_instance_method_bound(self): + """Test with bound instance method.""" + instance = TestClass() + result = get_callable_fullname(instance.instance_method) + expected = f"{__name__}.TestClass.instance_method" + self.assertEqual(result, expected) + + def test_class_method(self): + """Test with class method.""" + result = get_callable_fullname(TestClass.class_method) + expected = f"{__name__}.TestClass.class_method" + self.assertEqual(result, expected) + + def test_static_method(self): + """Test with static method.""" + result = get_callable_fullname(TestClass.static_method) + expected = f"{__name__}.TestClass.static_method" + self.assertEqual(result, expected) + + def test_functools_partial(self): + """Test with functools.partial object.""" + partial_func = functools.partial(test_function) + result = get_callable_fullname(partial_func) + expected = f"{__name__}.test_function" + self.assertEqual(result, expected) + + def test_nested_partial(self): + """Test with nested functools.partial objects.""" + partial_func = functools.partial(functools.partial(test_function)) + result = get_callable_fullname(partial_func) + expected = f"{__name__}.test_function" + self.assertEqual(result, expected) + + def test_callable_object_with_module_and_name(self): + """Test with callable object that has __module__ and __name__.""" + mock_callable = Mock() + mock_callable.__module__ = "test.module" + mock_callable.__name__ = "test_callable" + mock_callable.__qualname__ = "test_callable" + + result = get_callable_fullname(mock_callable) + self.assertEqual(result, "test.module.test_callable") + + def test_callable_object_with_qualname(self): + """Test with callable object that has __qualname__ but no __name__.""" + mock_callable = Mock() + mock_callable.__module__ = "test.module" + mock_callable.__qualname__ = "TestClass.method" + # Remove __name__ attribute + if hasattr(mock_callable, "__name__"): + del mock_callable.__name__ + + result = get_callable_fullname(mock_callable) + self.assertEqual(result, "test.module.TestClass.method") + + def test_callable_without_module(self): + """Test with callable that has no __module__ attribute.""" + mock_callable = Mock() + mock_callable.__name__ = "test_callable" + # Mock objects have persistent __module__ attribute, so we need to set it to None + mock_callable.__module__ = None + + result = get_callable_fullname(mock_callable) + # When module is None, _construct_qualified_name just returns the function name + self.assertEqual(result, "test_callable") + + def test_callable_without_name_or_qualname(self): + """Test with callable that has no __name__ or __qualname__.""" + mock_callable = Mock() + mock_callable.__module__ = "test.module" + # Remove __name__ and __qualname__ attributes + if hasattr(mock_callable, "__name__"): + del mock_callable.__name__ + if hasattr(mock_callable, "__qualname__"): + del mock_callable.__qualname__ + + result = get_callable_fullname(mock_callable) + # Should return repr of the object as fallback + self.assertTrue(result.startswith("<")) + + def test_exception_handling(self): + """Test exception handling in get_callable_fullname.""" + # Create a mock object that raises exception when accessing attributes + mock_callable = Mock() + mock_callable.__module__ = Mock(side_effect=Exception("Test exception")) + + result = get_callable_fullname(mock_callable) + # Should return repr as fallback when exceptions occur + self.assertTrue(result.startswith("", "MyClass", "my_function") + self.assertEqual(result, ".MyClass.my_function") + + def test_unknown_module_no_class(self): + """Test with unknown module and no class name.""" + result = _construct_qualified_name("", None, "my_function") + self.assertEqual(result, "my_function") + + def test_none_module_no_class(self): + """Test with None module and no class name.""" + result = _construct_qualified_name(None, None, "my_function") + self.assertEqual(result, "my_function") + + def test_empty_module_no_class(self): + """Test with empty module name and no class name.""" + result = _construct_qualified_name("", None, "my_function") + self.assertEqual(result, "my_function") + + def test_empty_class_name(self): + """Test with empty class name.""" + result = _construct_qualified_name("mymodule", "", "my_function") + # Empty class name is treated as falsy, so it's skipped + self.assertEqual(result, "mymodule.my_function") + + def test_empty_function_name(self): + """Test with empty function name.""" + result = _construct_qualified_name("mymodule", "MyClass", "") + self.assertEqual(result, "mymodule.MyClass.") + + def test_all_empty_strings(self): + """Test with all empty strings.""" + result = _construct_qualified_name("", "", "") + self.assertEqual(result, "") + + def test_whitespace_in_names(self): + """Test with whitespace in names.""" + result = _construct_qualified_name("my module", "My Class", "my function") + self.assertEqual(result, "my module.My Class.my function") + + +class TestUtilsIntegration(TestCase): + """Integration tests for utils functions.""" + + def test_frame_and_callable_consistency(self): + """Test that frame-based and callable-based functions give consistent results.""" + + def test_integration_func(): + frame = inspect.currentframe() + frame_name = get_function_fullname_from_frame(frame) + callable_name = get_callable_fullname(test_integration_func) + return frame_name, callable_name + + frame_name, callable_name = test_integration_func() + + # Both should end with the same function name, though they may have different levels of detail + self.assertTrue(frame_name.endswith("test_integration_func")) + self.assertTrue(callable_name.endswith("test_integration_func")) + # Both should start with the same module name + self.assertTrue(frame_name.startswith(__name__)) + self.assertTrue(callable_name.startswith(__name__)) + + def test_span_attributes_consistency(self): + """Test that both span attribute functions set consistent function names.""" + mock_span = Mock() + mock_span.is_recording.return_value = True + + def test_consistency_func(): + frame = inspect.currentframe() + # Test frame-based approach + add_code_attributes_to_span_from_frame(frame, mock_span) + + # Reset mock to check callable-based approach + mock_span.reset_mock() + mock_span.is_recording.return_value = True + + # Test callable-based approach + add_code_attributes_to_span(mock_span, test_consistency_func) + + return frame + + test_consistency_func() + + # Both approaches should have set the function name attribute + self.assertTrue(mock_span.set_attribute.called) + + @patch("amazon.opentelemetry.distro.code_correlation.utils.trace.get_current_span") + def test_decorator_integration(self, mock_get_current_span): + """Test decorator integration with span attribute functions.""" + mock_span = Mock() + mock_span.is_recording.return_value = True + mock_get_current_span.return_value = mock_span + + @record_code_attributes + def decorated_test_func(): + return "decorated" + + result = decorated_test_func() + + self.assertEqual(result, "decorated") + self.assertTrue(mock_span.set_attribute.called) + + # Check that function name was set + function_name_set = False + for call in mock_span.set_attribute.call_args_list: + if call[0][0] == CODE_FUNCTION_NAME: + function_name_set = True + self.assertTrue(call[0][1].endswith("decorated_test_func")) + break + + self.assertTrue(function_name_set, "Function name attribute was not set") diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/test_config.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/test_config.py new file mode 100644 index 000000000..c28e68606 --- /dev/null +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/test_config.py @@ -0,0 +1,680 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import json +import os +from unittest import TestCase +from unittest.mock import patch + +from amazon.opentelemetry.distro.code_correlation.config import _ENV_CONFIG, AwsCodeCorrelationConfig + + +class TestAwsCodeCorrelationConfig(TestCase): + """Test the AwsCodeCorrelationConfig class.""" + + def test_init_with_defaults(self): + """Test initialization with default parameters.""" + config = AwsCodeCorrelationConfig() + + self.assertEqual(config.include, []) + self.assertEqual(config.exclude, []) + self.assertEqual(config.stack_depth, 0) + + def test_init_with_none_parameters(self): + """Test initialization with None parameters.""" + config = AwsCodeCorrelationConfig(include=None, exclude=None, stack_depth=0) + + self.assertEqual(config.include, []) + self.assertEqual(config.exclude, []) + self.assertEqual(config.stack_depth, 0) + + def test_init_with_custom_parameters(self): + """Test initialization with custom parameters.""" + include = ["myapp", "mylib"] + exclude = ["third-party", "vendor"] + stack_depth = 10 + + config = AwsCodeCorrelationConfig(include=include, exclude=exclude, stack_depth=stack_depth) + + self.assertEqual(config.include, include) + self.assertEqual(config.exclude, exclude) + self.assertEqual(config.stack_depth, stack_depth) + + def test_init_with_empty_lists(self): + """Test initialization with empty lists.""" + config = AwsCodeCorrelationConfig(include=[], exclude=[], stack_depth=5) + + self.assertEqual(config.include, []) + self.assertEqual(config.exclude, []) + self.assertEqual(config.stack_depth, 5) + + def test_to_dict(self): + """Test conversion to dictionary.""" + config = AwsCodeCorrelationConfig(include=["app1", "app2"], exclude=["lib1", "lib2"], stack_depth=15) + + result = config.to_dict() + + expected = {"include": ["app1", "app2"], "exclude": ["lib1", "lib2"], "stack_depth": 15} + self.assertEqual(result, expected) + + def test_to_dict_with_defaults(self): + """Test conversion to dictionary with default values.""" + config = AwsCodeCorrelationConfig() + + result = config.to_dict() + + expected = {"include": [], "exclude": [], "stack_depth": 0} + self.assertEqual(result, expected) + + def test_to_json_with_indent(self): + """Test conversion to JSON with indentation.""" + config = AwsCodeCorrelationConfig(include=["myapp"], exclude=["vendor"], stack_depth=5) + + result = config.to_json(indent=2) + + expected_dict = {"include": ["myapp"], "exclude": ["vendor"], "stack_depth": 5} + expected_json = json.dumps(expected_dict, indent=2) + self.assertEqual(result, expected_json) + + def test_to_json_without_indent(self): + """Test conversion to JSON without indentation.""" + config = AwsCodeCorrelationConfig(include=["myapp"], exclude=["vendor"], stack_depth=5) + + result = config.to_json(indent=None) + + expected_dict = {"include": ["myapp"], "exclude": ["vendor"], "stack_depth": 5} + expected_json = json.dumps(expected_dict, indent=None) + self.assertEqual(result, expected_json) + + def test_to_json_default_indent(self): + """Test conversion to JSON with default indentation.""" + config = AwsCodeCorrelationConfig(include=["myapp"], exclude=["vendor"], stack_depth=5) + + result = config.to_json() + + expected_dict = {"include": ["myapp"], "exclude": ["vendor"], "stack_depth": 5} + expected_json = json.dumps(expected_dict, indent=2) + self.assertEqual(result, expected_json) + + def test_repr(self): + """Test string representation.""" + config = AwsCodeCorrelationConfig(include=["app1", "app2"], exclude=["lib1"], stack_depth=10) + + result = repr(config) + + expected = "AwsCodeCorrelationConfig(include=['app1', 'app2'], exclude=['lib1'], stack_depth=10)" + self.assertEqual(result, expected) + + def test_repr_with_defaults(self): + """Test string representation with default values.""" + config = AwsCodeCorrelationConfig() + + result = repr(config) + + expected = "AwsCodeCorrelationConfig(include=[], exclude=[], stack_depth=0)" + self.assertEqual(result, expected) + + +class TestAwsCodeCorrelationConfigFromEnv(TestCase): # pylint: disable=too-many-public-methods + """Test the from_env class method.""" + + def setUp(self): + """Set up test fixtures by clearing environment variable.""" + self.env_patcher = patch.dict(os.environ, {}, clear=False) + self.env_patcher.start() + if _ENV_CONFIG in os.environ: + del os.environ[_ENV_CONFIG] + + def tearDown(self): + """Clean up test fixtures.""" + self.env_patcher.stop() + + def test_from_env_no_environment_variable(self): + """Test from_env when environment variable is not set.""" + config = AwsCodeCorrelationConfig.from_env() + + self.assertEqual(config.include, []) + self.assertEqual(config.exclude, []) + self.assertEqual(config.stack_depth, 0) + + def test_from_env_empty_environment_variable(self): + """Test from_env when environment variable is empty.""" + os.environ[_ENV_CONFIG] = "" + + config = AwsCodeCorrelationConfig.from_env() + + self.assertEqual(config.include, []) + self.assertEqual(config.exclude, []) + self.assertEqual(config.stack_depth, 0) + + def test_from_env_whitespace_only_environment_variable(self): + """Test from_env when environment variable contains only whitespace.""" + os.environ[_ENV_CONFIG] = " \t\n " + + config = AwsCodeCorrelationConfig.from_env() + + self.assertEqual(config.include, []) + self.assertEqual(config.exclude, []) + self.assertEqual(config.stack_depth, 0) + + def test_from_env_empty_json_object(self): + """Test from_env with empty JSON object.""" + os.environ[_ENV_CONFIG] = "{}" + + config = AwsCodeCorrelationConfig.from_env() + + self.assertEqual(config.include, []) + self.assertEqual(config.exclude, []) + self.assertEqual(config.stack_depth, 0) + + def test_from_env_complete_configuration(self): + """Test from_env with complete configuration.""" + config_data = {"include": ["myapp", "mylib"], "exclude": ["third-party", "vendor"], "stack_depth": 15} + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + self.assertEqual(config.include, ["myapp", "mylib"]) + self.assertEqual(config.exclude, ["third-party", "vendor"]) + self.assertEqual(config.stack_depth, 15) + + def test_from_env_partial_configuration(self): + """Test from_env with partial configuration.""" + config_data = {"include": ["myapp"]} + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + self.assertEqual(config.include, ["myapp"]) + self.assertEqual(config.exclude, []) # Default value + self.assertEqual(config.stack_depth, 0) # Default value + + def test_from_env_only_exclude(self): + """Test from_env with only exclude configuration.""" + config_data = {"exclude": ["vendor", "third-party"]} + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + self.assertEqual(config.include, []) # Default value + self.assertEqual(config.exclude, ["vendor", "third-party"]) + self.assertEqual(config.stack_depth, 0) # Default value + + def test_from_env_only_stack_depth(self): + """Test from_env with only stack_depth configuration.""" + config_data = {"stack_depth": 25} + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + self.assertEqual(config.include, []) # Default value + self.assertEqual(config.exclude, []) # Default value + self.assertEqual(config.stack_depth, 25) + + def test_from_env_zero_stack_depth(self): + """Test from_env with zero stack_depth (unlimited).""" + config_data = {"stack_depth": 0} + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + self.assertEqual(config.stack_depth, 0) + + @patch("amazon.opentelemetry.distro.code_correlation.config._logger") + def test_from_env_negative_stack_depth(self, mock_logger): + """Test from_env with negative stack_depth.""" + config_data = {"stack_depth": -5} + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + # Negative stack_depth should be corrected to 0 + self.assertEqual(config.stack_depth, 0) + + # Should log a warning + mock_logger.warning.assert_called_once() + args = mock_logger.warning.call_args[0] + self.assertIn("'stack_depth'", args[0]) + self.assertIn("must be non-negative", args[0]) + self.assertEqual(args[1], _ENV_CONFIG) + self.assertEqual(args[2], -5) + + def test_from_env_empty_include_list(self): + """Test from_env with explicitly empty include list.""" + config_data = {"include": [], "exclude": ["vendor"], "stack_depth": 5} + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + self.assertEqual(config.include, []) + self.assertEqual(config.exclude, ["vendor"]) + self.assertEqual(config.stack_depth, 5) + + def test_from_env_empty_exclude_list(self): + """Test from_env with explicitly empty exclude list.""" + config_data = {"include": ["myapp"], "exclude": [], "stack_depth": 5} + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + self.assertEqual(config.include, ["myapp"]) + self.assertEqual(config.exclude, []) + self.assertEqual(config.stack_depth, 5) + + def test_from_env_single_item_lists(self): + """Test from_env with single-item lists.""" + config_data = {"include": ["single_app"], "exclude": ["single_vendor"]} + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + self.assertEqual(config.include, ["single_app"]) + self.assertEqual(config.exclude, ["single_vendor"]) + + @patch("amazon.opentelemetry.distro.code_correlation.config._logger") + def test_from_env_invalid_json(self, mock_logger): + """Test from_env with invalid JSON.""" + os.environ[_ENV_CONFIG] = "invalid json {" + + config = AwsCodeCorrelationConfig.from_env() + + # Should use default values when JSON is invalid + self.assertEqual(config.include, []) + self.assertEqual(config.exclude, []) + self.assertEqual(config.stack_depth, 0) + + # Should log a warning + mock_logger.warning.assert_called_once() + args = mock_logger.warning.call_args[0] + self.assertIn("Invalid JSON", args[0]) + self.assertEqual(args[1], _ENV_CONFIG) + + @patch("amazon.opentelemetry.distro.code_correlation.config._logger") + def test_from_env_malformed_json_syntax_error(self, mock_logger): + """Test from_env with malformed JSON that causes syntax error.""" + os.environ[_ENV_CONFIG] = '{"include": ["app1", "app2"' # Missing closing bracket and brace + + config = AwsCodeCorrelationConfig.from_env() + + # Should use default values + self.assertEqual(config.include, []) + self.assertEqual(config.exclude, []) + self.assertEqual(config.stack_depth, 0) + + # Should log a warning + mock_logger.warning.assert_called_once() + + @patch("amazon.opentelemetry.distro.code_correlation.config._logger") + def test_from_env_non_object_json(self, mock_logger): + """Test from_env with valid JSON that's not an object.""" + os.environ[_ENV_CONFIG] = '["not", "an", "object"]' + + config = AwsCodeCorrelationConfig.from_env() + + # Should handle gracefully and use defaults + self.assertEqual(config.include, []) + self.assertEqual(config.exclude, []) + self.assertEqual(config.stack_depth, 0) + + # Should log a warning about non-object JSON + mock_logger.warning.assert_called_once() + args = mock_logger.warning.call_args[0] + self.assertIn("Configuration in", args[0]) + self.assertIn("must be a JSON object", args[0]) + self.assertEqual(args[1], _ENV_CONFIG) + self.assertEqual(args[2], "list") + + def test_from_env_extra_fields_ignored(self): + """Test from_env ignores extra fields in JSON.""" + config_data = { + "include": ["myapp"], + "exclude": ["vendor"], + "stack_depth": 10, + "extra_field": "ignored", + "another_extra": 42, + } + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + self.assertEqual(config.include, ["myapp"]) + self.assertEqual(config.exclude, ["vendor"]) + self.assertEqual(config.stack_depth, 10) + # Extra fields should not affect the configuration + + @patch("amazon.opentelemetry.distro.code_correlation.config._logger") + def test_from_env_wrong_type_values(self, mock_logger): + """Test from_env with wrong type values.""" + config_data = { + "include": "not_a_list", # Should be a list + "exclude": 42, # Should be a list + "stack_depth": "not_a_number", # Should be a number + } + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + # Should validate and use defaults for invalid types + self.assertEqual(config.include, []) # Corrected to empty list + self.assertEqual(config.exclude, []) # Corrected to empty list + self.assertEqual(config.stack_depth, 0) # Corrected to default value + + # Should log warnings for all invalid types + self.assertEqual(mock_logger.warning.call_count, 3) + warning_calls = [call[0] for call in mock_logger.warning.call_args_list] + + # Check for include warning - format string and arguments + include_warnings = [call for call in warning_calls if "must be a list" in call[0] and call[1] == "include"] + self.assertEqual(len(include_warnings), 1) + self.assertEqual(include_warnings[0][1], "include") + self.assertEqual(include_warnings[0][2], _ENV_CONFIG) + self.assertEqual(include_warnings[0][3], "str") + + # Check for exclude warning + exclude_warnings = [call for call in warning_calls if "must be a list" in call[0] and call[1] == "exclude"] + self.assertEqual(len(exclude_warnings), 1) + self.assertEqual(exclude_warnings[0][1], "exclude") + self.assertEqual(exclude_warnings[0][2], _ENV_CONFIG) + self.assertEqual(exclude_warnings[0][3], "int") + + # Check for stack_depth warning + stack_warnings = [ + call for call in warning_calls if "'stack_depth'" in call[0] and "must be an integer" in call[0] + ] + self.assertEqual(len(stack_warnings), 1) + self.assertEqual(stack_warnings[0][1], _ENV_CONFIG) + self.assertEqual(stack_warnings[0][2], "str") + + @patch("amazon.opentelemetry.distro.code_correlation.config._logger") + def test_from_env_null_values(self, mock_logger): + """Test from_env with null values in JSON.""" + config_data = {"include": None, "exclude": None, "stack_depth": None} + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + # get() should return None for null values, and validation should handle it + self.assertEqual(config.include, []) # Constructor converts None to [] + self.assertEqual(config.exclude, []) # Constructor converts None to [] + self.assertEqual(config.stack_depth, 0) # None should be corrected to default value + + # Should log warnings for invalid types + self.assertEqual(mock_logger.warning.call_count, 3) + warning_calls = [call[0] for call in mock_logger.warning.call_args_list] + + # Check for include warning + include_warnings = [call for call in warning_calls if "must be a list" in call[0] and call[1] == "include"] + self.assertEqual(len(include_warnings), 1) + self.assertEqual(include_warnings[0][1], "include") + self.assertEqual(include_warnings[0][2], _ENV_CONFIG) + self.assertEqual(include_warnings[0][3], "NoneType") + + # Check for exclude warning + exclude_warnings = [call for call in warning_calls if "must be a list" in call[0] and call[1] == "exclude"] + self.assertEqual(len(exclude_warnings), 1) + self.assertEqual(exclude_warnings[0][1], "exclude") + self.assertEqual(exclude_warnings[0][2], _ENV_CONFIG) + self.assertEqual(exclude_warnings[0][3], "NoneType") + + # Check for stack_depth warning + stack_warnings = [ + call for call in warning_calls if "'stack_depth'" in call[0] and "must be an integer" in call[0] + ] + self.assertEqual(len(stack_warnings), 1) + self.assertEqual(stack_warnings[0][1], _ENV_CONFIG) + self.assertEqual(stack_warnings[0][2], "NoneType") + + def test_from_env_complex_package_names(self): + """Test from_env with complex package names.""" + config_data = { + "include": ["my.app.module", "com.company.service", "app_with_underscores", "app-with-dashes"], + "exclude": ["third.party.lib", "vendor.package.name", "test_framework"], + } + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + self.assertEqual( + config.include, ["my.app.module", "com.company.service", "app_with_underscores", "app-with-dashes"] + ) + self.assertEqual(config.exclude, ["third.party.lib", "vendor.package.name", "test_framework"]) + + def test_from_env_large_stack_depth(self): + """Test from_env with large stack depth value.""" + config_data = {"stack_depth": 999999} + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + self.assertEqual(config.stack_depth, 999999) + + def test_env_constant_value(self): + """Test that the environment variable constant has the expected value.""" + self.assertEqual(_ENV_CONFIG, "OTEL_AWS_CODE_CORRELATION_CONFIG") + + @patch("amazon.opentelemetry.distro.code_correlation.config._logger") + def test_from_env_mixed_type_include_list(self, mock_logger): + """Test from_env with include list containing mixed types.""" + config_data = { + "include": ["valid_string", 123, True, None, "another_valid_string"], + "exclude": [], + "stack_depth": 5, + } + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + # Should only keep string items + self.assertEqual(config.include, ["valid_string", "another_valid_string"]) + self.assertEqual(config.exclude, []) + self.assertEqual(config.stack_depth, 5) + + # Should log warnings for non-string items + self.assertEqual(mock_logger.warning.call_count, 3) # Exactly 3 non-string items + warning_calls = [call[0] for call in mock_logger.warning.call_args_list] + + # Check that warnings mention non-string items being skipped + include_warnings = [ + call + for call in warning_calls + if "list item" in call[0] and "must be a string" in call[0] and call[1] == "include" + ] + self.assertEqual(len(include_warnings), 3) # 123, True, None + + # Verify the specific types logged + logged_types = [call[3] for call in include_warnings] + self.assertIn("int", logged_types) # 123 + self.assertIn("bool", logged_types) # True + self.assertIn("NoneType", logged_types) # None + + @patch("amazon.opentelemetry.distro.code_correlation.config._logger") + def test_from_env_mixed_type_exclude_list(self, mock_logger): + """Test from_env with exclude list containing mixed types.""" + config_data = { + "include": [], + "exclude": ["valid_exclude", 42, False, "another_valid_exclude", [1, 2, 3]], + "stack_depth": 10, + } + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + # Should only keep string items + self.assertEqual(config.include, []) + self.assertEqual(config.exclude, ["valid_exclude", "another_valid_exclude"]) + self.assertEqual(config.stack_depth, 10) + + # Should log warnings for non-string items + self.assertEqual(mock_logger.warning.call_count, 3) # Exactly 3 non-string items + warning_calls = [call[0] for call in mock_logger.warning.call_args_list] + + # Check that warnings mention non-string items being skipped + exclude_warnings = [ + call + for call in warning_calls + if "list item" in call[0] and "must be a string" in call[0] and call[1] == "exclude" + ] + self.assertEqual(len(exclude_warnings), 3) # 42, False, [1, 2, 3] + + # Verify the specific types logged + logged_types = [call[3] for call in exclude_warnings] + self.assertIn("int", logged_types) # 42 + self.assertIn("bool", logged_types) # False + self.assertIn("list", logged_types) # [1, 2, 3] + + @patch("amazon.opentelemetry.distro.code_correlation.config._logger") + def test_from_env_all_non_string_list_items(self, mock_logger): + """Test from_env with lists containing only non-string types.""" + config_data = { + "include": [123, True, None, {"key": "value"}, [1, 2, 3]], + "exclude": [456, False, 0, 1.5, {"another": "object"}], + "stack_depth": 5, + } + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + # Should result in empty lists since no valid strings + self.assertEqual(config.include, []) + self.assertEqual(config.exclude, []) + self.assertEqual(config.stack_depth, 5) + + # Should log warnings for all non-string items + self.assertEqual(mock_logger.warning.call_count, 10) # Exactly 10 non-string items + warning_calls = [call[0] for call in mock_logger.warning.call_args_list] + + # Check include warnings + include_warnings = [ + call + for call in warning_calls + if "list item" in call[0] and "must be a string" in call[0] and call[1] == "include" + ] + self.assertEqual(len(include_warnings), 5) + + # Check exclude warnings + exclude_warnings = [ + call + for call in warning_calls + if "list item" in call[0] and "must be a string" in call[0] and call[1] == "exclude" + ] + self.assertEqual(len(exclude_warnings), 5) + + # Verify that different types are logged + include_logged_types = [call[3] for call in include_warnings] + exclude_logged_types = [call[3] for call in exclude_warnings] + + # For include: [123, True, None, {"key": "value"}, [1, 2, 3]] + self.assertIn("int", include_logged_types) # 123 + self.assertIn("bool", include_logged_types) # True + self.assertIn("NoneType", include_logged_types) # None + self.assertIn("dict", include_logged_types) # {"key": "value"} + self.assertIn("list", include_logged_types) # [1, 2, 3] + + # For exclude: [456, False, 0, 1.5, {"another": "object"}] + self.assertIn("int", exclude_logged_types) # 456 and 0 + self.assertIn("bool", exclude_logged_types) # False + self.assertIn("float", exclude_logged_types) # 1.5 + self.assertIn("dict", exclude_logged_types) # {"another": "object"} + + @patch("amazon.opentelemetry.distro.code_correlation.config._logger") + def test_from_env_float_stack_depth(self, mock_logger): + """Test from_env with float stack_depth.""" + config_data = {"include": ["myapp"], "exclude": ["vendor"], "stack_depth": 5.7} + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + # Float should be treated as invalid and corrected to 0 + self.assertEqual(config.include, ["myapp"]) + self.assertEqual(config.exclude, ["vendor"]) + self.assertEqual(config.stack_depth, 0) + + # Should log warning for invalid stack_depth type + mock_logger.warning.assert_called_once() + args = mock_logger.warning.call_args[0] + self.assertIn("'stack_depth'", args[0]) + self.assertIn("must be an integer", args[0]) + self.assertEqual(args[1], _ENV_CONFIG) + self.assertEqual(args[2], "float") + + @patch("amazon.opentelemetry.distro.code_correlation.config._logger") + def test_from_env_empty_string_in_lists(self, mock_logger): + """Test from_env with empty strings in lists.""" + config_data = { + "include": ["valid", "", "also_valid", ""], + "exclude": ["", "valid_exclude", ""], + "stack_depth": 5, + } + os.environ[_ENV_CONFIG] = json.dumps(config_data) + + config = AwsCodeCorrelationConfig.from_env() + + # Empty strings are still strings, so they should be preserved + self.assertEqual(config.include, ["valid", "", "also_valid", ""]) + self.assertEqual(config.exclude, ["", "valid_exclude", ""]) + self.assertEqual(config.stack_depth, 5) + + # Should not log warnings since empty strings are valid strings + mock_logger.warning.assert_not_called() + + +class TestAwsCodeCorrelationConfigIntegration(TestCase): + """Integration tests for AwsCodeCorrelationConfig.""" + + def setUp(self): + """Set up test fixtures.""" + self.env_patcher = patch.dict(os.environ, {}, clear=False) + self.env_patcher.start() + if _ENV_CONFIG in os.environ: + del os.environ[_ENV_CONFIG] + + def tearDown(self): + """Clean up test fixtures.""" + self.env_patcher.stop() + + def test_roundtrip_to_dict_from_env(self): + """Test roundtrip: config -> to_dict -> env -> from_env -> config.""" + original_config = AwsCodeCorrelationConfig( + include=["app1", "app2"], exclude=["vendor1", "vendor2"], stack_depth=20 + ) + + # Convert to dict and then to JSON for environment + config_dict = original_config.to_dict() + os.environ[_ENV_CONFIG] = json.dumps(config_dict) + + # Create new config from environment + new_config = AwsCodeCorrelationConfig.from_env() + + # Should be equivalent + self.assertEqual(new_config.include, original_config.include) + self.assertEqual(new_config.exclude, original_config.exclude) + self.assertEqual(new_config.stack_depth, original_config.stack_depth) + self.assertEqual(new_config.to_dict(), original_config.to_dict()) + + def test_roundtrip_to_json_from_env(self): + """Test roundtrip: config -> to_json -> env -> from_env -> config.""" + original_config = AwsCodeCorrelationConfig(include=["myapp"], exclude=["third-party"], stack_depth=5) + + # Convert to JSON for environment + config_json = original_config.to_json(indent=None) # Compact JSON + os.environ[_ENV_CONFIG] = config_json + + # Create new config from environment + new_config = AwsCodeCorrelationConfig.from_env() + + # Should be equivalent + self.assertEqual(new_config.include, original_config.include) + self.assertEqual(new_config.exclude, original_config.exclude) + self.assertEqual(new_config.stack_depth, original_config.stack_depth) + self.assertEqual(new_config.to_json(indent=None), original_config.to_json(indent=None)) + + def test_config_equality_comparison(self): + """Test that configs with same values produce same representations.""" + config1 = AwsCodeCorrelationConfig(include=["app"], exclude=["vendor"], stack_depth=10) + + config2 = AwsCodeCorrelationConfig(include=["app"], exclude=["vendor"], stack_depth=10) + + # They should have the same string representation + self.assertEqual(repr(config1), repr(config2)) + self.assertEqual(config1.to_dict(), config2.to_dict()) + self.assertEqual(config1.to_json(), config2.to_json()) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py index 4fcd71cb5..abe36e8e4 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py @@ -777,6 +777,72 @@ def test_customize_span_processors(self): os.environ.pop("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") + def test_customize_span_processors_with_code_correlation_enabled(self): + """Test that CodeAttributesSpanProcessor is added when code correlation is enabled""" + mock_tracer_provider: TracerProvider = MagicMock() + + # Clean up environment to ensure consistent test state + os.environ.pop("AGENT_OBSERVABILITY_ENABLED", None) + os.environ.pop("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", None) + os.environ.pop(CODE_CORRELATION_ENABLED_CONFIG, None) + + # Test without code correlation enabled - should not add CodeAttributesSpanProcessor + _customize_span_processors(mock_tracer_provider, Resource.get_empty()) + self.assertEqual(mock_tracer_provider.add_span_processor.call_count, 0) + + mock_tracer_provider.reset_mock() + + # Test with code correlation enabled - should add CodeAttributesSpanProcessor + os.environ[CODE_CORRELATION_ENABLED_CONFIG] = "true" + + with patch( + "amazon.opentelemetry.distro.code_correlation.CodeAttributesSpanProcessor" + ) as mock_code_processor_class: + mock_code_processor_instance = MagicMock() + mock_code_processor_class.return_value = mock_code_processor_instance + + _customize_span_processors(mock_tracer_provider, Resource.get_empty()) + + # Verify CodeAttributesSpanProcessor was created and added + mock_code_processor_class.assert_called_once() + mock_tracer_provider.add_span_processor.assert_called_once_with(mock_code_processor_instance) + + mock_tracer_provider.reset_mock() + + # Test with code correlation enabled along with application signals + os.environ[CODE_CORRELATION_ENABLED_CONFIG] = "true" + os.environ["OTEL_AWS_APPLICATION_SIGNALS_ENABLED"] = "True" + os.environ["OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED"] = "False" + + with patch( + "amazon.opentelemetry.distro.code_correlation.CodeAttributesSpanProcessor" + ) as mock_code_processor_class: + mock_code_processor_instance = MagicMock() + mock_code_processor_class.return_value = mock_code_processor_instance + + _customize_span_processors(mock_tracer_provider, Resource.get_empty()) + + # Should have 3 processors: CodeAttributesSpanProcessor, AttributePropagatingSpanProcessor, + # and AwsSpanMetricsProcessor + self.assertEqual(mock_tracer_provider.add_span_processor.call_count, 3) + + # First should be CodeAttributesSpanProcessor + first_call_args = mock_tracer_provider.add_span_processor.call_args_list[0].args[0] + self.assertEqual(first_call_args, mock_code_processor_instance) + + # Second should be AttributePropagatingSpanProcessor + second_call_args = mock_tracer_provider.add_span_processor.call_args_list[1].args[0] + self.assertIsInstance(second_call_args, AttributePropagatingSpanProcessor) + + # Third should be AwsSpanMetricsProcessor + third_call_args = mock_tracer_provider.add_span_processor.call_args_list[2].args[0] + self.assertIsInstance(third_call_args, AwsSpanMetricsProcessor) + + # Clean up + os.environ.pop(CODE_CORRELATION_ENABLED_CONFIG, None) + os.environ.pop("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", None) + os.environ.pop("OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED", None) + def test_customize_span_processors_lambda(self): mock_tracer_provider: TracerProvider = MagicMock() # Clean up environment to ensure consistent test state