From ebe0f1db8c1a25e2bd06090a9a592997adab634e Mon Sep 17 00:00:00 2001 From: Nacho Maiz Date: Fri, 29 Sep 2023 14:07:34 +0100 Subject: [PATCH] Fix: Parsing performance and filter bugs (#14) * Added more local nox sessions for docs * jupyter is now a private module * Fix & clarify required filters in brandscape_data * add docs and release badges to readme * remove old jupyter module * reduce dict access when determining rate limits * speed up parsing by 4x * formatting * update release notes and roadmap * bump version * formatting * release date --- CONTRIBUTING.md | 6 ++- README.md | 2 + bavapi/{jupyter.py => _jupyter.py} | 2 - bavapi/client.py | 17 ++++--- bavapi/filters.py | 32 +++++++++---- bavapi/http.py | 4 +- bavapi/parsing/responses.py | 53 ++++++++++++---------- bavapi/query.py | 2 +- bavapi/sync.py | 30 ++++++------ docs/endpoints/brandscape-data.md | 21 +++++---- docs/index.md | 2 + docs/release-notes.md | 19 ++++++++ docs/roadmap.md | 6 +-- noxfile.py | 27 +++++++---- pyproject.toml | 10 ++-- tests/reference/test_generate_reference.py | 8 ++-- tests/test_filters.py | 4 -- tests/test_jupyter.py | 10 ++-- 18 files changed, 154 insertions(+), 101 deletions(-) rename bavapi/{jupyter.py => _jupyter.py} (96%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 36af915..8c32f63 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,7 +58,7 @@ Please familiarize yourself with using these libraries in order to get started w It is highly recommended that you use [`mamba`](https://mamba.readthedocs.io/en/latest/) to manage Python environments in Windows. It is a faster implementation of `conda` and testing of `bavapi` on multiple versions of Python is set up to use `mamba` on Windows. -Once you have `mamba` installed in your system, you should be able to run `nox` commands. +Once you have `mamba` installed in your system, and you have set up an environment with `nox`, you should be able to run the `nox` commands for `bavapi`. ## Nox commands @@ -78,6 +78,8 @@ To see a list of all available `nox` commands, run `nox -l` in your terminal. He - `coverage`: combine and report coverage results. - `build`: build distributable files for `bavapi`. Suitable for CI/CD pipeline. - `docs_deploy`: publish `bavapi` documentation to GitHub Pages. Suitable for local development and CI/CD pipelines. +- `docs_serve`: serve `bavapi` documentation with `mike`. Suitable for local development. +- `docs_build_and_serve`: run both `docs_deploy` and `docs_serve`. Suitable for local development. ## Code Style Guidelines @@ -85,7 +87,7 @@ To see a list of all available `nox` commands, run `nox -l` in your terminal. He 1. Run `black bavapi` after writing or modifying code. This way the code style of the whole project will remain consistent. 2. Run `mypy bavapi` after writing or modifying code to make sure type hints are correctly defined. -3. Fully test your code using `pytest` and make sure you covered all your changes in the repository by running `nox -s tests_mamba`, `nox -s tests_mamba_e2e` and `nox -s coverage`. +3. Fully test your code using `pytest` and make sure you covered all your changes in the repository by running all `tests_mamba`, `tests_mamba_e2e` and `coverage` sessions from `nox`. ## Documentation diff --git a/README.md b/README.md index 4466069..26c2ba6 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![CI status](https://github.com/wppbav/bavapi-sdk-python/actions/workflows/ci.yml/badge.svg)](https://github.com/wppbav/bavapi-sdk-python/actions/workflows/ci.yml) [![coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/nachomaiz/32196acdc05431cd2bc7a8c73a587a8d/raw/covbadge.json)](https://github.com/wppbav/bavapi-sdk-python/actions/workflows/ci.yml) +[![release status](https://github.com/wppbav/bavapi-sdk-python/actions/workflows/release.yml/badge.svg)](https://github.com/wppbav/bavapi-sdk-python/actions/workflows/release.yml) +[![docs status](https://github.com/wppbav/bavapi-sdk-python/actions/workflows/docs.yml/badge.svg)](https://github.com/wppbav/bavapi-sdk-python/actions/workflows/docs.yml) [![PyPI](https://img.shields.io/pypi/v/wpp-bavapi)](https://pypi.org/project/wpp-bavapi/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/wpp-bavapi)](https://pypi.org/project/wpp-bavapi/) diff --git a/bavapi/jupyter.py b/bavapi/_jupyter.py similarity index 96% rename from bavapi/jupyter.py rename to bavapi/_jupyter.py index 453b0a8..992f05b 100644 --- a/bavapi/jupyter.py +++ b/bavapi/_jupyter.py @@ -12,8 +12,6 @@ if TYPE_CHECKING: from asyncio import AbstractEventLoop -__all__ = ("running_in_jupyter", "patch_loop") - def running_in_jupyter() -> bool: """ diff --git a/bavapi/client.py b/bavapi/client.py index 234cd30..bec6810 100644 --- a/bavapi/client.py +++ b/bavapi/client.py @@ -392,15 +392,18 @@ async def brandscape_data( This endpoint requires at least one of the following combinations of filters: - - `studies` - - `brand_name` - - `brands` - - `year_number`/`years` and `brands`/`brand_name` - - `country_code`/`countries` and `brands`/`brand_name` - - `year_number`/`years` and `country_code`/`countries` + - Study + Audience + Brand + Category + - Country + Year + Audience + - Brand + Audience + Country + Year + + You should read these from left to right. A combination of "Study + Audience" + worksjust as well as "Study + Audience + Brand". + However, "Category + Audience" will not. + + If you use Country or Year filters, you must use both filters together. An audience filter is also highly recommended, as otherwise the API will return - data for all audiences (there are more than 30 standard audiences). + data for all audiences (there are more than 100 standard audiences). The `Audiences` class is provided to make it easier to filter audiences. diff --git a/bavapi/filters.py b/bavapi/filters.py index 9cb5966..28cf0c2 100644 --- a/bavapi/filters.py +++ b/bavapi/filters.py @@ -181,10 +181,13 @@ class BrandscapeFilters(FountFilters): The `brandscape-data` endpoint requires the use of, at minimum, these filters: - - `studies` - - `brand_name` or `brands` - - `country_code` or `countries` and `brands` or `brand_name` - - `year_number` or `years` and `country_code` or `countries` + - Study + Audience + Brand + Category + - Country + Year + Audience + - Brand + Audience + Country + Year + + You should read these from left to right. A combination of "Study + Audience" + worksjust as well as "Study + Audience + Brand". + However, "Category + Audience" will not. An audience filter is also highly recommended, as otherwise the API will return data for all audiences (there are more than 30 standard audiences). @@ -236,14 +239,17 @@ class BrandscapeFilters(FountFilters): @model_validator(mode="before") @classmethod def _check_params(cls, values: Dict[str, object]) -> Dict[str, object]: + using_country_year = bool( + set(values).intersection( + ("country_code", "countries", "year_number", "years") + ) + ) + if not ( "brands" in values or "brand_name" in values or "studies" in values - or ( - ("country_code" in values or "countries" in values) - and ("year_number" in values or "years" in values) - ) + or using_country_year ): raise ValueError( "You need to apply either the `brands`, or `studies`, or `brand_name` " @@ -251,6 +257,16 @@ def _check_params(cls, values: Dict[str, object]) -> Dict[str, object]: "and `year_number`/`years` filters together." ) + if using_country_year: + if not ( + ("country_code" in values or "countries" in values) + and ("year_number" in values or "years" in values) + ): + raise ValueError( + "`country_code`/`countries` and `year_number`/`years` " + "filters must be used together." + ) + return values diff --git a/bavapi/http.py b/bavapi/http.py index cccde20..571a00a 100644 --- a/bavapi/http.py +++ b/bavapi/http.py @@ -242,10 +242,10 @@ async def query(self, endpoint: str, params: Query) -> Iterator[JSONDict]: (total) / (params.per_page or self.per_page) ) - if n_pages > int(resp.headers["x-ratelimit-remaining"]): + if n_pages > (limit_remaining := int(resp.headers["x-ratelimit-remaining"])): raise RateLimitExceededError( f"Number of pages ({n_pages}) for this request " - f"exceeds the rate limit ({resp.headers['x-ratelimit-remaining']}, " + f"exceeds the rate limit ({limit_remaining}, " f"total={resp.headers['x-ratelimit-limit']})." ) diff --git a/bavapi/parsing/responses.py b/bavapi/parsing/responses.py index dd38659..e0c18cc 100644 --- a/bavapi/parsing/responses.py +++ b/bavapi/parsing/responses.py @@ -16,12 +16,11 @@ T = TypeVar("T") +Entry = Mapping[str, Union[T, Dict[str, T]]] + def flatten_mapping( - mapping: Mapping[str, Union[T, Mapping[str, T]]], - parent: str = "", - sep: str = "_", - prefix: str = "", + mapping: Entry[T], parent: str = "", sep: str = "_", prefix: str = "" ) -> Dict[str, T]: """Recursively flattens all nested dictionaries into top level key-value pairs. @@ -29,7 +28,7 @@ def flatten_mapping( Parameters ---------- - mapping : dict[str, Any] or mapping + mapping : dict[str, Any] Dictionary with potential nested dictionaries parent : str Parent key for generating children keys, by default "" @@ -45,29 +44,36 @@ def flatten_mapping( dict[str, Any] Flattened dictionary. """ - new: Dict[str, T] = {} - + res: Dict[str, T] = {} + to_expand: Dict[str, Dict[str, T]] = {} for key, value in mapping.items(): if parent: key = f"{parent}{sep}{key}" - if isinstance(value, Mapping): - if prefix and any( - k.startswith(key) - for k, v in mapping.items() - if not isinstance(v, Mapping) - ): - key = f"{prefix}{sep}{key}" - - new = {**new, **flatten_mapping(value, key, sep, prefix)} + if isinstance(value, dict): + to_expand[key] = value else: - new[key] = value + res[key] = value + + with_prefix: set[str] = ( + {k for k in to_expand if any(_k.startswith(k) for _k in res)} + if prefix + else set() + ) + + expanded: Dict[str, T] = {} + for key, value in to_expand.items(): + if key in with_prefix: + key = f"{prefix}{sep}{key}" + + expanded.update(flatten_mapping(value, key, sep, prefix)) - return new + res.update(expanded) + return res def flatten( - mapping: Mapping[str, Union[T, Mapping[str, T]]], + mapping: Entry[T], parent: str = "", sep: str = "_", prefix: str = "", @@ -88,7 +94,7 @@ def flatten( Parameters ---------- - mapping : dict[str, Any] or mapping + mapping : dict[str, Any] Dictionary with potential nested dictionaries and lists of dictionaries parent : str Parent key for generating children keys, by default "" @@ -118,15 +124,14 @@ def flatten( yield new else: for key in expand_keys: - values = cast(List[Mapping[str, T]], new[key]) - print(key) + values = cast(List[Dict[str, T]], new[key]) for val in values: for i in flatten(val, key, sep, prefix, expand): yield {**{k: v for k, v in new.items() if k != key}, **i} def parse_response( - page: Iterable[Mapping[str, object]], + page: Iterable[Entry[T]], prefix: str = "", index: Optional[str] = None, expand: bool = False, @@ -135,7 +140,7 @@ def parse_response( Parameters ---------- - page : iterable of dicts with keys of type str + page : Iterable[dict[str, Any]] Page from API response. prefix : str, optional Prefix to prepend to columns with clashing names, by default `""` diff --git a/bavapi/query.py b/bavapi/query.py index e1bf9ce..0220922 100644 --- a/bavapi/query.py +++ b/bavapi/query.py @@ -123,7 +123,7 @@ def with_page(self, page: int, per_page: int) -> "Query[F]": return self return self.__class__.model_construct( - self.model_fields_set.union( # pylint: disable=no-member + self.model_fields_set.union( # pylint: disable=no-member {"page", "per_page"} ), page=self.page or page, diff --git a/bavapi/sync.py b/bavapi/sync.py index 5dc6a5a..e05de28 100644 --- a/bavapi/sync.py +++ b/bavapi/sync.py @@ -13,20 +13,11 @@ import asyncio import functools import sys -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Coroutine, - List, - Literal, - Optional, - TypeVar, -) +from typing import TYPE_CHECKING, Awaitable, Callable, List, Literal, Optional, TypeVar from bavapi import filters as _filters +from bavapi._jupyter import patch_loop, running_in_jupyter from bavapi.client import Client, OptionalFiltersOrMapping -from bavapi.jupyter import patch_loop, running_in_jupyter from bavapi.query import Query from bavapi.typing import BaseListOrValues, JSONDict, OptionalListOr @@ -45,7 +36,7 @@ F = TypeVar("F", bound=_filters.FountFilters) -def _coro(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]: +def _coro(func: Callable[P, Awaitable[T]]) -> Callable[P, T]: @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs): try: @@ -298,13 +289,18 @@ async def brandscape_data( This endpoint requires at least one of the following combinations of parameters: - - `studies` - - `brand_name` or `brands` - - `country_code` or `countries` and `brands` or `brand_name` - - `year_number` or `years` and `country_code` or `countries` + - Study + Audience + Brand + Category + - Country + Year + Audience + - Brand + Audience + Country + Year + + You should read these from left to right. A combination of "Study + Audience" + worksjust as well as "Study + Audience + Brand". + However, "Category + Audience" will not. + + If you use Country or Year filters, you must use both filters together. An audience filter is also highly recommended, as otherwise the API will return - data for all audiences (there are more than 30 standard audiences). + data for all audiences (there are more than 100 standard audiences). The `Audiences` class is provided to make it easier to filter audiences. diff --git a/docs/endpoints/brandscape-data.md b/docs/endpoints/brandscape-data.md index e19009b..787b7fa 100644 --- a/docs/endpoints/brandscape-data.md +++ b/docs/endpoints/brandscape-data.md @@ -43,11 +43,13 @@ For more information on available filters and functionality, see the Fount docum Thus, the `brandscape-data` endpoint has been restricted to require at least one of these specific set of filters: -- `studies` -- `brand_name`/`brands` -- `year_number`/`years` and `brands`/`brand_name` -- `country_code`/`countries` and `brands`/`brand_name` -- `year_number`/`years` and `country_code`/`countries` +- Study + Audience + Brand + Category +- Country + Year + Audience +- Brand + Audience + Country + Year + +You should read these from left to right. A combination of "Study + Audience" works just as well as "Study + Audience + Brand". However, "Category + Audience" will not. + +If you use Country or Year filters, you must use both filters together. If a query does not have any of these combinations of filters, it will raise a `ValidationError`: @@ -58,7 +60,7 @@ bavapi.brandscape_data("TOKEN", year_number=2022) # Error, not enough filters bavapi.brandscape_data("TOKEN", brand_name="Facebook") # OK -bavapi.brandscape_data("TOKEN", country_code="UK", brands=123) # OK +bavapi.brandscape_data("TOKEN", audience=22, brands=123) # OK ``` ## Default includes @@ -70,15 +72,14 @@ If you add any of these values in the `include` field by themselves, the default If, on the other hand, you request an `include` that is *not* part of the default values, `bavapi` will append that new value to the default `include` values. ```py -# All default includes will be requested +# All default (study, brand, category and audience) includes will be requested bavapi.brandscape_data("TOKEN", brand_name="Facebook") # Only the "brand" include will be requested bavapi.brandscape_data("TOKEN", brand_name="Facebook", include="brand") -# The "company" include will be appended to the default "include" values +# The "company" include will be appended to the default `include` values bavapi.brandscape_data("TOKEN", brand_name="Facebook", include="company") - ``` ## Clashing column names @@ -106,7 +107,7 @@ You can specify the metrics that your response should contain, and the API will - `relevance_c` - `relevance_rank` - Brand information such as `id`, `brand_name`, and `category_name` - - Any additional columns from the `include` parameter + - Any additional columns from the `include` parameter (see default behavior [above](#default-includes)) ```py bavapi.brandscape_data(studies=111, metric_keys="differentiation") # (1) diff --git a/docs/index.md b/docs/index.md index a7b1b10..e06b040 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,6 +2,8 @@ [![CI status](https://github.com/wppbav/bavapi-sdk-python/actions/workflows/ci.yml/badge.svg)](https://github.com/wppbav/bavapi-sdk-python/actions/workflows/ci.yml) [![coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/nachomaiz/32196acdc05431cd2bc7a8c73a587a8d/raw/covbadge.json)](https://github.com/wppbav/bavapi-sdk-python/actions/workflows/ci.yml) +[![release status](https://github.com/wppbav/bavapi-sdk-python/actions/workflows/release.yml/badge.svg)](https://github.com/wppbav/bavapi-sdk-python/actions/workflows/release.yml) +[![docs status](https://github.com/wppbav/bavapi-sdk-python/actions/workflows/docs.yml/badge.svg)](https://github.com/wppbav/bavapi-sdk-python/actions/workflows/docs.yml) [![PyPI](https://img.shields.io/pypi/v/wpp-bavapi)](https://pypi.org/project/wpp-bavapi/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/wpp-bavapi) ](https://pypi.org/project/wpp-bavapi/) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5c0bd28..246f38e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,25 @@ ## Version 0.8 +### Version 0.8.1 (September 29th, 2023) + +#### Performance + +- :rocket: Improved response parsing performance by ~4x (about 0.6 seconds faster per Fount query). + +#### Fix + +- :bug: Fix required filters in `brandscape_data` functions and methods. +- :broom: Remove buried print statement in response flattening logic. + +#### Docs + +- :notebook: Fixed and clarified required filters in `brandscape_data` functions and methods. + +#### Internal + +- :lock: Renamed `jupyter` compatibility module as private. This will remove it from the code reference docs. + ### Version 0.8.0 (September 15th, 2023) #### Feature diff --git a/docs/roadmap.md b/docs/roadmap.md index fa96693..fbab1b6 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -4,12 +4,12 @@ This is a non-exhaustive list of potential features & changes to `bavapi` before ## Core tooling -- ~~`pydantic` V2 support~~ :white_check_mark: +- ~~`pydantic` V2 support~~ :white_check_mark: v0.6.0 - Strict `mypy` support with [PEP 692](https://docs.python.org/3.12/whatsnew/3.12.html#whatsnew312-pep692) `Unpack` and `TypedDict` ## Known issues -- Fix sporadic `SSL: CERTIFICATE_VERIFY_FAILED` errors when making requests to the Fount API. +- Sporadic `SSL: CERTIFICATE_VERIFY_FAILED` errors when making requests to the Fount API. Currently, retrying the request usually fixes the issue. ## New fully-supported endpoints @@ -23,5 +23,5 @@ Eventually, the plan is to support all endpoints. This is the current priority l ## Stretch goals -- Smarter flattening of JSON responses, possibly through `pandas.json_normalize`. +- ~~Smarter flattening of JSON responses, possibly through `pandas.json_normalize`.~~ :white_check_mark: v0.8.1 speed-up by ~4x - Parse datetime values to `pandas` datetime. diff --git a/noxfile.py b/noxfile.py index fe7cd21..df52114 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,4 +1,4 @@ -# pylint: disable=import-outside-toplevel +# pylint: disable=import-outside-toplevel, invalid-name """Package tooling session definitions for `nox`""" @@ -10,6 +10,7 @@ import nox python_versions = ("3.8", "3.9", "3.10", "3.11") +python_latest = python_versions[-1] @nox.session(python=python_versions) @@ -37,7 +38,7 @@ def tests_nocov(session: nox.Session) -> None: session.run("pytest", "-m", "not e2e", *session.posargs) -@nox.session(python="3.11") +@nox.session(python=python_latest) def tests_e2e(session: nox.Session) -> None: """Run end to end tests on CI/CD pipeline.""" session.install("-e", ".[test]") @@ -54,7 +55,7 @@ def tests_e2e(session: nox.Session) -> None: ) -@nox.session(python="3.11") +@nox.session(python=python_latest) def tests_e2e_nocov(session: nox.Session) -> None: """Run end to end tests on CI/CD pipeline with no coverage.""" session.install("-e", ".[test]") @@ -90,7 +91,7 @@ def tests_mamba_nocov(session: nox.Session) -> None: session.run("pytest", "-m", "not e2e", *session.posargs, external=True) -@nox.session(python="3.11", venv_backend="mamba", reuse_venv=True) +@nox.session(python=python_latest, venv_backend="mamba", reuse_venv=True) def tests_mamba_e2e(session: nox.Session) -> None: """Run end to end tests locally with `mamba` as the backend.""" session.conda_install("--file", "requirements.txt", channel="conda-forge") @@ -109,7 +110,7 @@ def tests_mamba_e2e(session: nox.Session) -> None: ) -@nox.session(python="3.11", venv_backend="mamba", reuse_venv=True) +@nox.session(python=python_latest, venv_backend="mamba", reuse_venv=True) def tests_mamba_e2e_nocov(session: nox.Session) -> None: """Run end to end tests locally with `mamba` as the backend with no coverage.""" session.conda_install("--file", "requirements.txt", channel="conda-forge") @@ -118,7 +119,7 @@ def tests_mamba_e2e_nocov(session: nox.Session) -> None: session.run("pytest", "-m", "e2e", *session.posargs, external=True) -@nox.session(python="3.11") +@nox.session(python=python_latest) def coverage(session: nox.Session) -> None: """Compile and process coverage reports.""" args = session.posargs or ["report"] @@ -145,7 +146,7 @@ def coverage(session: nox.Session) -> None: file.write(f"### Total coverage: {total}%") -@nox.session(python="3.11") +@nox.session(python=python_latest) def lint(session: nox.Session) -> None: """Lint package.""" session.install("-e", ".[lint]") @@ -196,7 +197,6 @@ def docs_deploy(session: nox.Session) -> None: """ # Get current package version - if os.getenv("CI"): session.run("pip", "install", "-e", ".[doc]") @@ -240,3 +240,14 @@ def version_tuple(string: str) -> tuple[int, ...]: deploy_args.extend(("latest", "--update-aliases")) session.run("mike", "deploy", *deploy_args) + + +@nox.session(python=False) +def docs_serve(session: nox.Session) -> None: + session.run("mike", "serve") + + +@nox.session(python=False) +def docs_build_and_serve(session: nox.Session) -> None: + session.notify("docs_deploy") + session.notify("docs_serve") diff --git a/pyproject.toml b/pyproject.toml index e9f2872..ed7bc53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "wpp-bavapi" -version = "0.8.0" +version = "0.8.1" authors = [ { name = "Ignacio Maiz Vilches", email = "ignacio.maiz@bavgroup.com" }, ] @@ -34,21 +34,23 @@ classifiers = [ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Topic :: Database :: Database Engines/Servers', 'Topic :: Scientific/Engineering :: Information Analysis', 'Topic :: Software Development :: Libraries :: Python Modules', ] dependencies = [ "httpx >= 0.20", + "nest-asyncio >= 1.5.6", "pandas >= 0.16.2", "pydantic >= 2", "tqdm >= 4.62", - "nest-asyncio >= 1.5.6", "typing-extensions >= 3.10; python_version < '3.10'", ] [project.urls] -homepage = "https://github.com/wppbav/bavapi-sdk-python" +homepage = "https://wppbav.github.io/bavapi-sdk-python/" +repository = "https://github.com/wppbav/wpp-bavapi/" [project.optional-dependencies] dev = ["black", "nox", "pip-tools"] @@ -61,8 +63,8 @@ doc = [ "mkdocs-section-index", "mike", ] -test = ["coverage", "pytest", "pytest-asyncio", "python-dotenv"] lint = ["isort", "mypy", "pylint", "pandas-stubs"] +test = ["coverage", "pytest", "pytest-asyncio", "python-dotenv"] [project.scripts] bavapi-gen-refs = "bavapi.reference.generate_reference:main" diff --git a/tests/reference/test_generate_reference.py b/tests/reference/test_generate_reference.py index 96d76b8..9717f93 100644 --- a/tests/reference/test_generate_reference.py +++ b/tests/reference/test_generate_reference.py @@ -113,9 +113,7 @@ def test_parse_args_all(): assert args.name == "" -@mock.patch( - "bavapi.reference.generate_reference.os.getenv", return_value="test_token" - ) +@mock.patch("bavapi.reference.generate_reference.os.getenv", return_value="test_token") def test_main_no_args(mock_getenv: mock.Mock): assert uref.main([]) == 1 mock_getenv.assert_called_once_with("FOUNT_API_KEY", "") @@ -183,7 +181,9 @@ def test_main_no_token_no_dotenv(mock_load_dotenv: mock.Mock): @mock.patch("dotenv.load_dotenv", wraps=wraps(raises=ImportError)) @mock.patch("bavapi.reference.generate_reference.write_to_file") def test_main_with_token_arg( - mock_write_to_file: mock.Mock, mock_load_dotenv: mock.Mock, mock_raw_query: mock.AsyncMock + mock_write_to_file: mock.Mock, + mock_load_dotenv: mock.Mock, + mock_raw_query: mock.AsyncMock, ): args = ["-n", "audiences", "-t", "test_token"] diff --git a/tests/test_filters.py b/tests/test_filters.py index 26ded8e..457459b 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -48,10 +48,6 @@ def test_studies_skip_parse_date(): {"brands": [0]}, {"studies": [0]}, {"country_code": "", "year_number": 0}, - {"brands": [0], "country_code": ""}, - {"brands": [0], "year_number": 0}, - {"brand_name": "", "country_code": ""}, - {"brand_name": "", "year_number": 0}, {"brand_name": ""}, ), ) diff --git a/tests/test_jupyter.py b/tests/test_jupyter.py index 4191d2a..45bd41a 100644 --- a/tests/test_jupyter.py +++ b/tests/test_jupyter.py @@ -9,7 +9,7 @@ import pytest -from bavapi import jupyter +from bavapi import _jupyter SetIpythonFunc = Callable[[Optional[bool]], None] @@ -39,19 +39,19 @@ def _set_ipython(kernel: Optional[bool] = None) -> None: def test_running_in_jupyter(set_ipython: SetIpythonFunc): set_ipython(True) - assert jupyter.running_in_jupyter() + assert _jupyter.running_in_jupyter() def test_not_running_in_jupyter(set_ipython: SetIpythonFunc): set_ipython(None) - assert not jupyter.running_in_jupyter() + assert not _jupyter.running_in_jupyter() -@mock.patch("bavapi.jupyter.nest_asyncio.apply") +@mock.patch("bavapi._jupyter.nest_asyncio.apply") def test_enabled_nested(mock_apply: mock.Mock): loop = asyncio.new_event_loop() - jupyter.patch_loop(loop) + _jupyter.patch_loop(loop) mock_apply.assert_called_once_with(loop)