hexkit 3.0.1__tar.gz → 3.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. {hexkit-3.0.1/src/hexkit.egg-info → hexkit-3.1.0}/PKG-INFO +8 -8
  2. {hexkit-3.0.1 → hexkit-3.1.0}/pyproject.toml +8 -8
  3. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/custom_types.py +1 -1
  4. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/protocols/dao.py +1 -1
  5. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/protocols/objstorage.py +4 -4
  6. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/akafka/testcontainer.py +1 -1
  7. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/akafka/testutils.py +128 -35
  8. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/mongodb/provider.py +1 -1
  9. hexkit-3.1.0/src/hexkit/providers/mongodb/testutils.py +176 -0
  10. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/mongokafka/provider.py +16 -7
  11. hexkit-3.1.0/src/hexkit/providers/mongokafka/testutils.py +73 -0
  12. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/s3/testutils.py +111 -15
  13. {hexkit-3.0.1 → hexkit-3.1.0/src/hexkit.egg-info}/PKG-INFO +8 -8
  14. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit.egg-info/SOURCES.txt +1 -0
  15. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit.egg-info/requires.txt +7 -7
  16. hexkit-3.0.1/src/hexkit/providers/mongodb/testutils.py +0 -101
  17. {hexkit-3.0.1 → hexkit-3.1.0}/LICENSE +0 -0
  18. {hexkit-3.0.1 → hexkit-3.1.0}/MANIFEST.in +0 -0
  19. {hexkit-3.0.1 → hexkit-3.1.0}/README.md +0 -0
  20. {hexkit-3.0.1 → hexkit-3.1.0}/setup.cfg +0 -0
  21. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/__init__.py +0 -0
  22. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/__main__.py +0 -0
  23. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/base.py +0 -0
  24. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/config.py +0 -0
  25. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/correlation.py +0 -0
  26. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/log.py +0 -0
  27. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/protocols/__init__.py +0 -0
  28. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/protocols/daopub.py +0 -0
  29. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/protocols/daosub.py +0 -0
  30. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/protocols/eventpub.py +0 -0
  31. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/protocols/eventsub.py +0 -0
  32. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/__init__.py +0 -0
  33. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/akafka/__init__.py +0 -0
  34. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/akafka/config.py +0 -0
  35. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/akafka/provider/__init__.py +0 -0
  36. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/akafka/provider/daosub.py +0 -0
  37. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/akafka/provider/eventpub.py +0 -0
  38. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/akafka/provider/eventsub.py +0 -0
  39. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/akafka/provider/utils.py +0 -0
  40. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
  41. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/mongokafka/__init__.py +0 -0
  42. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/s3/__init__.py +0 -0
  43. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/s3/provider.py +0 -0
  44. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
  45. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
  46. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
  47. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
  48. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
  49. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/testing/__init__.py +0 -0
  50. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/providers/testing/eventpub.py +0 -0
  51. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/py.typed +0 -0
  52. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit/utils.py +0 -0
  53. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
  54. {hexkit-3.0.1 → hexkit-3.1.0}/src/hexkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hexkit
3
- Version: 3.0.1
3
+ Version: 3.1.0
4
4
  Summary: A Toolkit for Building Microservices using the Hexagonal Architecture
5
5
  Author-email: "German Human Genome Phenome Archive (GHGA)" <contact@ghga.de>
6
6
  License: Apache 2.0
@@ -23,21 +23,21 @@ Requires-Dist: pydantic_settings<3,>=2
23
23
  Requires-Dist: PyYAML<7,>=6.0
24
24
  Provides-Extra: akafka
25
25
  Requires-Dist: aiokafka~=0.10.0; extra == "akafka"
26
- Requires-Dist: jsonschema<5,>=4.21; extra == "akafka"
26
+ Requires-Dist: jsonschema<5,>=4.22; extra == "akafka"
27
27
  Provides-Extra: s3
28
- Requires-Dist: boto3<2,>=1.26.50; extra == "s3"
29
- Requires-Dist: botocore<2,>=1.29.50; extra == "s3"
28
+ Requires-Dist: boto3<2,>=1.34.104; extra == "s3"
29
+ Requires-Dist: botocore<2,>=1.34.104; extra == "s3"
30
30
  Provides-Extra: mongodb
31
- Requires-Dist: motor<4,>=3.1.1; extra == "mongodb"
31
+ Requires-Dist: motor<4,>=3.4.0; extra == "mongodb"
32
32
  Provides-Extra: test-akafka
33
33
  Requires-Dist: hexkit[akafka]; extra == "test-akafka"
34
- Requires-Dist: testcontainers[kafka]<5,>=4.3.3; extra == "test-akafka"
34
+ Requires-Dist: testcontainers[kafka]<5,>=4.4.1; extra == "test-akafka"
35
35
  Provides-Extra: test-s3
36
36
  Requires-Dist: hexkit[s3]; extra == "test-s3"
37
- Requires-Dist: testcontainers<5,>=4.3.3; extra == "test-s3"
37
+ Requires-Dist: testcontainers<5,>=4.4.1; extra == "test-s3"
38
38
  Provides-Extra: test-mongodb
39
39
  Requires-Dist: hexkit[mongodb]; extra == "test-mongodb"
40
- Requires-Dist: testcontainers[mongo]<5,>=4.3.3; extra == "test-mongodb"
40
+ Requires-Dist: testcontainers[mongo]<5,>=4.4.1; extra == "test-mongodb"
41
41
  Provides-Extra: test
42
42
  Requires-Dist: hexkit[test-akafka,test-mongodb,test-s3]; extra == "test"
43
43
  Provides-Extra: all
@@ -23,7 +23,7 @@ classifiers = [
23
23
  "Intended Audience :: Developers",
24
24
  ]
25
25
  name = "hexkit"
26
- version = "3.0.1"
26
+ version = "3.1.0"
27
27
  description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
28
28
  dependencies = [
29
29
  "pydantic >=2, <3",
@@ -37,26 +37,26 @@ text = "Apache 2.0"
37
37
  [project.optional-dependencies]
38
38
  akafka = [
39
39
  "aiokafka~=0.10.0",
40
- "jsonschema >=4.21, <5",
40
+ "jsonschema >=4.22, <5",
41
41
  ]
42
42
  s3 = [
43
- "boto3 >=1.26.50, <2",
44
- "botocore >=1.29.50, <2",
43
+ "boto3 >=1.34.104, <2",
44
+ "botocore >=1.34.104, <2",
45
45
  ]
46
46
  mongodb = [
47
- "motor >=3.1.1, <4",
47
+ "motor >=3.4.0, <4",
48
48
  ]
49
49
  test-akafka = [
50
50
  "hexkit[akafka]",
51
- "testcontainers[kafka] >=4.3.3, <5",
51
+ "testcontainers[kafka] >=4.4.1, <5",
52
52
  ]
53
53
  test-s3 = [
54
54
  "hexkit[s3]",
55
- "testcontainers >=4.3.3, <5",
55
+ "testcontainers >=4.4.1, <5",
56
56
  ]
57
57
  test-mongodb = [
58
58
  "hexkit[mongodb]",
59
- "testcontainers[mongo] >=4.3.3, <5",
59
+ "testcontainers[mongo] >=4.4.1, <5",
60
60
  ]
61
61
  test = [
62
62
  "hexkit[test-akafka,test-s3,test-mongodb]",
@@ -22,7 +22,7 @@ from typing import Any, Literal, Union
22
22
  # This is intended to type objects that could be a potential output of `json.loads`.
23
23
  # (Scalar types as well as arrays are excluded from the above assumption.)
24
24
  JsonObject = Mapping[
25
- str, Union[int, float, str, bool, Sequence[Any], Mapping[str, Any]]
25
+ str, Union[int, float, str, bool, Sequence[Any], Mapping[str, Any], None]
26
26
  ]
27
27
 
28
28
 
@@ -132,7 +132,7 @@ class DaoCommons(typing.Protocol[Dto]):
132
132
  """
133
133
  ...
134
134
 
135
- async def delete(self, *, id_: str) -> None:
135
+ async def delete(self, id_: str) -> None:
136
136
  """Delete a resource by providing its ID.
137
137
 
138
138
  Args:
@@ -517,16 +517,16 @@ class ObjectStorageProtocol(ABC):
517
517
  """Thrown when trying to access a bucket with an ID that doesn't exist."""
518
518
 
519
519
  def __init__(self, bucket_id: Optional[str]):
520
- in_bucket = f" in bucket with ID '{bucket_id}'" if bucket_id else ""
521
- message = f"The bucket{in_bucket} does not exist."
520
+ with_id = f" with ID '{bucket_id}'" if bucket_id else ""
521
+ message = f"The bucket{with_id} does not exist."
522
522
  super().__init__(message)
523
523
 
524
524
  class BucketAlreadyExistsError(BucketError):
525
525
  """Thrown when trying to create a bucket with an ID that already exists."""
526
526
 
527
527
  def __init__(self, bucket_id: Optional[str]):
528
- in_bucket = f" in bucket with ID '{bucket_id}'" if bucket_id else ""
529
- message = f"The bucket{in_bucket} already exists."
528
+ with_id = f" with ID '{bucket_id}'" if bucket_id else ""
529
+ message = f"The bucket{with_id} already exists."
530
530
  super().__init__(message)
531
531
 
532
532
  class BucketNotEmptyError(BucketError):
@@ -27,7 +27,7 @@ from testcontainers.core.waiting_utils import wait_for_logs
27
27
 
28
28
  __all__ = ["KafkaSSLContainer"]
29
29
 
30
- DEFAULT_IMAGE = "confluentinc/cp-kafka:7.6.0"
30
+ DEFAULT_IMAGE = "confluentinc/cp-kafka:7.6.1"
31
31
 
32
32
  DEFAULT_PORT = 9093 # default port for the Kafka container
33
33
  BROKER_PORT = 9092 # auxiliary port for inter broker listener
@@ -20,12 +20,19 @@ Please note, only use for testing purposes.
20
20
  """
21
21
 
22
22
  import json
23
- from collections.abc import AsyncGenerator, Sequence
23
+ from collections.abc import AsyncGenerator, Generator, Sequence
24
24
  from contextlib import asynccontextmanager
25
25
  from dataclasses import dataclass
26
26
  from functools import partial
27
- from typing import Callable, Optional, Union
27
+ from typing import Any, Callable, Optional, Union
28
28
 
29
+ try:
30
+ from typing import Self
31
+ except ImportError: # Python < 3.11
32
+ from typing_extensions import Self
33
+
34
+
35
+ import pytest
29
36
  import pytest_asyncio
30
37
  from aiokafka import AIOKafkaConsumer, TopicPartition
31
38
  from aiokafka.admin import AIOKafkaAdminClient
@@ -41,6 +48,26 @@ from hexkit.providers.akafka.provider import (
41
48
  )
42
49
  from hexkit.providers.akafka.testcontainer import DEFAULT_IMAGE as KAFKA_IMAGE
43
50
 
51
+ __all__ = [
52
+ "KAFKA_IMAGE",
53
+ "EventBase",
54
+ "EventRecorder",
55
+ "ExpectedEvent",
56
+ "RecordedEvent",
57
+ "ValidationError",
58
+ "KafkaConfig",
59
+ "KafkaContainerFixture",
60
+ "KafkaEventPublisher",
61
+ "KafkaFixture",
62
+ "get_kafka_container_fixture",
63
+ "get_clean_kafka_fixture",
64
+ "get_persistent_kafka_fixture",
65
+ "kafka_container_fixture",
66
+ "clean_kafka_fixture",
67
+ "persistent_kafka_fixture",
68
+ "kafka_fixture",
69
+ ]
70
+
44
71
 
45
72
  @dataclass(frozen=True)
46
73
  class EventBase:
@@ -339,7 +366,7 @@ class EventRecorder:
339
366
 
340
367
 
341
368
  class KafkaFixture:
342
- """Yielded by the `kafka_fixture` function"""
369
+ """A fixture with utility methods for tests that use Apache Kafka."""
343
370
 
344
371
  def __init__(
345
372
  self,
@@ -468,49 +495,115 @@ class KafkaFixture:
468
495
  )
469
496
 
470
497
 
471
- async def kafka_fixture_function() -> AsyncGenerator[KafkaFixture, None]:
472
- """Pytest fixture for tests depending on the Kafka-base providers.
498
+ class KafkaContainerFixture(KafkaContainer):
499
+ """Kafka test container with configuration and command execution."""
473
500
 
474
- **Do not call directly** Instead, use get_kafka_fixture()
475
- """
476
- with KafkaContainer(image=KAFKA_IMAGE) as kafka:
477
- kafka_servers = [kafka.get_bootstrap_server()]
478
- config = KafkaConfig(
501
+ kafka_config: KafkaConfig
502
+
503
+ def __init__(self, port: int = 9093, **kwargs: Any) -> None:
504
+ """Initialize the container."""
505
+ super().__init__(image=KAFKA_IMAGE, port=port, **kwargs)
506
+
507
+ def __enter__(self) -> Self:
508
+ """Enter the container context."""
509
+ super().__enter__()
510
+ kafka_servers = [self.get_bootstrap_server()]
511
+ self.kafka_config = KafkaConfig(
479
512
  service_name="test_publisher",
480
513
  service_instance_id="001",
481
514
  kafka_servers=kafka_servers,
482
515
  )
516
+ return self
483
517
 
484
- def wrapped_exec_run(command: str, run_in_shell: bool):
485
- """Run the given command in the kafka testcontainer.
518
+ def wrapped_exec_run(self, command: str, run_in_shell: bool):
519
+ """Run the given command in the kafka testcontainer.
486
520
 
487
- Args:
488
- - `command`: The full command to run.
489
- - `run_in_shell`: If True, will run the command in a shell.
521
+ Args:
522
+ - `command`: The full command to run.
523
+ - `run_in_shell`: If True, will run the command in a shell.
490
524
 
491
- Raises:
492
- - `RuntimeError`: when the exit code returned by the command is not zero.
493
- """
494
- cmd = ["sh", "-c", command] if run_in_shell else command
495
- exit_code, output = kafka.get_wrapped_container().exec_run(cmd)
525
+ Raises:
526
+ - `RuntimeError`: when the exit code returned by the command is not zero.
527
+ """
528
+ cmd = ["sh", "-c", command] if run_in_shell else command
529
+ exit_code, output = self.get_wrapped_container().exec_run(cmd)
496
530
 
497
- if exit_code != 0:
498
- raise RuntimeError(
499
- f"result: {exit_code}, output: {output.decode('utf-8')}"
500
- )
531
+ if exit_code != 0:
532
+ raise RuntimeError(f"result: {exit_code}, output: {output.decode('utf-8')}")
501
533
 
502
- async with KafkaEventPublisher.construct(config=config) as publisher:
503
- yield KafkaFixture(
504
- config=config,
505
- kafka_servers=kafka_servers,
506
- publisher=publisher,
507
- cmd_exec_func=wrapped_exec_run,
508
- )
534
+
535
+ def _kafka_container_fixture() -> Generator[KafkaContainerFixture, None, None]:
536
+ """Fixture function for getting a running Kafka test container."""
537
+ with KafkaContainerFixture() as kafka_container:
538
+ yield kafka_container
539
+
540
+
541
+ def get_kafka_container_fixture(
542
+ scope: PytestScope = "session", name: str = "kafka_container"
543
+ ):
544
+ """Get a Kafka test container fixture with desired scope and name.
545
+
546
+ By default, the session scope is used for Kafka test containers.
547
+ """
548
+ return pytest.fixture(_kafka_container_fixture, scope=scope, name=name)
549
+
550
+
551
+ kafka_container_fixture = get_kafka_container_fixture()
509
552
 
510
553
 
511
- def get_kafka_fixture(scope: PytestScope = "function"):
512
- """Produce a kafka fixture with desired scope. Default is the function scope."""
513
- return pytest_asyncio.fixture(kafka_fixture_function, scope=scope)
554
+ async def _persistent_kafka_fixture(
555
+ kafka_container: KafkaContainerFixture,
556
+ ) -> AsyncGenerator[KafkaFixture, None]:
557
+ """Fixture function that gets a persistent Kafka fixture.
558
+
559
+ The state of Kafka is not cleaned up by this function.
560
+ """
561
+ config = kafka_container.kafka_config
562
+ async with KafkaEventPublisher.construct(config=config) as publisher:
563
+ kafka_fixture = KafkaFixture(
564
+ config=config,
565
+ kafka_servers=config.kafka_servers,
566
+ cmd_exec_func=kafka_container.wrapped_exec_run,
567
+ publisher=publisher,
568
+ )
569
+ yield kafka_fixture
570
+
571
+
572
+ def get_persistent_kafka_fixture(scope: PytestScope = "function", name: str = "kafka"):
573
+ """Get a Kafka fixture with desired scope and name.
574
+
575
+ The state of the Kafka test container is persisted across tests.
576
+
577
+ By default, the function scope is used for this fixture,
578
+ while the session scope is used for the underlying Kafka test container.
579
+ """
580
+ return pytest_asyncio.fixture(_persistent_kafka_fixture, scope=scope, name=name)
581
+
582
+
583
+ persistent_kafka_fixture = get_persistent_kafka_fixture()
584
+
585
+
586
+ async def _clean_kafka_fixture(
587
+ kafka_container: KafkaContainerFixture,
588
+ ) -> AsyncGenerator[KafkaFixture, None]:
589
+ """Fixture function that gets a clean Kafka fixture.
590
+
591
+ The clean state is achieved by clearing all Kafka topics upfront.
592
+ """
593
+ async for kafka_fixture in _persistent_kafka_fixture(kafka_container):
594
+ await kafka_fixture.clear_topics()
595
+ yield kafka_fixture
596
+
597
+
598
+ def get_clean_kafka_fixture(scope: PytestScope = "function", name: str = "kafka"):
599
+ """Get a Kafka fixture with desired scope and name.
600
+
601
+ The state of Kafka is reset by clearing all topics before running tests.
602
+
603
+ By default, the function scope is used for this fixture,
604
+ while the session scope is used for the underlying Kafka test container.
605
+ """
606
+ return pytest_asyncio.fixture(_clean_kafka_fixture, scope=scope, name=name)
514
607
 
515
608
 
516
- kafka_fixture = get_kafka_fixture()
609
+ kafka_fixture = clean_kafka_fixture = get_clean_kafka_fixture()
@@ -201,7 +201,7 @@ class MongoDbDaoBase(ABC, Generic[Dto]):
201
201
  # (trusting MongoDB that matching on the _id field can only yield one or
202
202
  # zero matches)
203
203
 
204
- async def delete(self, *, id_: str) -> None:
204
+ async def delete(self, id_: str) -> None:
205
205
  """Delete a resource by providing its ID.
206
206
 
207
207
  Args:
@@ -0,0 +1,176 @@
1
+ # Copyright 2021 - 2024 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
+ # for the German Human Genome-Phenome Archive (GHGA)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ """Utilities for testing code that uses the MongoDbDaoFactory provider.
18
+
19
+ Please note, only use for testing purposes.
20
+ """
21
+
22
+ from collections.abc import Generator
23
+ from dataclasses import dataclass
24
+ from typing import Optional, Union
25
+
26
+ import pytest
27
+ from pydantic import SecretStr
28
+ from pymongo import MongoClient
29
+ from pymongo.errors import ExecutionTimeout, OperationFailure
30
+ from testcontainers.mongodb import MongoDbContainer
31
+
32
+ from hexkit.custom_types import PytestScope
33
+ from hexkit.providers.mongodb.provider import MongoDbConfig, MongoDbDaoFactory
34
+
35
+ MONGODB_IMAGE = "mongo:7.0.9"
36
+
37
+ __all__ = [
38
+ "MONGODB_IMAGE",
39
+ "MongoClient",
40
+ "MongoDbConfig",
41
+ "MongoDbContainerFixture",
42
+ "MongoDbDaoFactory",
43
+ "MongoDbFixture",
44
+ "get_mongodb_container_fixture",
45
+ "get_persistent_mongodb_fixture",
46
+ "get_clean_mongodb_fixture",
47
+ "mongodb_container_fixture",
48
+ "clean_mongodb_fixture",
49
+ "persistent_mongodb_fixture",
50
+ "mongodb_fixture",
51
+ ]
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class MongoDbFixture:
56
+ """A fixture with utility methods for tests that use MongoDB"""
57
+
58
+ client: MongoClient
59
+ config: MongoDbConfig
60
+ dao_factory: MongoDbDaoFactory
61
+
62
+ def empty_collections(
63
+ self,
64
+ exclude_collections: Optional[Union[str, list[str]]] = None,
65
+ ):
66
+ """Drop all mongodb collections in the database.
67
+
68
+ You can also specify collection(s) that should be excluded
69
+ from the operation, i.e. collections that should be kept.
70
+ """
71
+ db_name = self.config.db_name
72
+ if exclude_collections is None:
73
+ exclude_collections = []
74
+ if isinstance(exclude_collections, str):
75
+ exclude_collections = [exclude_collections]
76
+ excluded_collections = set(exclude_collections)
77
+ try:
78
+ collection_names = self.client[db_name].list_collection_names()
79
+ for collection_name in collection_names:
80
+ if collection_name not in excluded_collections:
81
+ self.client[db_name].drop_collection(collection_name)
82
+ except (ExecutionTimeout, OperationFailure) as error:
83
+ raise RuntimeError(
84
+ f"Could not drop collection(s) of Mongo database {db_name}"
85
+ ) from error
86
+
87
+
88
+ class MongoDbContainerFixture(MongoDbContainer):
89
+ """MongoDB test container with MongoDB configuration."""
90
+
91
+ mongodb_config: MongoDbConfig
92
+
93
+
94
+ def _mongodb_container_fixture() -> Generator[MongoDbContainerFixture, None, None]:
95
+ """Fixture function for getting a running MongoDB test container."""
96
+ with MongoDbContainerFixture(image=MONGODB_IMAGE) as mongodb_container:
97
+ db_connection_str = mongodb_container.get_connection_url()
98
+ mongodb_config = MongoDbConfig(
99
+ db_connection_str=SecretStr(db_connection_str), db_name="test"
100
+ )
101
+ mongodb_container.mongodb_config = mongodb_config
102
+ yield mongodb_container
103
+
104
+
105
+ def get_mongodb_container_fixture(
106
+ scope: PytestScope = "session", name: str = "mongodb_container"
107
+ ):
108
+ """Get a MongoDB test container fixture with desired scope and name.
109
+
110
+ By default, the session scope is used for MongoDB test containers.
111
+ """
112
+ return pytest.fixture(_mongodb_container_fixture, scope=scope, name=name)
113
+
114
+
115
+ mongodb_container_fixture = get_mongodb_container_fixture()
116
+
117
+
118
+ def _persistent_mongodb_fixture(
119
+ mongodb_container: MongoDbContainerFixture,
120
+ ) -> Generator[MongoDbFixture, None, None]:
121
+ """Fixture function that gets a persistent MongoDB fixture.
122
+
123
+ The state of the MongoDB is not cleaned up by the function.
124
+ """
125
+ config = mongodb_container.mongodb_config
126
+ dao_factory = MongoDbDaoFactory(config=config)
127
+ client = mongodb_container.get_connection_client()
128
+ yield MongoDbFixture(
129
+ client=client,
130
+ config=config,
131
+ dao_factory=dao_factory,
132
+ )
133
+
134
+ client.close()
135
+
136
+
137
+ def get_persistent_mongodb_fixture(
138
+ scope: PytestScope = "function", name: str = "mongodb"
139
+ ):
140
+ """Get a MongoDB fixture with desired scope and name.
141
+
142
+ The state of the MongoDB test container is persisted across tests.
143
+
144
+ By default, the function scope is used for this fixture,
145
+ while the session scope is used for the underlying MongoDB test container.
146
+ """
147
+ return pytest.fixture(_persistent_mongodb_fixture, scope=scope, name=name)
148
+
149
+
150
+ persistent_mongodb_fixture = get_persistent_mongodb_fixture()
151
+
152
+
153
+ def _clean_mongodb_fixture(
154
+ mongodb_container: MongoDbContainerFixture,
155
+ ) -> Generator[MongoDbFixture, None, None]:
156
+ """Fixture function that gets a clean MongoDB fixture.
157
+
158
+ The clean state is achieved by emptying all MongoDB collections upfront.
159
+ """
160
+ for mongodb_fixture in _persistent_mongodb_fixture(mongodb_container):
161
+ mongodb_fixture.empty_collections()
162
+ yield mongodb_fixture
163
+
164
+
165
+ def get_clean_mongodb_fixture(scope: PytestScope = "function", name: str = "mongodb"):
166
+ """Get a MongoDB fixture with desired scope and name.
167
+
168
+ The state of the MongoDB is reset by emptying all collections before running tests.
169
+
170
+ By default, the function scope is used for this fixture,
171
+ while the session scope is used for the underlying MongoDB test container.
172
+ """
173
+ return pytest.fixture(_clean_mongodb_fixture, scope=scope, name=name)
174
+
175
+
176
+ mongodb_fixture = clean_mongodb_fixture = get_clean_mongodb_fixture()
@@ -135,7 +135,7 @@ def get_delete_publish_func(
135
135
  )
136
136
 
137
137
  document = {"_id": id_, "__metadata__": {"deleted": True, "published": True}}
138
- await collection.replace_one({"_id": document["_id"]}, document, upsert=True)
138
+ await collection.replace_one({"_id": document["_id"]}, document)
139
139
 
140
140
  return publish_deletion
141
141
 
@@ -233,13 +233,17 @@ class MongoKafkaDaoPublisher(Generic[Dto]):
233
233
  ResourceNotFoundError:
234
234
  when resource with the id specified in the dto was not found
235
235
  """
236
- with assert_not_deleted():
237
- await self._dao.update(dto)
236
+ document = self._dao._dto_to_document(dto)
237
+ result = await self._collection.replace_one(
238
+ {"_id": document["_id"], "__metadata__.deleted": False}, document
239
+ )
240
+ if result.matched_count == 0:
241
+ raise ResourceNotFoundError(id_=document["_id"])
238
242
 
239
243
  if self._autopublish:
240
244
  await self._publish_change(dto)
241
245
 
242
- async def delete(self, *, id_: str) -> None:
246
+ async def delete(self, id_: str) -> None:
243
247
  """Delete a resource by providing its ID.
244
248
 
245
249
  Args:
@@ -248,10 +252,15 @@ class MongoKafkaDaoPublisher(Generic[Dto]):
248
252
  Raises:
249
253
  ResourceNotFoundError: when resource with the specified id_ was not found
250
254
  """
251
- document = {"_id": id_, "__metadata__": {"deleted": True, "published": False}}
252
- await self._collection.replace_one(
253
- {"_id": document["_id"]}, document, upsert=True
255
+ document = {
256
+ "_id": id_,
257
+ "__metadata__": {"deleted": True, "published": False},
258
+ }
259
+ result = await self._collection.replace_one(
260
+ {"_id": document["_id"], "__metadata__.deleted": False}, document
254
261
  )
262
+ if result.matched_count == 0:
263
+ raise ResourceNotFoundError(id_=id_)
255
264
 
256
265
  if self._autopublish:
257
266
  await self._publish_delete(id_)
@@ -0,0 +1,73 @@
1
+ # Copyright 2021 - 2024 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
+ # for the German Human Genome-Phenome Archive (GHGA)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ """Utilities for testing code that uses the MongoKafkaDaoPublisherFactory provider.
18
+
19
+ Please note, only use for testing purposes.
20
+ """
21
+
22
+ from typing import NamedTuple
23
+
24
+ import pytest
25
+
26
+ from hexkit.providers.akafka.testutils import KafkaContainerFixture, KafkaFixture
27
+ from hexkit.providers.mongodb.testutils import MongoDbContainerFixture, MongoDbFixture
28
+ from hexkit.providers.mongokafka import MongoKafkaConfig
29
+
30
+ __all__ = [
31
+ "MongoKafkaConfig",
32
+ "MongoKafkaFixture",
33
+ "mongo_kafka_config_fixture",
34
+ "mongo_kafka_fixture",
35
+ ]
36
+
37
+
38
+ @pytest.fixture(name="mongo_kafka_config", scope="session")
39
+ def mongo_kafka_config_fixture(
40
+ mongodb_container: MongoDbContainerFixture, kafka_container: KafkaContainerFixture
41
+ ) -> MongoKafkaConfig:
42
+ """Fixture to get the combined Mongo and Kafka configuration."""
43
+ return MongoKafkaConfig(
44
+ **mongodb_container.mongodb_config.model_dump(),
45
+ **kafka_container.kafka_config.model_dump(),
46
+ )
47
+
48
+
49
+ class MongoKafkaFixture(NamedTuple):
50
+ """Combined MongoDB and Kafka fixture"""
51
+
52
+ mongodb: MongoDbFixture
53
+ kafka: KafkaFixture
54
+ config: MongoKafkaConfig
55
+
56
+
57
+ @pytest.fixture(name="mongo_kafka", scope="function")
58
+ def mongo_kafka_fixture(
59
+ mongodb: MongoDbFixture, kafka: KafkaFixture
60
+ ) -> MongoKafkaFixture:
61
+ """Get a MongoKafka fixture with desired scope and name.
62
+
63
+ The state of the MongoDb and Kafka is reset by emptying all MongoDB collections
64
+ and clearing all Kafka topics before running tests.
65
+
66
+ By default, the function scope is used for this fixture,
67
+ while the session scope is used for the underlying test containers.
68
+ """
69
+ config = MongoKafkaConfig(
70
+ **mongodb.config.model_dump(),
71
+ **kafka.config.model_dump(),
72
+ )
73
+ return MongoKafkaFixture(mongodb, kafka, config)
@@ -23,13 +23,14 @@ Please note, only use for testing purposes.
23
23
 
24
24
  import hashlib
25
25
  import os
26
- from collections.abc import Generator
26
+ from collections.abc import AsyncGenerator, Generator
27
27
  from contextlib import contextmanager
28
28
  from pathlib import Path
29
29
  from tempfile import NamedTemporaryFile
30
30
  from typing import Optional
31
31
 
32
32
  import pytest
33
+ import pytest_asyncio
33
34
  import requests
34
35
  from pydantic import BaseModel, SecretStr, computed_field
35
36
  from testcontainers.localstack import LocalStackContainer
@@ -38,7 +39,26 @@ from hexkit.custom_types import PytestScope
38
39
  from hexkit.protocols.objstorage import ObjectStorageProtocol, PresignedPostURL
39
40
  from hexkit.providers.s3.provider import S3Config, S3ObjectStorage
40
41
 
41
- LOCALSTACK_IMAGE = "localstack/localstack:0.14.5"
42
+ __all__ = [
43
+ "LOCALSTACK_IMAGE",
44
+ "MEBIBYTE",
45
+ "TEST_FILE_DIR",
46
+ "TEST_FILE_PATHS",
47
+ "calc_md5",
48
+ "FileObject",
49
+ "S3Fixture",
50
+ "S3ContainerFixture",
51
+ "get_s3_container_fixture",
52
+ "get_clean_s3_fixture",
53
+ "get_persistent_s3_fixture",
54
+ "clean_s3_fixture",
55
+ "persistent_s3_fixture",
56
+ "s3_fixture",
57
+ "file_fixture",
58
+ ]
59
+
60
+
61
+ LOCALSTACK_IMAGE = "localstack/localstack:3.4.0"
42
62
 
43
63
  TEST_FILE_DIR = Path(__file__).parent.resolve() / "test_files"
44
64
 
@@ -81,7 +101,7 @@ class FileObject(BaseModel):
81
101
 
82
102
 
83
103
  class S3Fixture:
84
- """Yielded by the `s3_fixture` function"""
104
+ """A fixture with utility methods for tests that use S3 file storage"""
85
105
 
86
106
  def __init__(self, config: S3Config, storage: S3ObjectStorage):
87
107
  """Initialize with config."""
@@ -120,24 +140,100 @@ class S3Fixture:
120
140
  buckets.discard(bucket)
121
141
 
122
142
 
123
- def s3_fixture_function() -> Generator[S3Fixture, None, None]:
124
- """Pytest fixture for tests depending on the S3ObjectStorage DAO.
143
+ class S3ContainerFixture(LocalStackContainer):
144
+ """LocalStack test container with S3 configuration."""
145
+
146
+ s3_config: S3Config
147
+
148
+ _created_buckets: set[str]
149
+
150
+
151
+ def _s3_container_fixture() -> Generator[S3ContainerFixture, None, None]:
152
+ """Fixture function for getting a running S3 test container."""
153
+ with S3ContainerFixture(image=LOCALSTACK_IMAGE) as s3_container:
154
+ s3_endpoint_url = s3_container.get_url()
155
+ s3_config = S3Config( # type: ignore [call-arg]
156
+ s3_endpoint_url=s3_endpoint_url,
157
+ s3_access_key_id="test",
158
+ s3_secret_access_key=SecretStr("test"),
159
+ )
160
+ s3_container.s3_config = s3_config
161
+ s3_container._created_buckets = set()
162
+ yield s3_container
163
+
164
+
165
+ def get_s3_container_fixture(
166
+ scope: PytestScope = "session", name: str = "s3_container"
167
+ ):
168
+ """Get a LocalStack test container fixture with desired scope and name.
169
+
170
+ By default, the session scope is used for LocalStack test containers.
171
+ """
172
+ return pytest.fixture(_s3_container_fixture, scope=scope, name=name)
173
+
174
+
175
+ s3_container_fixture = get_s3_container_fixture()
176
+
125
177
 
126
- **Do not call directly** Instead, use get_s3_fixture()
178
+ def _persistent_s3_fixture(
179
+ s3_container: S3ContainerFixture,
180
+ ) -> Generator[S3Fixture, None, None]:
181
+ """Fixture function that gets a persistent S3 storage fixture.
182
+
183
+ The state of the S3 storage is not cleaned up by the function.
184
+ """
185
+ config = s3_container.s3_config
186
+ storage = S3ObjectStorage(config=config)
187
+ yield S3Fixture(config=config, storage=storage)
188
+
189
+
190
+ def get_persistent_s3_fixture(scope: PytestScope = "function", name: str = "s3"):
191
+ """Get an S3 fixture with desired scope and name.
192
+
193
+ The state of the LocalStack test container is persisted across tests.
194
+
195
+ By default, the function scope is used for this fixture,
196
+ while the session scope is used for the underlying LocalStack test container.
197
+ """
198
+ return pytest.fixture(_persistent_s3_fixture, scope=scope, name=name)
199
+
200
+
201
+ persistent_s3_fixture = get_persistent_s3_fixture()
202
+
203
+
204
+ async def _clean_s3_fixture(
205
+ s3_container: S3ContainerFixture,
206
+ ) -> AsyncGenerator[S3Fixture, None]:
207
+ """Async fixture function that gets a clean S3 storage fixture.
208
+
209
+ The clean state is achieved by deleting all S3 buckets upfront.
127
210
  """
128
- with LocalStackContainer(image=LOCALSTACK_IMAGE).with_services("s3") as localstack:
129
- config = config_from_localstack_container(localstack)
211
+ for s3_fixture in _persistent_s3_fixture(s3_container):
212
+ container_buckets = s3_container._created_buckets
213
+ fixture_buckets = s3_fixture.storage._created_buckets
214
+ fixture_buckets.update(container_buckets)
215
+ fixture_buckets_before = fixture_buckets.copy()
216
+ await s3_fixture.storage.delete_created_buckets()
217
+ deleted_buckets = fixture_buckets_before - fixture_buckets
218
+ container_buckets.difference_update(deleted_buckets)
130
219
 
131
- storage = S3ObjectStorage(config=config)
132
- yield S3Fixture(config=config, storage=storage)
220
+ yield s3_fixture
133
221
 
222
+ container_buckets.update(fixture_buckets)
134
223
 
135
- def get_s3_fixture(scope: PytestScope = "function"):
136
- """Produce an S3 fixture with desired scope. Default is the function scope."""
137
- return pytest.fixture(s3_fixture_function, scope=scope)
224
+
225
+ def get_clean_s3_fixture(scope: PytestScope = "function", name: str = "s3"):
226
+ """Get an S3 storage fixture with desired scope and name.
227
+
228
+ The state of the S3 storage is reset by deleting all buckets before running tests.
229
+
230
+ By default, the function scope is used for this fixture,
231
+ while the session scope is used for the underlying LocalStack test container.
232
+ """
233
+ return pytest_asyncio.fixture(_clean_s3_fixture, scope=scope, name=name)
138
234
 
139
235
 
140
- s3_fixture = get_s3_fixture()
236
+ s3_fixture = clean_s3_fixture = get_clean_s3_fixture()
141
237
 
142
238
 
143
239
  @contextmanager
@@ -146,7 +242,7 @@ def temp_file_object(
146
242
  object_id: str = "default-test-object",
147
243
  size: int = 5 * MEBIBYTE,
148
244
  ) -> Generator[FileObject, None, None]:
149
- """Generates a file object with the specified size in bytes."""
245
+ """Generate a file object with the specified size in bytes."""
150
246
  chunk_size = 1024
151
247
  chunk = b"\0" * chunk_size
152
248
  current_size = 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hexkit
3
- Version: 3.0.1
3
+ Version: 3.1.0
4
4
  Summary: A Toolkit for Building Microservices using the Hexagonal Architecture
5
5
  Author-email: "German Human Genome Phenome Archive (GHGA)" <contact@ghga.de>
6
6
  License: Apache 2.0
@@ -23,21 +23,21 @@ Requires-Dist: pydantic_settings<3,>=2
23
23
  Requires-Dist: PyYAML<7,>=6.0
24
24
  Provides-Extra: akafka
25
25
  Requires-Dist: aiokafka~=0.10.0; extra == "akafka"
26
- Requires-Dist: jsonschema<5,>=4.21; extra == "akafka"
26
+ Requires-Dist: jsonschema<5,>=4.22; extra == "akafka"
27
27
  Provides-Extra: s3
28
- Requires-Dist: boto3<2,>=1.26.50; extra == "s3"
29
- Requires-Dist: botocore<2,>=1.29.50; extra == "s3"
28
+ Requires-Dist: boto3<2,>=1.34.104; extra == "s3"
29
+ Requires-Dist: botocore<2,>=1.34.104; extra == "s3"
30
30
  Provides-Extra: mongodb
31
- Requires-Dist: motor<4,>=3.1.1; extra == "mongodb"
31
+ Requires-Dist: motor<4,>=3.4.0; extra == "mongodb"
32
32
  Provides-Extra: test-akafka
33
33
  Requires-Dist: hexkit[akafka]; extra == "test-akafka"
34
- Requires-Dist: testcontainers[kafka]<5,>=4.3.3; extra == "test-akafka"
34
+ Requires-Dist: testcontainers[kafka]<5,>=4.4.1; extra == "test-akafka"
35
35
  Provides-Extra: test-s3
36
36
  Requires-Dist: hexkit[s3]; extra == "test-s3"
37
- Requires-Dist: testcontainers<5,>=4.3.3; extra == "test-s3"
37
+ Requires-Dist: testcontainers<5,>=4.4.1; extra == "test-s3"
38
38
  Provides-Extra: test-mongodb
39
39
  Requires-Dist: hexkit[mongodb]; extra == "test-mongodb"
40
- Requires-Dist: testcontainers[mongo]<5,>=4.3.3; extra == "test-mongodb"
40
+ Requires-Dist: testcontainers[mongo]<5,>=4.4.1; extra == "test-mongodb"
41
41
  Provides-Extra: test
42
42
  Requires-Dist: hexkit[test-akafka,test-mongodb,test-s3]; extra == "test"
43
43
  Provides-Extra: all
@@ -38,6 +38,7 @@ src/hexkit/providers/mongodb/provider.py
38
38
  src/hexkit/providers/mongodb/testutils.py
39
39
  src/hexkit/providers/mongokafka/__init__.py
40
40
  src/hexkit/providers/mongokafka/provider.py
41
+ src/hexkit/providers/mongokafka/testutils.py
41
42
  src/hexkit/providers/s3/__init__.py
42
43
  src/hexkit/providers/s3/provider.py
43
44
  src/hexkit/providers/s3/testutils.py
@@ -4,29 +4,29 @@ PyYAML<7,>=6.0
4
4
 
5
5
  [akafka]
6
6
  aiokafka~=0.10.0
7
- jsonschema<5,>=4.21
7
+ jsonschema<5,>=4.22
8
8
 
9
9
  [all]
10
10
  hexkit[test]
11
11
 
12
12
  [mongodb]
13
- motor<4,>=3.1.1
13
+ motor<4,>=3.4.0
14
14
 
15
15
  [s3]
16
- boto3<2,>=1.26.50
17
- botocore<2,>=1.29.50
16
+ boto3<2,>=1.34.104
17
+ botocore<2,>=1.34.104
18
18
 
19
19
  [test]
20
20
  hexkit[test-akafka,test-mongodb,test-s3]
21
21
 
22
22
  [test-akafka]
23
23
  hexkit[akafka]
24
- testcontainers[kafka]<5,>=4.3.3
24
+ testcontainers[kafka]<5,>=4.4.1
25
25
 
26
26
  [test-mongodb]
27
27
  hexkit[mongodb]
28
- testcontainers[mongo]<5,>=4.3.3
28
+ testcontainers[mongo]<5,>=4.4.1
29
29
 
30
30
  [test-s3]
31
31
  hexkit[s3]
32
- testcontainers<5,>=4.3.3
32
+ testcontainers<5,>=4.4.1
@@ -1,101 +0,0 @@
1
- # Copyright 2021 - 2024 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
- # for the German Human Genome-Phenome Archive (GHGA)
3
- #
4
- # Licensed under the Apache License, Version 2.0 (the "License");
5
- # you may not use this file except in compliance with the License.
6
- # You may obtain a copy of the License at
7
- #
8
- # http://www.apache.org/licenses/LICENSE-2.0
9
- #
10
- # Unless required by applicable law or agreed to in writing, software
11
- # distributed under the License is distributed on an "AS IS" BASIS,
12
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- # See the License for the specific language governing permissions and
14
- # limitations under the License.
15
- #
16
-
17
- """Utilities for testing code that uses the MongoDbDaoFactory provider.
18
-
19
- Please note, only use for testing purposes.
20
- """
21
-
22
- from collections.abc import Generator
23
- from dataclasses import dataclass
24
- from typing import Optional, Union
25
-
26
- import pytest
27
- from pymongo import MongoClient
28
- from pymongo.errors import ExecutionTimeout, OperationFailure
29
- from testcontainers.mongodb import MongoDbContainer
30
-
31
- from hexkit.custom_types import PytestScope
32
- from hexkit.providers.mongodb.provider import MongoDbConfig, MongoDbDaoFactory
33
-
34
- MONGODB_IMAGE = "mongo:6.0.3"
35
-
36
-
37
- @dataclass(frozen=True)
38
- class MongoDbFixture:
39
- """Yielded by the `mongodb_fixture` function"""
40
-
41
- client: MongoClient
42
- config: MongoDbConfig
43
- dao_factory: MongoDbDaoFactory
44
-
45
- def empty_collections(
46
- self,
47
- exclude_collections: Optional[Union[str, list[str]]] = None,
48
- ):
49
- """Drop all mongodb collections in the database.
50
-
51
- You can also specify collection(s) that should be excluded
52
- from the operation, i.e. collections that should be kept.
53
- """
54
- db_name = self.config.db_name
55
- if exclude_collections is None:
56
- exclude_collections = []
57
- if isinstance(exclude_collections, str):
58
- exclude_collections = [exclude_collections]
59
- excluded_collections = set(exclude_collections)
60
- try:
61
- collection_names = self.client[db_name].list_collection_names()
62
- for collection_name in collection_names:
63
- if collection_name not in excluded_collections:
64
- self.client[db_name].drop_collection(collection_name)
65
- except (ExecutionTimeout, OperationFailure) as error:
66
- raise RuntimeError(
67
- f"Could not drop collection(s) of Mongo database {db_name}"
68
- ) from error
69
-
70
-
71
- def config_from_mongodb_container(container: MongoDbContainer) -> MongoDbConfig:
72
- """Prepares a MongoDbConfig from an instance of a MongoDbContainer container."""
73
- db_connection_str = container.get_connection_url()
74
- return MongoDbConfig(db_connection_str=db_connection_str, db_name="test")
75
-
76
-
77
- def mongodb_fixture_function() -> Generator[MongoDbFixture, None, None]:
78
- """Pytest fixture for tests depending on the MongoDbDaoFactory DAO.
79
-
80
- **Do not call directly** Instead, use get_mongodb_fixture()
81
- """
82
- with MongoDbContainer(image=MONGODB_IMAGE) as mongodb:
83
- config = config_from_mongodb_container(mongodb)
84
- dao_factory = MongoDbDaoFactory(config=config)
85
- client = mongodb.get_connection_client()
86
-
87
- yield MongoDbFixture(
88
- client=client,
89
- config=config,
90
- dao_factory=dao_factory,
91
- )
92
-
93
- client.close()
94
-
95
-
96
- def get_mongodb_fixture(scope: PytestScope = "function"):
97
- """Produce a MongoDb fixture with desired scope. Default is the function scope."""
98
- return pytest.fixture(mongodb_fixture_function, scope=scope)
99
-
100
-
101
- mongodb_fixture = get_mongodb_fixture()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes