From 9692478a813085747d515134affc717225fb33d0 Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:25:22 +0200 Subject: [PATCH 1/4] :boom: :recycle: remove version slug from V1 and V2 urls --- .pre-commit-config.yaml | 2 +- mindee/v1/mindee_http/base_settings.py | 2 +- mindee/v1/mindee_http/endpoint.py | 4 ++-- mindee/v1/mindee_http/mindee_api.py | 2 +- mindee/v1/mindee_http/workflow_settings.py | 2 +- mindee/v2/mindee_http/mindee_api_v2.py | 8 ++++---- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f764c96e..df6e23dd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -55,4 +55,4 @@ repos: entry: make -C docs linkcheck language: system pass_filenames: false - files: ^docs/.*$|^mindee/.*\.py$ \ No newline at end of file + files: ^docs/.*$|^mindee/.*\.py$ diff --git a/mindee/v1/mindee_http/base_settings.py b/mindee/v1/mindee_http/base_settings.py index 84470b46..539d73fc 100644 --- a/mindee/v1/mindee_http/base_settings.py +++ b/mindee/v1/mindee_http/base_settings.py @@ -9,7 +9,7 @@ API_KEY_DEFAULT = "" BASE_URL_ENV_NAME = "MINDEE_BASE_URL" -BASE_URL_DEFAULT = "https://api.mindee.net/v1" +BASE_URL_DEFAULT = "https://api.mindee.net" REQUEST_TIMEOUT_ENV_NAME = "MINDEE_REQUEST_TIMEOUT" TIMEOUT_DEFAULT = 120 diff --git a/mindee/v1/mindee_http/endpoint.py b/mindee/v1/mindee_http/endpoint.py index 4b29737d..8348fd94 100644 --- a/mindee/v1/mindee_http/endpoint.py +++ b/mindee/v1/mindee_http/endpoint.py @@ -109,7 +109,7 @@ def _custom_request( params["rag"] = "true" if workflow_id: - url = f"{self.settings.base_url}/workflows/{workflow_id}/{route}" + url = f"{self.settings.base_url}/v1/workflows/{workflow_id}/{route}" else: url = f"{self.settings.url_root}/{route}" @@ -165,7 +165,7 @@ def document_feedback_req_put( :param feedback: Feedback object to send. """ return requests.put( - f"{self.settings.base_url}/documents/{document_id}/feedback", + f"{self.settings.base_url}/v1/documents/{document_id}/feedback", headers=self.settings.base_headers, data=json.dumps(feedback, indent=0), timeout=self.settings.request_timeout, diff --git a/mindee/v1/mindee_http/mindee_api.py b/mindee/v1/mindee_http/mindee_api.py index e7408de7..de15f9aa 100644 --- a/mindee/v1/mindee_http/mindee_api.py +++ b/mindee/v1/mindee_http/mindee_api.py @@ -26,4 +26,4 @@ def __init__( self.endpoint_name = endpoint_name self.account_name = account_name self.version = version - self.url_root = f"{self.base_url}/products/{self.account_name}/{self.endpoint_name}/v{self.version}" + self.url_root = f"{self.base_url}/v1/products/{self.account_name}/{self.endpoint_name}/v{self.version}" diff --git a/mindee/v1/mindee_http/workflow_settings.py b/mindee/v1/mindee_http/workflow_settings.py index 625c074d..894915b7 100644 --- a/mindee/v1/mindee_http/workflow_settings.py +++ b/mindee/v1/mindee_http/workflow_settings.py @@ -21,4 +21,4 @@ def __init__( "You can set this using the " f"'{API_KEY_ENV_NAME}' environment variable." ) - self.url_root = f"{self.base_url}/workflows/{workflow_id}/executions" + self.url_root = f"{self.base_url}/v1/workflows/{workflow_id}/executions" diff --git a/mindee/v2/mindee_http/mindee_api_v2.py b/mindee/v2/mindee_http/mindee_api_v2.py index f0805249..c1ab501a 100644 --- a/mindee/v2/mindee_http/mindee_api_v2.py +++ b/mindee/v2/mindee_http/mindee_api_v2.py @@ -14,7 +14,7 @@ API_KEY_V2_DEFAULT = "" BASE_URL_ENV_NAME = "MINDEE_V2_BASE_URL" -BASE_URL_DEFAULT = "https://api-v2.mindee.net/v2" +BASE_URL_DEFAULT = "https://api-v2.mindee.net" REQUEST_TIMEOUT_ENV_NAME = "MINDEE_REQUEST_TIMEOUT" TIMEOUT_DEFAULT = 120 @@ -84,7 +84,7 @@ def req_post_inference_enqueue( :return: requests response. """ data = params.get_form_data() - url = f"{self.url_root}/{slug}/enqueue" + url = f"{self.url_root}/v2/{slug}/enqueue" if isinstance(input_source, LocalInputSource): files = {"file": input_source.read_contents(params.close_file)} @@ -114,7 +114,7 @@ def req_get_job(self, job_id: str) -> requests.Response: :param job_id: Job ID, returned by the enqueue request. """ return requests.get( - f"{self.url_root}/jobs/{job_id}", + f"{self.url_root}/v2/jobs/{job_id}", headers=self.base_headers, timeout=self.request_timeout, allow_redirects=False, @@ -128,7 +128,7 @@ def req_get_inference(self, inference_id: str, slug: str) -> requests.Response: :param slug: Slug of the inference, defaults to nothing. """ - url = f"{self.url_root}/{slug}/{inference_id}" + url = f"{self.url_root}/v2/{slug}/{inference_id}" return requests.get( url, headers=self.base_headers, From 1ce5b2bb1cf474738150039d57087fe0ca09a272 Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:17:50 +0200 Subject: [PATCH 2/4] :boom: :recycle: remove deprecated methods and classes --- mindee/geometry/__init__.py | 6 ---- mindee/geometry/polygon.py | 37 ------------------------ mindee/image/extracted_image.py | 6 ---- mindee/input/local_input_source.py | 10 +++---- mindee/pdf/extracted_pdf.py | 4 --- mindee/pdf/pdf_utils.py | 11 +++++-- mindee/v1/parsing/common/ocr/ocr_page.py | 10 +++---- mindee/v1/parsing/standard/date.py | 10 ++----- mindee/v2/client.py | 7 ----- mindee/v2/product/crop/crop_item.py | 2 +- tests/conftest.py | 9 ++++++ tests/test_geometry.py | 24 +++++++-------- 12 files changed, 43 insertions(+), 93 deletions(-) create mode 100644 tests/conftest.py diff --git a/mindee/geometry/__init__.py b/mindee/geometry/__init__.py index 1c002f2a..ed4c8ae4 100644 --- a/mindee/geometry/__init__.py +++ b/mindee/geometry/__init__.py @@ -3,10 +3,7 @@ from mindee.geometry.point import Point, Points from mindee.geometry.polygon import ( Polygon, - is_point_in_polygon_x, - is_point_in_polygon_y, merge_polygons, - polygon_from_prediction, ) from mindee.geometry.polygon_utils import get_centroid, is_point_in_x, is_point_in_y from mindee.geometry.quadrilateral import ( @@ -27,11 +24,8 @@ "get_centroid", "get_min_max_x", "get_min_max_y", - "is_point_in_polygon_x", - "is_point_in_polygon_y", "is_point_in_x", "is_point_in_y", "merge_polygons", - "polygon_from_prediction", "quadrilateral_from_prediction", ] diff --git a/mindee/geometry/polygon.py b/mindee/geometry/polygon.py index 93d1a8ca..88d093d0 100644 --- a/mindee/geometry/polygon.py +++ b/mindee/geometry/polygon.py @@ -55,43 +55,6 @@ def __str__(self): return "(" + ", ".join(str(p) for p in self) + ")" -def is_point_in_polygon_x(point: Point, polygon: Polygon) -> bool: - """ - Deprecated, use ``is_point_in_x`` from ``Polygon`` class instead. - - Determine if the Point is in the Polygon's X-axis. - - :param point: Point to compare - :param polygon: Polygon to look into - """ - min_x, max_x = get_min_max_x(polygon) - return is_point_in_x(point, min_x, max_x) - - -def is_point_in_polygon_y(point: Point, polygon: Polygon) -> bool: - """ - Deprecated, use ``is_point_in_y`` from ``Polygon`` class instead. - - Determine if the Point is in the Polygon's Y-axis. - - :param point: Point to compare - :param polygon: Polygon to look into - """ - min_y, max_y = get_min_max_y(polygon) - return is_point_in_y(point, min_y, max_y) - - -def polygon_from_prediction(prediction: Sequence[list[float]]) -> Polygon: - """ - Deprecated, init ``Polygon`` class directly instead. - - Transform a prediction into a Polygon. - - :param prediction: API prediction. - """ - return Polygon([Point(point[0], point[1]) for point in prediction]) - - def merge_polygons(vertices: Sequence[Polygon]) -> Polygon: """ Given a sequence of polygons, calculate a polygon box that encompasses all polygons. diff --git a/mindee/image/extracted_image.py b/mindee/image/extracted_image.py index 0029c30c..b315695e 100644 --- a/mindee/image/extracted_image.py +++ b/mindee/image/extracted_image.py @@ -70,12 +70,6 @@ def save_to_file(self, output_path: Path | str, file_format: str | None = None): print(exc) raise MindeeError(f"Could not save file {Path(output_path).name}.") from exc - def as_source(self) -> FileInput: - """ - Deprecated. Use ``as_input_source`` instead. - """ - return self.as_input_source() - def as_input_source(self) -> FileInput: """ Return the file as a Mindee-compatible BufferInput source. diff --git a/mindee/input/local_input_source.py b/mindee/input/local_input_source.py index 15033925..c4375b6a 100644 --- a/mindee/input/local_input_source.py +++ b/mindee/input/local_input_source.py @@ -117,12 +117,12 @@ def page_count(self) -> int: self._page_count = len(pdf) else: self._page_count = 1 + if self._page_count is None: + raise MindeeSourceError( + f"Failed to determine page count for {self.filename}" + ) return self._page_count - def count_doc_pages(self) -> int: - """Deprecated. Use ``page_count`` instead.""" - return self.page_count - def apply_page_options(self, page_options: PageOptions) -> None: """Apply cut and merge options on multipage documents.""" if not self.is_pdf(): @@ -137,7 +137,7 @@ def process_pdf( self, behavior: str, on_min_pages: int, - page_indexes: Sequence, + page_indexes: Sequence[int], ) -> None: """Run any required processing on a PDF file.""" if self.is_pdf_empty(): diff --git a/mindee/pdf/extracted_pdf.py b/mindee/pdf/extracted_pdf.py index 0d6c0118..4bbfbb5a 100644 --- a/mindee/pdf/extracted_pdf.py +++ b/mindee/pdf/extracted_pdf.py @@ -27,10 +27,6 @@ def get_page_count(self) -> int: "Could not retrieve page count from Extracted PDF object." ) from exc - def write_to_file(self, output_path: str): - """Deprecated. Use ``save_to_file`` instead.""" - self.save_to_file(output_path) - def save_to_file(self, output_path: Path | str): """ Writes the contents of the current PDF object to a file. diff --git a/mindee/pdf/pdf_utils.py b/mindee/pdf/pdf_utils.py index f7b96b22..129ead22 100644 --- a/mindee/pdf/pdf_utils.py +++ b/mindee/pdf/pdf_utils.py @@ -15,10 +15,17 @@ def has_source_text(pdf_bytes: bytes) -> bool: Checks if the provided PDF bytes contain source text. :param pdf_bytes: Raw bytes representation of a PDF file - :return: + :return: True if source text is found, False otherwise. """ pdf = pdfium.PdfDocument(pdf_bytes) - return any(len(page.get_textpage().get_text_bounded().strip()) > 0 for page in pdf) + + try: + return any( + len(page.get_textpage().get_text_bounded().strip()) > 0 for page in pdf + ) + finally: + if hasattr(pdf, "close"): + pdf.close() def extract_text_from_pdf(pdf_bytes: bytes) -> list[list[PDFCharData]]: diff --git a/mindee/v1/parsing/common/ocr/ocr_page.py b/mindee/v1/parsing/common/ocr/ocr_page.py index 37da47ee..6a041c49 100644 --- a/mindee/v1/parsing/common/ocr/ocr_page.py +++ b/mindee/v1/parsing/common/ocr/ocr_page.py @@ -1,5 +1,4 @@ from mindee.geometry.minmax import get_min_max_y -from mindee.geometry.polygon import is_point_in_polygon_y from mindee.geometry.polygon_utils import get_centroid from mindee.parsing.common.string_dict import StringDict from mindee.v1.parsing.common.ocr.ocr_line import OCRLine @@ -25,12 +24,11 @@ def __init__(self, raw_prediction: StringDict) -> None: @staticmethod def _are_words_on_same_line(current_word: OCRWord, next_word: OCRWord) -> bool: """Determine if two words are on the same line.""" - current_in_next = is_point_in_polygon_y( - get_centroid(current_word.polygon), - next_word.polygon, + current_in_next = current_word.polygon.is_point_in_y( + get_centroid(next_word.polygon), ) - next_in_current = is_point_in_polygon_y( - get_centroid(next_word.polygon), current_word.polygon + next_in_current = current_word.polygon.is_point_in_y( + get_centroid(next_word.polygon) ) # We need to check both to eliminate any issues due to word order. return current_in_next or next_in_current diff --git a/mindee/v1/parsing/standard/date.py b/mindee/v1/parsing/standard/date.py index 5eb26bb9..b3c9ea3b 100644 --- a/mindee/v1/parsing/standard/date.py +++ b/mindee/v1/parsing/standard/date.py @@ -1,7 +1,5 @@ from datetime import date, datetime -import pytz - from mindee.parsing.common import StringDict from mindee.v1.parsing.standard.base import BaseField, FieldPositionMixin @@ -44,11 +42,9 @@ def __init__( self.is_computed = raw_prediction["is_computed"] if self.value: try: - self.date_object = ( - datetime.strptime(self.value, ISO8601_DATE_FORMAT) - .replace(tzinfo=pytz.utc) - .date() - ) + self.date_object = datetime.strptime( + self.value, ISO8601_DATE_FORMAT + ).date() except (TypeError, ValueError): self.date_object = None self.confidence = 0.0 diff --git a/mindee/v2/client.py b/mindee/v2/client.py index dc22723c..b98b9aba 100644 --- a/mindee/v2/client.py +++ b/mindee/v2/client.py @@ -96,13 +96,6 @@ def get_job(self, job_id: str) -> JobResponse: handle_error_v2(dict_response) return JobResponse(dict_response) - def get_inference( - self, - inference_id: str, - ) -> BaseResponse: - """[Deprecated] Use `get_result` instead.""" - return self.get_result(ExtractionResponse, inference_id) - def get_result( self, response_type: type[TypeBaseResponse], diff --git a/mindee/v2/product/crop/crop_item.py b/mindee/v2/product/crop/crop_item.py index ac8724fb..5ee0d6ea 100644 --- a/mindee/v2/product/crop/crop_item.py +++ b/mindee/v2/product/crop/crop_item.py @@ -7,7 +7,7 @@ class CropItem: - """Deprecated class. Use CropItem instead.""" + """Result of a cropped document region.""" location: FieldLocation """Location which includes cropping coordinates for the detected object, within the source document.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..c130f51b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +import gc + +import pytest + + +@pytest.fixture(autouse=True) +def force_gc(): + yield + gc.collect() diff --git a/tests/test_geometry.py b/tests/test_geometry.py index a513ca87..501999bb 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -64,18 +64,18 @@ def test_is_point_in_polygon_y(rectangle_a, rectangle_b, quadrangle_a): # Should only be in polygon C point_b = geometry.Point(0.300, 0.420) - assert geometry.is_point_in_polygon_y(point_a, rectangle_a) + assert rectangle_a.is_point_in_y(point_a) assert rectangle_a_polygon.is_point_in_y(point_a) - assert geometry.is_point_in_polygon_y(point_a, rectangle_b) + assert rectangle_b.is_point_in_y(point_a) assert rectangle_b_polygon.is_point_in_y(point_a) - assert geometry.is_point_in_polygon_y(point_a, quadrangle_a) is False + assert quadrangle_a.is_point_in_y(point_a) is False assert quadrangle_a_polygon.is_point_in_y(point_a) is False - assert geometry.is_point_in_polygon_y(point_b, rectangle_a) is False + assert rectangle_a.is_point_in_y(point_b) is False assert rectangle_a_polygon.is_point_in_y(point_b) is False - assert geometry.is_point_in_polygon_y(point_b, rectangle_b) is False + assert rectangle_b.is_point_in_y(point_b) is False assert rectangle_b_polygon.is_point_in_y(point_b) is False - assert geometry.is_point_in_polygon_y(point_b, quadrangle_a) + assert quadrangle_a.is_point_in_y(point_b) assert quadrangle_a_polygon.is_point_in_y(point_b) @@ -89,18 +89,18 @@ def test_is_point_in_polygon_x(rectangle_a, rectangle_b, quadrangle_a): # Should only be in polygon C point_b = geometry.Point(0.300, 0.420) - assert geometry.is_point_in_polygon_x(point_a, rectangle_a) + assert rectangle_a.is_point_in_x(point_a) assert rectangle_a_polygon.is_point_in_x(point_a) - assert geometry.is_point_in_polygon_x(point_a, rectangle_b) + assert rectangle_b.is_point_in_x(point_a) assert rectangle_b_polygon.is_point_in_x(point_a) - assert geometry.is_point_in_polygon_x(point_a, quadrangle_a) is False + assert quadrangle_a.is_point_in_x(point_a) is False assert quadrangle_a_polygon.is_point_in_x(point_a) is False - assert geometry.is_point_in_polygon_x(point_b, rectangle_a) is False + assert rectangle_a.is_point_in_x(point_b) is False assert rectangle_a_polygon.is_point_in_x(point_b) is False - assert geometry.is_point_in_polygon_x(point_b, rectangle_b) is False + assert rectangle_b.is_point_in_x(point_b) is False assert rectangle_b_polygon.is_point_in_x(point_b) is False - assert geometry.is_point_in_polygon_x(point_b, quadrangle_a) + assert quadrangle_a.is_point_in_x(point_b) def test_get_centroid(rectangle_a): From c3f2558381f84af4c9c59c3f0a3435a4cd7c353a Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:04:41 +0200 Subject: [PATCH 3/4] :boom: :wrench: convert LocalInputSource page_count to an attribute --- mindee/input/local_input_source.py | 43 +++++++++---------- .../v1/workflows/test_workflow_integration.py | 2 - 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/mindee/input/local_input_source.py b/mindee/input/local_input_source.py index c4375b6a..09b19e6b 100644 --- a/mindee/input/local_input_source.py +++ b/mindee/input/local_input_source.py @@ -35,7 +35,7 @@ class LocalInputSource: filename: str file_mimetype: str filepath: str | None - _page_count: int | None = None + page_count: int def __init__(self) -> None: """ @@ -43,6 +43,19 @@ def __init__(self) -> None: """ self._check_mimetype() + if self.is_pdf(): + self.file_object.seek(0) + try: + pdf = pdfium.PdfDocument(self.file_object) + self.page_count = len(pdf) + except pdfium.PdfiumError as exc: + logger.warning( + "Could not open PDF file: %s due to %s", self.filename, exc + ) + self.page_count = 0 + self.file_object.seek(0) + else: + self.page_count = 1 logger.debug( "Loaded new input '%s' from %s", self.filename, {type(self).__name__} ) @@ -103,26 +116,6 @@ def is_pdf(self) -> bool: """:return: True if the file is a PDF.""" return self.file_mimetype == "application/pdf" - @property - def page_count(self) -> int: - """ - Count the pages in the document. - - :return: The number of pages. - """ - if self._page_count is None: - if self.is_pdf(): - self.file_object.seek(0) - pdf = pdfium.PdfDocument(self.file_object) - self._page_count = len(pdf) - else: - self._page_count = 1 - if self._page_count is None: - raise MindeeSourceError( - f"Failed to determine page count for {self.filename}" - ) - return self._page_count - def apply_page_options(self, page_options: PageOptions) -> None: """Apply cut and merge options on multipage documents.""" if not self.is_pdf(): @@ -132,6 +125,10 @@ def apply_page_options(self, page_options: PageOptions) -> None: page_options.on_min_pages, page_options.page_indexes, ) + self.file_object.seek(0) + pdf = pdfium.PdfDocument(self.file_object) + self.page_count = len(pdf) + pdf.close() def process_pdf( self, @@ -183,7 +180,9 @@ def merge_pdf_pages(self, page_numbers: set) -> None: bytes_io = io.BytesIO() new_pdf.save(bytes_io) self.file_object = bytes_io - self._page_count = len(new_pdf) + self.page_count = len(new_pdf) + new_pdf.close() + pdf.close() def is_pdf_empty(self) -> bool: """ diff --git a/tests/v1/workflows/test_workflow_integration.py b/tests/v1/workflows/test_workflow_integration.py index 68889783..00544a9c 100644 --- a/tests/v1/workflows/test_workflow_integration.py +++ b/tests/v1/workflows/test_workflow_integration.py @@ -41,7 +41,6 @@ def test_workflow_execution(mindee_client: Client, workflow_id: str, input_path: assert response.execution.priority == "low" -@pytest.mark.skip(reason="Currently not working") @pytest.mark.integration def test_workflow_predict_ots_rag( mindee_client: Client, workflow_id: str, input_path: str @@ -71,7 +70,6 @@ def test_workflow_predict_ots_no_rag( assert response.document.inference.extras is None -@pytest.mark.skip(reason="Currently not working") @pytest.mark.integration def test_workflow_predict_custom_rag( mindee_client: Client, workflow_id: str, input_path: str From 663cb4ad955c7c86f57a44b467b62cc5ce49c49c Mon Sep 17 00:00:00 2001 From: sebastianMindee <130448732+sebastianMindee@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:08:16 +0200 Subject: [PATCH 4/4] :arrow_down: remove pytz dependency --- .github/dependabot.yml | 1 - .pre-commit-config.yaml | 1 - pyproject.toml | 2 -- tests/conftest.py | 9 --------- 4 files changed, 13 deletions(-) delete mode 100644 tests/conftest.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b4290f2e..fe065afa 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,7 +14,6 @@ updates: groups: runtime-dependencies: patterns: - - "pytz" - "requests" optional-dependencies: patterns: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index df6e23dd..c7264114 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,6 @@ repos: exclude: "tests/|examples/|docs/" additional_dependencies: - toml - - types-pytz - types-requests - types-setuptools - importlib-metadata diff --git a/pyproject.toml b/pyproject.toml index 842edfee..b4afb752 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,6 @@ requires-python = ">=3.10" dependencies = [ "pypdfium2>=4.0,<6.0", "Pillow>=12.2.0", - "pytz>=2026.2", "requests>=2.34.2", ] @@ -45,7 +44,6 @@ Changelog = "https://github.com/mindee/mindee-api-python/blob/main/CHANGELOG.md" lint = [ "pylint==4.0.5", "pre-commit~=4.6.0", - "types-pytz>=2026.2.0.20260518", "types-requests>=2.33.0.20260518", "pip-audit>=2.10.0", ] diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index c130f51b..00000000 --- a/tests/conftest.py +++ /dev/null @@ -1,9 +0,0 @@ -import gc - -import pytest - - -@pytest.fixture(autouse=True) -def force_gc(): - yield - gc.collect()