diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 68b4362..e025840 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,13 +35,13 @@ pip install -e .[dev, doc, test, lint] This will install all optional dependencies which are necessary for contributing to the code base. -## Fount API Key +## BAV API Key -You will need a Fount API key to perform requests to the API through `bavapi`. +You will need a BAV API key to perform requests to the API through `bavapi`. To get and use an API key, see the [Authentication](https://wppbav.github.io/bavapi-sdk-python/latest/getting-started/authentication) section of the Getting Started guide. -In order to run integration tests, you will need to use an `.env` file to store your Fount API key as `FOUNT_API_KEY` (you can also set the environment variable directly). See the [instructions](https://wppbav.github.io/bavapi-sdk-python/latest/getting-started/authentication#recommended-way-to-manage-api-keys) for more details. +In order to run integration tests and the `bavapi-gen-refs` [command](https://wppbav.github.io/bavapi-sdk-python/latest/getting-started/reference-classes), you will need to use an `.env` file to store your BAV API key as `BAV_API_KEY` (you can also set the environment variable directly). See the [instructions](https://wppbav.github.io/bavapi-sdk-python/latest/getting-started/authentication#recommended-way-to-manage-api-keys) for more details. ## Tools and frameworks diff --git a/README.md b/README.md index 26c2ba6..902425e 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,11 @@ You will also need a BAV API token. For more information, go to the [Authenticat ### Dependencies - `httpx >= 0.20` -- `nest-asyncio >= 1.5.6` -- `pandas >= 0.16.2` +- `nest-asyncio >= 1.5` +- `pandas >= 1.0` - `pydantic >= 2.0` - `tqdm >= 4.62` -- `typing-extensions >= 3.10` for Python < 3.10 +- `typing-extensions >= 4.6` for Python < 3.12 ## Installation @@ -74,12 +74,20 @@ Once you have acquired a token, you can start using this library directly in pyt ## Features -- Support for all endpoints in the Fount API. Extended support for the `audiences`, `brands`, `brandscape-data` and `studies` endpoints. +- Support for all endpoints in the WPPBAV Fount API. + - Extended support for the following endpoints: + - `audiences` + - `brand-metrics` + - `brand-metric-groups` + - `brands` + - `brandscape-data` + - `categories` + - `collections` + - `sectors` + - `studies` - Other endpoints are available via the `raw_query` functions and methods. -- Validates query parameters are of the correct types. - - Provides type hints for better IDE support. -- Retrieve multiple pages of data simultaneously. - - Monitors and prevents exceeding API rate limit. +- Validates query parameters are of the correct types and provides type hints for better IDE support. +- Retrieve multiple pages of data simultaneously, monitoring and preventing exceeding API rate limit. - Both synchronous and asynchronous APIs for accessing BAV data. ## Documentation diff --git a/bavapi/__init__.py b/bavapi/__init__.py index 1895cba..5eda14c 100644 --- a/bavapi/__init__.py +++ b/bavapi/__init__.py @@ -18,7 +18,7 @@ >>> import bavapi >>> res = bavapi.brands("TOKEN", name="Facebook") -For more advanced (and async compatibility), use the `Client` pattern: +For more advanced usage (and async compatibility), use the `bavapi.Client` class: >>> import bavapi >>> async with bavapi.Client("API_TOKEN") as bav: @@ -31,14 +31,14 @@ from bavapi.query import Query from bavapi.sync import ( audiences, - brand_metrics, brand_metric_groups, + brand_metrics, brands, brandscape_data, categories, collections, - sectors, raw_query, + sectors, studies, ) diff --git a/bavapi/reference/__init__.py b/bavapi/_reference/__init__.py similarity index 58% rename from bavapi/reference/__init__.py rename to bavapi/_reference/__init__.py index b5c329f..f159f1d 100644 --- a/bavapi/reference/__init__.py +++ b/bavapi/_reference/__init__.py @@ -3,7 +3,10 @@ These reference classes make it easier to filter results by mapping commonly used IDs to human-readable options. -To generate reference classes you will need a Fount API token. +To generate reference classes you will need a WPPBAV Fount API token. Please see the authentication section of the documentation for more info: + +As well as the `bavapi` documentation on Authentication: + """ diff --git a/bavapi/reference/generate_reference.py b/bavapi/_reference/generate_reference.py similarity index 96% rename from bavapi/reference/generate_reference.py rename to bavapi/_reference/generate_reference.py index 42d8bc4..bbd5a45 100644 --- a/bavapi/reference/generate_reference.py +++ b/bavapi/_reference/generate_reference.py @@ -145,7 +145,7 @@ def generate_source( ref_name: str, ref_items: Dict[str, str], updated: datetime.datetime, - import_items: Tuple[str, str] = ("bavapi.reference._int_enum", "IntEnum"), + import_items: Tuple[str, str] = ("bavapi._reference.int_enum", "IntEnum"), ) -> str: """Generate updated module source from reference items. @@ -225,7 +225,7 @@ def parse_args(argv: Optional[List[str]] = None) -> Args: "Existing reference files will be overwritten.", epilog="DON'T PUSH REFERENCES TO GIT! Add `bavapi_refs/` to `.gitignore`.", ) - parser.add_argument("-t", "--token", default="", help="Fount API token.") + parser.add_argument("-t", "--token", default="", help="WPPBAV Fount API token.") parser.add_argument( "-a", "--all", action="store_true", help="Generate all reference files." ) @@ -266,10 +266,10 @@ def main(argv: Optional[List[str]] = None) -> int: except ImportError as exc: raise ValueError( "You must specify a Fount API token with the `-t`/`--token` argument, " - "or install `python-dotenv` and set `FOUNT_API_KEY` in a `.env` file." + "or install `python-dotenv` and set `BAV_API_KEY` in a `.env` file." ) from exc - fount = Client(os.getenv("FOUNT_API_KEY", args.token)) + fount = Client(os.getenv("BAV_API_KEY", args.token)) ref_configs: Dict[str, RefConfig] = { "audiences": RefConfig("audiences", "audiences", parse_audiences), diff --git a/bavapi/reference/_int_enum.py b/bavapi/_reference/int_enum.py similarity index 100% rename from bavapi/reference/_int_enum.py rename to bavapi/_reference/int_enum.py diff --git a/bavapi/client.py b/bavapi/client.py index 513f6cf..2dc4303 100644 --- a/bavapi/client.py +++ b/bavapi/client.py @@ -1,4 +1,38 @@ -"""Fount API interface.""" +""" +Asynchronous BAV API client interface. + +Similar to `requests.Session` or `httpx.AsyncClient` (uses the latter as dependency). + +Examples +-------- + +Create a client instance and make a request to the `brands` endpoint. + +>>> async with bavapi.Client("TOKEN") as bav: +... result = await bav.brands("Facebook") + +A more complex query: + +>>> from bavapi_refs.audiences import Audiences +>>> async with bavapi.Client("TOKEN") as bav: +... bss = await bav.brandscape_data( +... country_code="UK", +... year_number=2022, +... audiences=Audiences.ALL_ADULTS, +... ) + +Multiple queries will share the client connection for better performance: + +>>> async with bavapi.Client("TOKEN") as bav: +... result1 = await bav.brands("Facebook") +... result2 = await bav.brands("Instagram") + +Use `Client.raw_query` (with `bavapi.Query`) for endpoints that aren't fully supported: + +>>> query = bavapi.Query(filters=bavapi.filters.FountFilters(name="Meta")) +>>> async with bavapi.Client("TOKEN") as bav: +... result = await bav.raw_query("companies", params=query) +""" # pylint: disable=too-many-arguments, too-many-lines @@ -18,10 +52,11 @@ from bavapi.http import HTTPClient from bavapi.parsing.responses import parse_response from bavapi.query import Query -from bavapi.typing import JSONDict, OptionalListOr, Unpack, CommonQueryParams +from bavapi.typing import CommonQueryParams, JSONDict, OptionalListOr, Unpack if TYPE_CHECKING: from types import TracebackType + from pandas import DataFrame __all__ = ("Client",) @@ -37,7 +72,7 @@ class Client: - """Asynchronous API to interact with the WPPBAV Fount. + """Asynchronous API to interact with the WPPBAV Fount API. This class uses `asyncio` to perform asynchronous requests to the Fount API. @@ -48,7 +83,7 @@ class Client: To use the Client class, you will need to precede calls with `await`: ```py - bav = Client("TOKEN") # creating instance does not use `await` + bav = Client("TOKEN") # creating client instance does not use `await` data = await bav.brands("Swatch") # must use `await` ``` @@ -59,7 +94,7 @@ class Client: Parameters ---------- auth_token : str, optional - Fount API authorization token, by default `''` + WPPBAV Fount API authorization token, by default `''` per_page : int, optional Default number of entries per page, by default 100 timeout : float, optional @@ -197,8 +232,8 @@ async def aclose(self) -> None: return await self._client.aclose() async def raw_query(self, endpoint: str, params: Query[F]) -> List[JSONDict]: - """Perform a raw GET query to the Fount API, returning the response JSON data - instead of a `pandas` DataFrame. + """Perform a raw GET query to the WPPBAV Fount API, returning + the response JSON data instead of a `pandas` DataFrame. Parameters ---------- @@ -240,13 +275,13 @@ async def audiences( Fount audience ID, by default None If an audience ID is provided, only that audience will be returned - active : Literal[0, 1] + active : Literal[0, 1], optional Return active audiences only if set to `1`, by default 0 - inactive : Literal[0, 1] + inactive : Literal[0, 1], optional Return inactive audiences only if set to `1`, by default 0 - public : Literal[0, 1] + public : Literal[0, 1], optional Return active audiences only if set to `1`, by default 0 - private : Literal[0, 1] + private : Literal[0, 1], optional Return inactive audiences only if set to `1`, by default 0 groups : int or list[int], optional Audience group ID or list of audience group IDs, by default None @@ -336,13 +371,13 @@ async def brand_metrics( Fount metric ID, by default None If an metric ID is provided, only that metric will be returned - active : Literal[0, 1] + active : Literal[0, 1], optional Return active brand metrics only if set to `1`, by default 0 - inactive : Literal[0, 1] + inactive : Literal[0, 1], optional Return inactive brand metrics only if set to `1`, by default 0 - public : Literal[0, 1] + public : Literal[0, 1], optional Return active brand metrics only if set to `1`, by default 0 - private : Literal[0, 1] + private : Literal[0, 1], optional Return inactive brand metrics only if set to `1`, by default 0 groups : int or list[int], optional Brand metrics group ID or list of Brand metrics group IDs, by default None @@ -429,9 +464,9 @@ async def brand_metric_groups( Fount metric group ID, by default None If an metric group ID is provided, only that metric group will be returned - active : Literal[0, 1] + active : Literal[0, 1], optional Return active brand metric groups only if set to `1`, by default 0 - inactive : Literal[0, 1] + inactive : Literal[0, 1], optional Return inactive brand metric groups only if set to `1`, by default 0 filters : BrandMetricGroupsFilters or dict of filters, optional BrandMetricGroupsFilters object or dictionary of filter parameters, by default None @@ -739,7 +774,7 @@ async def categories( If an category ID is provided, only that category will be returned sector : int or list[int], optional - Filter categories by sector ID, by default 0 + Filter categories by sector ID, by default None filters : CategoriesFilters or dict of filters, optional CategoriesFilters object or dictionary of filter parameters, by default None fields : str or list[str], optional diff --git a/bavapi/exceptions.py b/bavapi/exceptions.py index 733beda..c2a5b87 100644 --- a/bavapi/exceptions.py +++ b/bavapi/exceptions.py @@ -1,4 +1,4 @@ -"""Exceptions for handling errors with the Fount API.""" +"""Exceptions for handling errors with the WPPBAV Fount API.""" class APIError(Exception): diff --git a/bavapi/filters.py b/bavapi/filters.py index 06f6d42..8820f1e 100644 --- a/bavapi/filters.py +++ b/bavapi/filters.py @@ -1,4 +1,28 @@ -"""Filter objects for Fount API queries based on `pydantic`.""" +""" +Filter objects for WPPBAV Fount API queries based on `pydantic`. + +All endpoint filters are subclasses of `FountFilters`. + +You can use any endpoint filter class with `raw_query` functions and methods, +but you must use endpoint-specific filters for each endpoint function or method. + +Examples +-------- + +Use `BrandsFilters` with the `brands` endpoint: + +>>> import bavapi +>>> bavapi.brands("TOKEN", filters=bavapi.filters.BrandsFilters(name="Facebook")) + +`FountFilters` is compatible with all endpoints (including `raw_query`): + +>>> bavapi.brands("TOKEN", filters=bavapi.filters.FountFilters(name="Facebook")) + +Using the wrong filter can lead to unexpected results: +>>> bavapi.brands("TOKEN", filters=bavapi.filters.CategoriesFilters(country_codes="UK")) + +The above example may work, but it is highly discouraged. +""" # pylint: disable=no-name-in-module, too-few-public-methods @@ -64,21 +88,21 @@ def ensure( filters: Optional[FiltersOrMapping["FountFilters"]], **addl_filters: InputSequenceOrValues, ) -> Optional[F]: - """Ensure FountFilters class from dictionary or other FountFilters class. + """Ensure `FountFilters` class from dictionary or other `FountFilters` class. Defaults to values passed to `filters` when any additional filters overlap. Parameters ---------- filters : FountFilters or dict of filter values, optional - Dictionary of filters or FountFilters class. + Dictionary of filters or `FountFilters` class. **addl_filters : SequenceOrValues, optional - Additional filters to add to the new FountFilters instance. + Additional filters to add to the new `FountFilters` instance. Returns ------- FountFilters, optional - FountFilters class or None if `filters` is None and no additional filters are passed. + `FountFilters` class or `None` if `filters` is `None` and no additional filters are passed. """ addl_filters = {k: v for k, v in addl_filters.items() if v} diff --git a/bavapi/http.py b/bavapi/http.py index 1f86feb..4e7afd3 100644 --- a/bavapi/http.py +++ b/bavapi/http.py @@ -31,7 +31,7 @@ __all__ = ("HTTPClient",) -class Query(Protocol): +class _Query(Protocol): """Protocol for Query objects.""" item_id: Optional[int] @@ -43,7 +43,7 @@ def to_params(self, endpoint: str) -> BaseParamsMapping: """HTTP-compatible params dictionary""" raise NotImplementedError - def paginated(self, per_page: int, n_pages: int) -> Iterator["Query"]: + def paginated(self, per_page: int, n_pages: int) -> Iterator["_Query"]: """Yields Query objects with page parameters for paginated queries""" raise NotImplementedError @@ -138,7 +138,7 @@ async def aclose(self) -> None: """Asynchronously close all client connections.""" return await self.client.aclose() - async def get(self, endpoint: str, params: Query) -> httpx.Response: + async def get(self, endpoint: str, params: _Query) -> httpx.Response: """Perform GET request on the given endpoint. Parameters @@ -177,7 +177,7 @@ async def get(self, endpoint: str, params: Query) -> httpx.Response: return resp async def get_pages( - self, endpoint: str, params: Query, n_pages: int + self, endpoint: str, params: _Query, n_pages: int ) -> List[httpx.Response]: """Perform GET requests for a given number of pages on an endpoint. @@ -200,11 +200,8 @@ async def get_pages( for p in params.paginated(self.per_page, n_pages) ] try: - return cast( - List[httpx.Response], - await tqdm.gather( - *tasks, desc=f"{endpoint} query", disable=not self.verbose - ), + return await tqdm.gather( + *tasks, desc=f"{endpoint} query", disable=not self.verbose ) except Exception as exc: for task in tasks: @@ -212,7 +209,7 @@ async def get_pages( raise exc - async def query(self, endpoint: str, params: Query) -> Iterator[JSONDict]: + async def query(self, endpoint: str, params: _Query) -> Iterator[JSONDict]: """Perform a paginated GET request on the given endpoint. Parameters diff --git a/bavapi/sync.py b/bavapi/sync.py index b422383..9ac7348 100644 --- a/bavapi/sync.py +++ b/bavapi/sync.py @@ -1,11 +1,33 @@ """ -Convenience functions to perform queries to the Fount synchronously. +Top level functions to perform queries to the Fount. -Can be used directly without `asyncio`. +You will need your BAV API token to use these functions. -Meant for experimentation, Jupyter notebooks, one-off scripts, etc. +Examples +-------- -Use `bavapi.Client` for more advanced usage and performance benefits. +Use top level functions for one-off downloads: + +>>> import bavapi +>>> result = bavapi.brands("TOKEN", "Facebook") # Replace TOKEN with your API key + +A more complex query: + +>>> from bavapi_refs.audiences import Audiences +>>> bss = bavapi.brandscape_data( +... "TOKEN", # Replace TOKEN with your API key +... country_code="UK", +... year_number=2022, +... audiences=Audiences.ALL_ADULTS, +... ) + +Use `bavapi.raw_query` (with `bavapi.Query`) for endpoints that aren't fully supported: + +>>> query = bavapi.Query(filters=bavapi.filters.FountFilters(name="Meta")) +>>> result = bavapi.raw_query("companies", params=query) + +If you want to make multiple requests or embed `bavapi` into applications, +consider using the `bavapi.Client` interface. """ # pylint: disable=redefined-outer-name, too-many-arguments, too-many-locals @@ -18,7 +40,7 @@ from bavapi._jupyter import patch_loop, running_in_jupyter from bavapi.client import Client, OptionalFiltersOrMapping from bavapi.query import Query -from bavapi.typing import JSONDict, OptionalListOr, Unpack, CommonQueryParams, ParamSpec +from bavapi.typing import CommonQueryParams, JSONDict, OptionalListOr, ParamSpec, Unpack if TYPE_CHECKING: from pandas import DataFrame @@ -72,7 +94,7 @@ async def raw_query( Parameters ---------- token : str - Fount API token + WPPBAV Fount API token endpoint : str Endpoint name params : Query @@ -116,20 +138,20 @@ async def audiences( Parameters ---------- token : str - Fount API token + WPPBAV Fount API token name : str, optional Search audiences by name, by default None audience_id : int, optional Fount audience ID, by default None If an audience ID is provided, only that audience will be returned - active : Literal[0, 1] + active : Literal[0, 1], optional Return active audiences only if set to `1`, by default 0 - inactive : Literal[0, 1] + inactive : Literal[0, 1], optional Return inactive audiences only if set to `1`, by default 0 - public : Literal[0, 1] + public : Literal[0, 1], optional Return active audiences only if set to `1`, by default 0 - private : Literal[0, 1] + private : Literal[0, 1], optional Return inactive audiences only if set to `1`, by default 0 groups : int or list[int], optional Audience group ID or list of audience group IDs, by default None @@ -144,7 +166,7 @@ async def audiences( Additional resources to include in API response, by default None stack_data : bool, optional Whether to expand nested lists into new dictionaries, by default False - timeout : float + timeout : float, optional Maximum timeout for requests in seconds, by default 30.0 verbose : bool, optional Set to False to disable progress bar, by default True @@ -216,20 +238,20 @@ async def brand_metrics( Parameters ---------- token : str - Fount API token + WPPBAV Fount API token name : str, optional Search brand metrics by name, by default None metric_id : int, optional Fount metric ID, by default None If an metric ID is provided, only that metric will be returned - active : Literal[0, 1] + active : Literal[0, 1], optional Return active brand metrics only if set to `1`, by default 0 - inactive : Literal[0, 1] + inactive : Literal[0, 1], optional Return inactive brand metrics only if set to `1`, by default 0 - public : Literal[0, 1] + public : Literal[0, 1], optional Return active brand metrics only if set to `1`, by default 0 - private : Literal[0, 1] + private : Literal[0, 1], optional Return inactive brand metrics only if set to `1`, by default 0 groups : int or list[int], optional Brand metrics group ID or list of brand metrics group IDs, by default None @@ -244,7 +266,7 @@ async def brand_metrics( Additional resources to include in API response, by default None stack_data : bool, optional Whether to expand nested lists into new dictionaries, by default False - timeout : float + timeout : float, optional Maximum timeout for requests in seconds, by default 30.0 verbose : bool, optional Set to False to disable progress bar, by default True @@ -296,7 +318,7 @@ async def brand_metrics( async def brand_metric_groups( token: str, name: Optional[str] = None, - metric_id: Optional[int] = None, + group_id: Optional[int] = None, active: Literal[0, 1] = 0, inactive: Literal[0, 1] = 0, *, @@ -313,16 +335,16 @@ async def brand_metric_groups( Parameters ---------- token : str - Fount API token + WPPBAV Fount API token name : str, optional Search brand metric groups by name, by default None - metric_id : int, optional + group_id : int, optional Fount brand metric group ID, by default None If a metric group ID is provided, only that metric group will be returned - active : Literal[0, 1] + active : Literal[0, 1], optional Return active brand metric groups only if set to `1`, by default 0 - inactive : Literal[0, 1] + inactive : Literal[0, 1], optional Return inactive brand metric groups only if set to `1`, by default 0 filters : BrandMetricGroupsFilters or dict of filters, optional BrandMetricGroupsFilters object or dictionary of filter parameters, by default None @@ -335,7 +357,7 @@ async def brand_metric_groups( Additional resources to include in API response, by default None stack_data : bool, optional Whether to expand nested lists into new dictionaries, by default False - timeout : float + timeout : float, optional Maximum timeout for requests in seconds, by default 30.0 verbose : bool, optional Set to False to disable progress bar, by default True @@ -369,7 +391,7 @@ async def brand_metric_groups( async with Client(token, timeout=timeout, verbose=verbose) as client: return await client.brand_metric_groups( name, - metric_id, + group_id, active, inactive, filters=filters, @@ -402,7 +424,7 @@ async def brands( Parameters ---------- token : str - Fount API token + WPPBAV Fount API token name : str, optional Search brands by name, by default None country_codes: str or list[str], optional @@ -426,7 +448,7 @@ async def brands( Additional resources to include in API response, by default None stack_data : bool, optional Whether to expand nested lists into new dictionaries, by default False - timeout : float + timeout : float, optional Maximum timeout for requests in seconds, by default 30.0 verbose : bool, optional Set to False to disable progress bar, by default True @@ -525,7 +547,7 @@ async def brandscape_data( Parameters ---------- token : str - Fount API token + WPPBAV Fount API token country_code : str or list[str], optional ISO-3166-1 alpha-2 country codes, by default None year_number : int or list[int], optional @@ -551,7 +573,7 @@ async def brandscape_data( Key or list of keys for the metrics included in the response, by default None stack_data : bool, optional Whether to expand nested lists into new dictionaries, by default False - timeout : float + timeout : float, optional Maximum timeout for requests in seconds, by default 30.0 verbose : bool, optional Set to False to disable progress bar, by default True @@ -646,7 +668,7 @@ async def categories( Additional resources to include in API response, by default None stack_data : bool, optional Whether to expand nested lists into new dictionaries, by default False - timeout : float + timeout : float, optional Maximum timeout for requests in seconds, by default 30.0 verbose : bool, optional Set to False to disable progress bar, by default True @@ -734,7 +756,7 @@ async def collections( Additional resources to include in API response, by default None stack_data : bool, optional Whether to expand nested lists into new dictionaries, by default False - timeout : float + timeout : float, optional Maximum timeout for requests in seconds, by default 30.0 verbose : bool, optional Set to False to disable progress bar, by default True @@ -821,7 +843,7 @@ async def sectors( Additional resources to include in API response, by default None stack_data : bool, optional Whether to expand nested lists into new dictionaries, by default False - timeout : float + timeout : float, optional Maximum timeout for requests in seconds, by default 30.0 verbose : bool, optional Set to False to disable progress bar, by default True @@ -887,7 +909,7 @@ async def studies( Parameters ---------- token : str - Fount API token + WPPBAV Fount API token country_codes: str or list[str], optional ISO-3166-1 alpha-2 country codes, by default None year_numbers : int or list[int], optional @@ -911,7 +933,7 @@ async def studies( Additional resources to include in API response, by default None stack_data : bool, optional Whether to expand nested lists into new dictionaries, by default False - timeout : float + timeout : float, optional Maximum timeout for requests in seconds, by default 30.0 verbose : bool, optional Set to False to disable progress bar, by default True diff --git a/bavapi/typing.py b/bavapi/typing.py index 33c5afd..f88910d 100644 --- a/bavapi/typing.py +++ b/bavapi/typing.py @@ -11,8 +11,8 @@ MutableMapping, Optional, Sequence, - TypeVar, TypedDict, + TypeVar, Union, ) diff --git a/docs/endpoints/brand-metric-groups.md b/docs/endpoints/brand-metric-groups.md index e675424..0162ab2 100644 --- a/docs/endpoints/brand-metric-groups.md +++ b/docs/endpoints/brand-metric-groups.md @@ -34,6 +34,7 @@ For more information on available filters and functionality, see the Fount docum These filters are available directly within the function/method: - `name` +- `group_id` - `active` - `inactive` diff --git a/docs/endpoints/brand-metrics.md b/docs/endpoints/brand-metrics.md index cba514c..a6f24dc 100644 --- a/docs/endpoints/brand-metrics.md +++ b/docs/endpoints/brand-metrics.md @@ -34,6 +34,7 @@ For more information on available filters and functionality, see the Fount docum These filters are available directly within the function/method: - `name` +- `metric_id` - `active` - `inactive` - `public` diff --git a/docs/endpoints/categories.md b/docs/endpoints/categories.md index b10a301..dad47b6 100644 --- a/docs/endpoints/categories.md +++ b/docs/endpoints/categories.md @@ -34,6 +34,7 @@ For more information on available filters and functionality, see the Fount docum These filters are available directly within the function/method: - `name` +- `category_id` - `sector` For other filters, passing a `CategoriesFilters` instance to the `filters` parameter is required. diff --git a/docs/endpoints/collections.md b/docs/endpoints/collections.md index 510cf7a..b49daa7 100644 --- a/docs/endpoints/collections.md +++ b/docs/endpoints/collections.md @@ -34,6 +34,7 @@ For more information on available filters and functionality, see the Fount docum These filters are available directly within the function/method: - `name` +- `collection_id` - `public` - `shared_with_me` - `mine` diff --git a/docs/endpoints/sectors.md b/docs/endpoints/sectors.md index 4d493f8..e4b9541 100644 --- a/docs/endpoints/sectors.md +++ b/docs/endpoints/sectors.md @@ -34,6 +34,7 @@ For more information on available filters and functionality, see the Fount docum These filters are available directly within the function/method: - `name` +- `sector_id` - `in_most_influential` - `not_in_most_influential` diff --git a/docs/gen_ref_pages.py b/docs/gen_ref_pages.py index da1dd0b..5291699 100644 --- a/docs/gen_ref_pages.py +++ b/docs/gen_ref_pages.py @@ -1,5 +1,4 @@ """Generate the code reference pages and navigation.""" - from pathlib import Path import mkdocs_gen_files @@ -7,22 +6,23 @@ nav = mkdocs_gen_files.nav.Nav() for path in sorted(Path("./bavapi").rglob("*.py")): - if path.name == "__init__.py": - pass module_path = path.relative_to("./bavapi").with_suffix("") doc_path = path.relative_to("./bavapi").with_suffix(".md") full_doc_path = Path("reference", doc_path) - parts = tuple(module_path.parts) + parts = module_path.parts + end_part = parts[-1] + if ( + end_part in {"__main__", "typing"} + or (end_part != "__init__" and end_part.startswith("_")) + or any(part.startswith("_") for part in parts[:-1]) + ): + continue if parts[-1] == "__init__": parts = parts[:-1] doc_path = doc_path.with_name("index.md") full_doc_path = full_doc_path.with_name("index.md") - elif parts[-1] in {"__main__", "typing"}: - continue - elif parts[-1].startswith("_"): - continue try: nav[parts] = doc_path.as_posix() @@ -34,5 +34,8 @@ mkdocs_gen_files.set_edit_path(full_doc_path, path) +nav_lines = list(nav.build_literate_nav()) +nav_lines.insert(0, nav_lines.pop(-1)) + with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: - nav_file.writelines(nav.build_literate_nav()) + nav_file.writelines(nav_lines) diff --git a/docs/getting-started/authentication.md b/docs/getting-started/authentication.md index 8d288bb..f58a594 100644 --- a/docs/getting-started/authentication.md +++ b/docs/getting-started/authentication.md @@ -9,7 +9,7 @@ This token is a specific code that is assigned to you and is needed to confirm t Please follow the instructions in the [Authentication](https://developer.wppbav.com/docs/2.x/authentication) section of the Fount API documentation. !!! warning - Do NOT share your API token publicly or with anyone else. That token is tied to your account exclusively. If somebody else needs a token, they should create their own from their account settings. + Do **NOT** share your API token publicly or with anyone else. That token is tied to your account exclusively. If somebody else needs a token, they should create their own from their account settings. ## Recommended way to manage API keys @@ -24,9 +24,12 @@ my-project-folder Create this `.env` file (note the leading dot) in the top level of your working directory, and write down your token like so: ```env -FOUNT_API_KEY = "your_token_here" +BAV_API_KEY = "your_token_here" ``` +!!! tip + The `bavapi-gen-refs` [command](reference-classes.md) also uses the `BAV_API_KEY` variable name. + To now use this file, you will need to install the [`python-dotenv`](https://github.com/theskumar/python-dotenv) package: ```prompt @@ -40,11 +43,11 @@ import os from dotenv import load_dotenv load_dotenv() # (1) -TOKEN = os.environ["FOUNT_API_KEY"] # (2) +TOKEN = os.environ["BAV_API_KEY"] # (2) ``` 1. Load variables from `.env` into the system's environment -2. Assign the `"FOUNT_API_KEY"` environment variable to our `TOKEN` local variable +2. Assign the `"BAV_API_KEY"` environment variable to our `TOKEN` local variable Now you can use `TOKEN` in your API requests: diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index e4ef58c..41f1aae 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -19,7 +19,7 @@ Once you have installed Python and have acquired your Fount API token, return to - `pydantic` to validate query and filter parameters. - `nest-asyncio` to support Jupyter notebooks. - `tqdm` to show helpful progress bars. -- `typing-extensions` for type checking compatibility in Python < 3.10. +- `typing-extensions` for type-checking compatibility in Python < 3.12. These libraries will be installed automatically when you install `bavapi`. diff --git a/docs/getting-started/reference-classes.md b/docs/getting-started/reference-classes.md index 0464df3..cfae3bc 100644 --- a/docs/getting-started/reference-classes.md +++ b/docs/getting-started/reference-classes.md @@ -44,7 +44,7 @@ And could be used to have better visibility when filtering API responses: To generate these reference classes, there are two options for authenticating your requests: - Specify the Fount API token via the `-t`/`--token` argument -- Use a `.env` file to store your Fount API token and install `python-dotenv` to read the file into your environment. See the [Authentication](authentication.md#recommended-way-to-manage-api-keys) section for more info. +- Use a `.env` file to store your Fount API token as the `BAV_API_KEY` environment variable, and install `python-dotenv` to read the file into your environment. See the [Authentication](authentication.md#recommended-way-to-manage-api-keys) section for more info. To generate the reference files, run the following command: @@ -57,13 +57,13 @@ To generate the reference files, run the following command: === "Using the `-t`/`--token` argument" ```prompt - bavapi-gen-refs --all -t your_token + bavapi-gen-refs -t "TOKEN" --all ``` -Alternatively, you can specify the name of the reference class to generate: +You can also specify the name of the reference class to generate: ```prompt -bavapi-gen-refs --name audiences +bavapi-gen-refs -t "TOKEN" --name audiences ``` -To update existing reference classes with the latest data, re-run `bavapi-gen-refs --all` on your terminal. +To update existing reference classes with the latest data, re-run `bavapi-gen-refs` with the appropriate parameters on your terminal. diff --git a/docs/index.md b/docs/index.md index 857a630..22ef107 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -# BAV API Python SDK - `bavapi` +# BAV API Python SDK - `bavapi` documentation [![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) diff --git a/docs/reference/SUMMARY.md b/docs/reference/SUMMARY.md index fcbec80..dc0d2b5 100644 --- a/docs/reference/SUMMARY.md +++ b/docs/reference/SUMMARY.md @@ -1,3 +1,4 @@ +* [sync](sync.md) * [client](client.md) * [exceptions](exceptions.md) * [filters](filters.md) @@ -6,7 +7,3 @@ * [params](parsing/params.md) * [responses](parsing/responses.md) * [query](query.md) -* [reference](reference/index.md) - * [generate_reference](reference/generate_reference.md) -* [sync](sync.md) -* [typing](typing.md) diff --git a/docs/release-notes.md b/docs/release-notes.md index 66caff3..f672d68 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,24 @@ ## Version 0.10 +### Version 0.10.1 (October 17th, 2023) + +#### Fix + +- :warning: (Breaking) Fix `metric_id` param to correct `group_id` name in `bavapi.brand_metric_groups` top level function. + +#### Internal + +- :lock: Renamed `reference` module as private. This will remove it from the code reference docs. +- :recycle: Set new dependency minimum versions for compatibility. + +#### Docs + +- :tada: Code reference section now directs to the `sync` documentation by default. +- :notebook: More documentation for the `sync` and `client` modules. +- :notebook: Added more clarity around expected environment variables when storing API keys. `bavapi` will always look for an API key in the `BAV_API_KEY` environment variable. +- :gear: Refactored code reference generation to support renaming of `reference` module. + ### Version 0.10.0 (October 16th, 2023) #### Feature diff --git a/docs/roadmap.md b/docs/roadmap.md index 990b044..8687faa 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,14 +1,14 @@ # `bavapi` Roadmap -!!! warning - `bavapi` is currently in alpha stage of development. Future updates may break existing functionality while the interface becomes fully mature. +!!! note + As of `v0.10.1`, `bavapi` is in **beta**. New features won't likely be developed until the full release of `bavapi`. This is a non-exhaustive list of potential features & changes to `bavapi` before it is ready for full release: ## Core tooling -- ~~`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`~~ :white_check_mark: `v0.9.0` +- [x] ~~`pydantic` V2 support~~ `v0.6.0` +- [x] ~~Strict `mypy` support with [PEP 692](https://docs.python.org/3.12/whatsnew/3.12.html#whatsnew312-pep692) `Unpack` and `TypedDict`~~ `v0.9.0` ## Known issues @@ -18,13 +18,13 @@ This is a non-exhaustive list of potential features & changes to `bavapi` before Eventually, the plan is to support all endpoints. This is the current priority list: -1. ~~Categories~~ :white_check_mark: `v0.10.0` -2. ~~Collections~~ :white_check_mark: `v0.10.0` -3. ~~Brand Metrics~~ :white_check_mark: `v0.10.0` -4. ~~Sectors~~ :white_check_mark: `v0.10.0` -5. ~~Brand Metric Groups~~ :white_check_mark: `v0.10.0` +- [x] ~~Categories~~ `v0.10.0` +- [x] ~~Collections~~ `v0.10.0` +- [x] ~~Brand Metrics~~ `v0.10.0` +- [x] ~~Sectors~~ `v0.10.0` +- [x] ~~Brand Metric Groups~~ `v0.10.0` ## Stretch goals -- ~~Smarter flattening of JSON responses, possibly through `pandas.json_normalize`.~~ :white_check_mark: `v0.8.1` -- Parse datetime values to `pandas` datetime. +- [x] ~~Smarter flattening of JSON responses, possibly through `pandas.json_normalize`.~~ `v0.8.1` +- [ ] ~~Parse datetime values to `pandas` datetime.~~ `de-scoped` diff --git a/mkdocs.yml b/mkdocs.yml index 49930a3..c6e793f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -site_name: BAV API Python SDK documentation +site_name: BAV API Python SDK site_url: https://wppbav.github.io/bavapi-sdk-python repo_name: wppbav/bavapi-sdk-python @@ -49,7 +49,9 @@ extra: markdown_extensions: - admonition - attr_list + - def_list - md_in_html + - tables - pymdownx.caret - pymdownx.mark - pymdownx.tilde @@ -66,6 +68,8 @@ markdown_extensions: - pymdownx.superfences - pymdownx.tabbed: alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true - toc: permalink: true @@ -89,6 +93,7 @@ nav: plugins: - search + # - social - gen-files: scripts: - docs/gen_ref_pages.py diff --git a/noxfile.py b/noxfile.py index 57621b5..32032e2 100644 --- a/noxfile.py +++ b/noxfile.py @@ -209,14 +209,17 @@ def docs_deploy(session: nox.Session) -> None: def version_tuple(string: str) -> tuple[int, ...]: return tuple(int(i) for i in string.split(".")) - - def get_versions(list_args: tuple[str, ...], rebase: bool = False) -> set[tuple[int, ...]]: + + def get_versions( + list_args: tuple[str, ...], rebase: bool = False + ) -> set[tuple[int, ...]]: if rebase: list_args = tuple(["--rebase"] + list(list_args)) - + out = cast(str, session.run("mike", "list", *list_args, silent=True)) return { - tuple(int(i) for i in v.partition(" ")[0].split(".")) for v in out.splitlines() + tuple(int(i) for i in v.partition(" ")[0].split(".")) + for v in out.splitlines() } if not any( diff --git a/pyproject.toml b/pyproject.toml index b40bc3d..be93ecb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "wpp-bavapi" -version = "0.10.0" +version = "0.10.1" authors = [ { name = "Ignacio Maiz Vilches", email = "ignacio.maiz@bavgroup.com" }, ] @@ -24,7 +24,7 @@ keywords = [ ] license = { text = "Apache 2.0" } classifiers = [ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: Apache Software License', @@ -41,11 +41,11 @@ classifiers = [ ] dependencies = [ "httpx >= 0.20", - "nest-asyncio >= 1.5.6", - "pandas >= 0.16.2", - "pydantic >= 2", - "tqdm >= 4.62", - "typing-extensions >= 4.6; python_version < '3.12'", + "nest-asyncio >= 1.5", + "pandas >= 1.0", + "pydantic >= 2.0", # incompatible with v1 + "tqdm >= 4.62", # consistent asyncio.gather + "typing-extensions >= 4.6; python_version < '3.12'", # PEP-692 Unpack support ] [project.urls] @@ -56,7 +56,7 @@ repository = "https://github.com/wppbav/wpp-bavapi/" dev = ["black", "nox", "pip-tools"] doc = [ "mkdocs", - "mkdocs-material", + "mkdocs-material[imaging]", "mkdocstrings[python]", "mkdocs-gen-files", "mkdocs-literate-nav", @@ -67,7 +67,7 @@ lint = ["isort", "mypy", "pylint", "pandas-stubs"] test = ["coverage", "pytest", "pytest-asyncio", "python-dotenv"] [project.scripts] -bavapi-gen-refs = "bavapi.reference.generate_reference:main" +bavapi-gen-refs = "bavapi._reference.generate_reference:main" [tool.setuptools.packages.find] include = ["bavapi*"] diff --git a/requirements.txt b/requirements.txt index f464f8f..0a15f33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ -httpx>=0.24.1 -nest-asyncio>=1.5.6 -numpy>=1.24.3 -pandas>=2.0.3 +httpx>=0.20 +nest-asyncio>=1.5 +pandas>=1.0 pydantic>=2.0 -tqdm>=4.66.1 +tqdm>=4.62 diff --git a/tests/reference/test_generate_reference.py b/tests/reference/test_generate_reference.py index 9717f93..c9f7597 100644 --- a/tests/reference/test_generate_reference.py +++ b/tests/reference/test_generate_reference.py @@ -8,9 +8,9 @@ import pandas as pd import pytest +from bavapi._reference import generate_reference as uref from bavapi.client import Client from bavapi.query import Query -from bavapi.reference import generate_reference as uref from ..helpers import wraps @@ -66,7 +66,7 @@ def test_generate_source(): '"""Tests class for holding tests IDs."""\n\n' "# This file was generated from active tests in the Fount.\n" f"# Tests retrieved on {TEST_DT.strftime('%Y-%m-%d %H:%M:%S')}\n\n" - "from bavapi.reference._int_enum import IntEnum\n\n\n" + "from bavapi._reference.int_enum import IntEnum\n\n\n" 'class Tests(IntEnum):\n """Tests IDs for Fount API queries."""\n\n' " A = 1\n" ) @@ -77,7 +77,7 @@ def test_generate_source(): ) -@mock.patch("bavapi.reference.generate_reference.Path.exists", return_value=True) +@mock.patch("bavapi._reference.generate_reference.Path.exists", return_value=True) def test_write_to_file(mock_exists: mock.MagicMock): with mock.patch("builtins.open", new_callable=mock.mock_open) as mock_open: uref.write_to_file("", Path("folder/blah.py")) @@ -86,7 +86,7 @@ def test_write_to_file(mock_exists: mock.MagicMock): mock_exists.assert_called_once() -@mock.patch("bavapi.reference.generate_reference.Path.mkdir") +@mock.patch("bavapi._reference.generate_reference.Path.mkdir") def test_write_to_file_not_exists(mock_mkdir: mock.MagicMock): with mock.patch("builtins.open", new_callable=mock.mock_open) as mock_open: uref.write_to_file("", Path("folder/blah.py")) @@ -113,32 +113,32 @@ 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", "") + mock_getenv.assert_called_once_with("BAV_API_KEY", "") @mock.patch( - "bavapi.reference.generate_reference.Client.raw_query", + "bavapi._reference.generate_reference.Client.raw_query", wraps=wraps([{"id": 1, "name": "A"}, {"id": 2, "name": "B"}]), ) -@mock.patch("bavapi.reference.generate_reference.write_to_file") +@mock.patch("bavapi._reference.generate_reference.write_to_file") def test_main(mock_write_to_file: mock.Mock, mock_raw_query: mock.AsyncMock): args = ["-n", "audiences"] with mock.patch( - "bavapi.reference.generate_reference.os.getenv", return_value="test_token" + "bavapi._reference.generate_reference.os.getenv", return_value="test_token" ) as mock_getenv: uref.main(args) mock_write_to_file.assert_called_once() mock_raw_query.assert_awaited_once_with("audiences", Query()) - mock_getenv.assert_called_once_with("FOUNT_API_KEY", "") + mock_getenv.assert_called_once_with("BAV_API_KEY", "") @mock.patch( - "bavapi.reference.generate_reference.Client.raw_query", + "bavapi._reference.generate_reference.Client.raw_query", wraps=wraps( [ {"id": 1, "name": "A", "country_name": "A"}, @@ -146,18 +146,18 @@ def test_main(mock_write_to_file: mock.Mock, mock_raw_query: mock.AsyncMock): ] ), ) -@mock.patch("bavapi.reference.generate_reference.write_to_file") +@mock.patch("bavapi._reference.generate_reference.write_to_file") def test_main_all(mock_write_to_file: mock.Mock, mock_raw_query: mock.AsyncMock): args = ["-a"] with mock.patch( - "bavapi.reference.generate_reference.os.getenv", return_value="test_token" + "bavapi._reference.generate_reference.os.getenv", return_value="test_token" ) as mock_getenv: uref.main(args) assert len(mock_write_to_file.call_args_list) == 2 assert len(mock_raw_query.call_args_list) == 2 - mock_getenv.assert_called_once_with("FOUNT_API_KEY", "") + mock_getenv.assert_called_once_with("BAV_API_KEY", "") @mock.patch("dotenv.load_dotenv", wraps=wraps(raises=ImportError)) @@ -169,17 +169,17 @@ def test_main_no_token_no_dotenv(mock_load_dotenv: mock.Mock): assert excinfo.value.args == ( "You must specify a Fount API token with the `-t`/`--token` argument, " - "or install `python-dotenv` and set `FOUNT_API_KEY` in a `.env` file.", + "or install `python-dotenv` and set `BAV_API_KEY` in a `.env` file.", ) mock_load_dotenv.assert_called_once() @mock.patch( - "bavapi.reference.generate_reference.Client.raw_query", + "bavapi._reference.generate_reference.Client.raw_query", wraps=wraps([{"id": 1, "name": "A"}, {"id": 2, "name": "B"}]), ) @mock.patch("dotenv.load_dotenv", wraps=wraps(raises=ImportError)) -@mock.patch("bavapi.reference.generate_reference.write_to_file") +@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, @@ -188,11 +188,11 @@ def test_main_with_token_arg( args = ["-n", "audiences", "-t", "test_token"] with mock.patch( - "bavapi.reference.generate_reference.os.getenv", return_value="test_token" + "bavapi._reference.generate_reference.os.getenv", return_value="test_token" ) as mock_getenv: uref.main(args) mock_write_to_file.assert_called_once() mock_load_dotenv.assert_not_called() - mock_getenv.assert_called_once_with("FOUNT_API_KEY", "test_token") + mock_getenv.assert_called_once_with("BAV_API_KEY", "test_token") mock_raw_query.assert_awaited_once_with("audiences", Query()) diff --git a/tests/reference/test_int_enum.py b/tests/reference/test_int_enum.py index cd78737..2363970 100644 --- a/tests/reference/test_int_enum.py +++ b/tests/reference/test_int_enum.py @@ -1,6 +1,6 @@ # pylint: disable=missing-class-docstring, missing-module-docstring, missing-function-docstring -from bavapi.reference._int_enum import IntEnum +from bavapi._reference.int_enum import IntEnum def test_int_enum_str(): diff --git a/tests/test_integration.py b/tests/test_integration.py index 3436c9c..79812f4 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -26,7 +26,7 @@ def test_raw_query(): for _ in range(5): # pragma: no cover try: result = bavapi.raw_query( - os.environ["FOUNT_API_KEY"], + os.environ["BAV_API_KEY"], "countries", Query(filters={"is_active": 1}, include="region", max_pages=2), ) @@ -46,7 +46,7 @@ def test_with_filters_one_page(): for _ in range(5): # pragma: no cover try: result = bavapi.studies( - os.environ["FOUNT_API_KEY"], + os.environ["BAV_API_KEY"], filters=filters.StudiesFilters(active=1), include="country", page=1, @@ -82,7 +82,7 @@ def test_endpoints(endpoint: str, filters: Dict[str, Any]): for _ in range(5): # pragma: no cover try: result = func( - os.environ["FOUNT_API_KEY"], filters=filters, max_pages=2, per_page=25 + os.environ["BAV_API_KEY"], filters=filters, max_pages=2, per_page=25 ) break except ssl.SSLError: