Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correlation id utilities (GSI 501) #74

Merged
merged 7 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .deprecated_files
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
.github/workflows/check_mandatory_and_static_files.yaml
.github/workflows/dev_cd.yaml
.github/workflows/unit_and_int_tests.yaml
.github/workflows/cd.yaml

scripts/check_mandatory_and_static_files.py
scripts/update_static_files.py
Expand Down
7 changes: 5 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
},
"editor.formatOnSave": true,
"editor.renderWhitespace": "all",
"editor.rulers": [
88
],
"editor.defaultFormatter": "ms-python.black-formatter",
"ruff.organizeImports": true,
"editor.defaultFormatter": "charliermarsh.ruff",
"licenser.license": "Custom",
"licenser.customHeaderFile": "/workspace/.devcontainer/license_header.txt"
},
Expand All @@ -52,7 +56,6 @@
"visualstudioexptteam.vscodeintellicode",
"ymotongpoo.licenser",
"charliermarsh.ruff",
"ms-python.black-formatter",
"ms-python.mypy-type-checker"
]
}
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/static_code_analysis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ jobs:
env:
SKIP: no-commit-to-branch
- name: ruff
uses: chartboost/ruff-action@v1
- name: black
run: |
black --check .
ruff check --output-format=github .
ruff format --check .
- name: mypy
run: |
mypy .
Expand Down
7 changes: 2 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,9 @@ repos:
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/psf/black
rev: 23.11.0
hooks:
- id: black
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.7.0
rev: v1.7.1
hooks:
- id: mypy
args: [--no-warn-unused-ignores]
2 changes: 2 additions & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ ignore = [
"D400", # first doc line ends in period
"D401", # non-imperative-mood
"D107", # missing docstring in __init__
"D206", # indent-with-spaces (ignored for formatter)
"D300", # triple-single-quotes (ignored for formatter)
]

line-length = 88
Expand Down
9 changes: 3 additions & 6 deletions examples/stream_calc/stream_calc/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,12 @@

"""Config parameters."""

from hexkit.providers.akafka import KafkaConfig
from typing import Literal

from hexkit.providers.akafka.provider import KafkaConfig
from stream_calc.translators.eventpub import EventResultEmitterConfig
from stream_calc.translators.eventsub import EventProblemReceiverConfig

try: # workaround for https://github.com/pydantic/pydantic/issues/5821
from typing_extensions import Literal
except ImportError:
from typing import Literal # type: ignore

LOGLEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]


Expand Down
1 change: 0 additions & 1 deletion examples/stream_calc/stream_calc/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

"""Module hosting the dependency injection container."""

# pylint: disable=wrong-import-order
from hexkit.inject import ContainerBase, get_configurator, get_constructor
from hexkit.providers.akafka import KafkaEventPublisher, KafkaEventSubscriber
from stream_calc.config import Config
Expand Down
4 changes: 3 additions & 1 deletion examples/stream_calc/stream_calc/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ def get_container(config: Config) -> Container:


async def main(
*, config: Config = Config(), run_forever: bool = True # type: ignore
*,
config: Config = Config(), # type: ignore [call-arg]
run_forever: bool = True,
) -> None:
"""
Coroutine to run the stream calculator.
Expand Down
2 changes: 1 addition & 1 deletion examples/stream_calc/stream_calc/translators/eventsub.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ async def _consume_validated(
type_: Ascii,
# This implementation does NOT use the `topic` information that is provided as
# part of the EventSubscriberProtocol:
topic: Ascii, # pylint: disable=unused-argument
topic: Ascii,
) -> None:
"""
Receive and process an event with already validated topic and type.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "hexkit"
version = "1.0.0"
version = "1.1.0"
description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
readme = "README.md"
authors = [
Expand Down
2 changes: 0 additions & 2 deletions requirements-dev-common.in
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ mypy-extensions>=1.0.0

ruff>=0.0.290

black>=23.1.0

click>=8.1.0
typer>=0.7.0

Expand Down
575 changes: 272 additions & 303 deletions requirements-dev.txt

Large diffs are not rendered by default.

396 changes: 198 additions & 198 deletions requirements.txt

Large diffs are not rendered by default.

97 changes: 97 additions & 0 deletions src/hexkit/correlation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
# for the German Human Genome-Phenome Archive (GHGA)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""Utilities related to correlation IDs"""

import logging
from contextlib import asynccontextmanager
from contextvars import ContextVar
from uuid import UUID, uuid4

from hexkit.utils import set_context_var

log = logging.getLogger()

correlation_id_var: ContextVar[str] = ContextVar("correlation_id", default="")

__all__ = [
"set_correlation_id",
"get_correlation_id",
"new_correlation_id",
"validate_correlation_id",
"CorrelationIdContextError",
"InvalidCorrelationIdError",
]


class CorrelationIdContextError(RuntimeError):
"""Raised when the correlation ID ContextVar is unexpectedly not set."""


class InvalidCorrelationIdError(RuntimeError):
"""Raised when a correlation ID fails validation."""

def __init__(self, *, correlation_id: str):
message = f"Invalid correlation ID found: '{correlation_id}'"
super().__init__(message)


def new_correlation_id() -> str:
"""Generates a new correlation ID."""
return str(uuid4())


def validate_correlation_id(correlation_id: str):
"""Raises an error if the correlation ID is invalid.

Raises:
InvalidCorrelationIdError: If the correlation ID is empty or invalid.
"""
try:
UUID(correlation_id)
except ValueError as err:
raise InvalidCorrelationIdError(correlation_id=correlation_id) from err


@asynccontextmanager
async def set_correlation_id(correlation_id: str):
"""Set the correlation ID for the life of the context.

Raises:
InvalidCorrelationIdError: when the correlation ID is empty or invalid.
"""
validate_correlation_id(correlation_id)

async with set_context_var(correlation_id_var, correlation_id):
log.info("Set context correlation ID to %s", correlation_id)
yield


def get_correlation_id() -> str:
"""Get the correlation ID.

This should only be called when the correlation ID ContextVar is expected to be set.

Raises:
CorrelationIdContextError: when the correlation ID ContextVar is not set.
InvalidCorrelationIdError: when the correlation ID is invalid.
"""
if not (correlation_id := correlation_id_var.get()):
raise CorrelationIdContextError()

validate_correlation_id(correlation_id)

return correlation_id
5 changes: 0 additions & 5 deletions src/hexkit/inject.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@
"Configurator",
]

# pylint: disable=c-extension-no-member


class NotConstructableError(TypeError):
"""Thrown when an AsyncContextConstructable was expected but not obtained."""
Expand Down Expand Up @@ -108,9 +106,6 @@ async def resource(*args: Any, **kwargs: Any) -> AsyncIterator[Any]:

return resource

# This pylint error is inherited from the dependency_injector framework, other
# DI providers use the same basic signature:
# pylint: disable=keyword-arg-before-vararg
def __init__(
self,
provides: Optional[
Expand Down
2 changes: 1 addition & 1 deletion src/hexkit/protocols/dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ async def _get_dao(

@overload
@abstractmethod
async def _get_dao( # pylint: disable=arguments-differ
async def _get_dao(
self,
*,
name: str,
Expand Down
2 changes: 0 additions & 2 deletions src/hexkit/protocols/objstorage.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ async def abort_multipart_upload(
upload_id=upload_id, bucket_id=bucket_id, object_id=object_id
)

# pylint: disable=too-many-arguments
async def complete_multipart_upload(
self,
*,
Expand Down Expand Up @@ -367,7 +366,6 @@ async def _abort_multipart_upload(
"""
...

# pylint: disable=too-many-arguments
@abstractmethod
async def _complete_multipart_upload(
self,
Expand Down
9 changes: 1 addition & 8 deletions src/hexkit/providers/akafka/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import logging
import ssl
from contextlib import asynccontextmanager
from typing import Any, Callable, Optional, Protocol, TypeVar
from typing import Any, Callable, Literal, Optional, Protocol, TypeVar

from aiokafka import AIOKafkaConsumer, AIOKafkaProducer
from aiokafka.helpers import create_ssl_context
Expand All @@ -37,11 +37,6 @@
from hexkit.protocols.eventpub import EventPublisherProtocol
from hexkit.protocols.eventsub import EventSubscriberProtocol

try: # workaround for https://github.com/pydantic/pydantic/issues/5821
from typing_extensions import Literal
except ImportError:
from typing import Literal # type: ignore

__all__ = [
"KafkaConfig",
"KafkaEventPublisher",
Expand Down Expand Up @@ -358,8 +353,6 @@ async def construct(
finally:
await consumer.stop()

# pylint: disable=too-many-arguments
# (some arguments are only used for testing)
def __init__(
self, *, consumer: KafkaConsumerCompatible, translator: EventSubscriberProtocol
):
Expand Down
2 changes: 1 addition & 1 deletion src/hexkit/providers/mongodb/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ async def _get_dao(
...

@overload
async def _get_dao( # pylint: disable=arguments-differ
async def _get_dao(
self,
*,
name: str,
Expand Down
10 changes: 2 additions & 8 deletions src/hexkit/providers/s3/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ class S3Config(BaseSettings):
description=(
"Path to a config file for specifying more advanced S3 parameters."
+ " This should follow the format described here:"
# pylint: disable=line-too-long
+ " https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#using-a-configuration-file"
),
)
Expand All @@ -114,12 +113,10 @@ def read_aws_config_ini(aws_config_ini: Path) -> botocore.config.Config:
return botocore.config.Config(**config_profile)


class S3ObjectStorage(
ObjectStorageProtocol
): # pylint: disable=too-many-instance-attributes
class S3ObjectStorage(ObjectStorageProtocol):
"""S3-based provider implementing the ObjectStorageProtocol."""

def __init__( # pylint: disable=too-many-arguments
def __init__(
self,
*,
config: S3Config,
Expand Down Expand Up @@ -548,7 +545,6 @@ async def _get_parts_info(
object_id=object_id,
) from error

# pylint: disable=too-many-arguments
async def _check_uploaded_parts(
self,
*,
Expand Down Expand Up @@ -629,7 +625,6 @@ async def _check_uploaded_parts(
+ f" first part which had only {first_part_size}.",
)

# pylint: disable=too-many-arguments
async def _abort_multipart_upload(
self,
*,
Expand Down Expand Up @@ -686,7 +681,6 @@ async def _abort_multipart_upload(
upload_id=upload_id, bucket_id=bucket_id, object_id=object_id
)

# pylint: disable=too-many-arguments
async def _complete_multipart_upload(
self,
*,
Expand Down
3 changes: 0 additions & 3 deletions src/hexkit/providers/s3/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@ def check_part_size(file_path: Path, anticipated_size: int) -> None:
)


# pylint: disable=too-many-arguments
async def upload_part(
storage_dao: ObjectStorageProtocol,
upload_id: str,
Expand All @@ -220,7 +219,6 @@ async def upload_part(
response.raise_for_status()


# pylint: disable=too-many-arguments
async def upload_part_of_size(
storage_dao: ObjectStorageProtocol,
upload_id: str,
Expand Down Expand Up @@ -372,7 +370,6 @@ async def prepare_non_completed_upload(s3_fixture_: S3Fixture):
# This workflow is defined as a seperate function so that it can also be used
# outside of the `tests` package e.g. to test the compliance of an S3-compatible
# object storage implementation:
# pylint: disable=too-many-arguments
async def typical_workflow(
storage_client: ObjectStorageProtocol,
test_file_path: Path,
Expand Down
Loading