Skip to content

Commit

Permalink
Merge pull request #5 from wppbav/dev-pydantic-v2
Browse files Browse the repository at this point in the history
Pydantic v2 upgrade
  • Loading branch information
Nacho Maiz authored Jul 13, 2023
2 parents a8302d9 + 2284aa7 commit 3ac85e1
Show file tree
Hide file tree
Showing 28 changed files with 289 additions and 194 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: release

on:
release:
types: [released]
types: [published]

jobs:
pypi-publish:
Expand Down
10 changes: 5 additions & 5 deletions bavapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
... result = await client.brands(name="Facebook")
"""

from importlib.metadata import version, PackageNotFoundError
from importlib.metadata import PackageNotFoundError, version

from bavapi import filters
from bavapi.client import Client
Expand All @@ -28,20 +28,20 @@
from bavapi.sync import audiences, brands, brandscape_data, raw_query, studies

__all__ = (
"raw_query",
"audiences",
"brands",
"brandscape_data",
"raw_query",
"studies",
"Client",
"Query",
"filters",
"APIError",
"DataNotFoundError",
"RateLimitExceededError",
"Query",
"filters",
)

try:
__version__ = version(__package__ or __name__)
except PackageNotFoundError: # pragma: no cover
pass
__version__ = "not_found"
12 changes: 10 additions & 2 deletions bavapi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
List,
Literal,
Optional,
Type,
TypeVar,
Union,
overload,
Expand All @@ -20,6 +21,8 @@
from bavapi.typing import BaseListOrValues, JSONDict, OptionalListOr

if TYPE_CHECKING:
from types import TracebackType

from pandas import DataFrame

__all__ = ("Client",)
Expand Down Expand Up @@ -149,8 +152,13 @@ async def __aenter__(self) -> "Client":
await self._client.__aenter__()
return self

async def __aexit__(self, *args, **kwargs):
await self._client.__aexit__(*args, **kwargs)
async def __aexit__(
self,
exc_type: Optional[Type[BaseException]] = None,
exc_value: Optional[BaseException] = None,
traceback: "Optional[TracebackType]" = None,
) -> None:
await self._client.__aexit__(exc_type, exc_value, traceback)

async def aclose(self) -> None:
"""Close existing HTTP connections."""
Expand Down
1 change: 1 addition & 0 deletions bavapi/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Exceptions for handling errors with the Fount API."""


class APIError(Exception):
"""Exception for errors interacting with APIs."""

Expand Down
16 changes: 8 additions & 8 deletions bavapi/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing import Dict, Literal, Mapping, Optional, Type, TypeVar, Union

from pydantic import BaseModel, root_validator, validator
from pydantic import BaseModel, field_validator, model_validator

from bavapi.parsing.params import parse_date
from bavapi.typing import (
Expand Down Expand Up @@ -41,12 +41,12 @@ class FountFilters(BaseModel):
the response data.
"""

updated_since: DTValues = None
# Allow arbitrary filters for compatibility with raw_query
model_config = {"extra": "allow"}

class Config: #pylint: disable=missing-class-docstring
extra = "allow"
updated_since: DTValues = None

@validator("updated_since", pre=True)
@field_validator("updated_since", mode="before")
@classmethod
def _parse_date(cls, value: DTValues) -> Optional[str]:
if value is None:
Expand Down Expand Up @@ -87,7 +87,7 @@ def ensure(
if isinstance(filters, Mapping):
new_filters.update(filters)
else:
new_filters.update(filters.dict(exclude_defaults=True))
new_filters.update(filters.model_dump(exclude_defaults=True))

return cls(**new_filters) # type: ignore[arg-type]

Expand Down Expand Up @@ -234,7 +234,7 @@ class BrandscapeFilters(FountFilters):
brands: OptionalListOr[int] = None
categories: OptionalListOr[int] = None

@root_validator(pre=True)
@model_validator(mode="before")
@classmethod
def _check_params(cls, values: Dict[str, object]) -> Dict[str, object]:
if not (
Expand Down Expand Up @@ -312,7 +312,7 @@ class StudiesFilters(FountFilters):
countries: OptionalListOr[int] = None
regions: OptionalListOr[int] = None

@validator("data_updated_since", pre=True)
@field_validator("data_updated_since", mode="before")
@classmethod
def _parse_date(cls, value: DTValues) -> Optional[str]:
if value is None:
Expand Down
14 changes: 12 additions & 2 deletions bavapi/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
import math
from json import JSONDecodeError
from typing import (
TYPE_CHECKING,
Dict,
Iterator,
List,
Optional,
Protocol,
Type,
TypeVar,
Union,
cast,
Expand All @@ -23,6 +25,9 @@
from bavapi.exceptions import APIError, DataNotFoundError, RateLimitExceededError
from bavapi.typing import BaseParamsMapping, JSONData, JSONDict

if TYPE_CHECKING:
from types import TracebackType

__all__ = ("HTTPClient",)


Expand Down Expand Up @@ -108,8 +113,13 @@ async def __aenter__(self: C) -> C:
await self.client.__aenter__()
return self

async def __aexit__(self, *args, **kwargs) -> None:
await self.client.__aexit__(*args, **kwargs)
async def __aexit__(
self,
exc_type: Optional[Type[BaseException]] = None,
exc_value: Optional[BaseException] = None,
traceback: "Optional[TracebackType]" = None,
) -> None:
await self.client.__aexit__(exc_type, exc_value, traceback)

async def aclose(self) -> None:
"""Asynchronously close all client connections."""
Expand Down
15 changes: 9 additions & 6 deletions bavapi/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
BaseMutableParamsMapping,
BaseParamsDict,
BaseParamsDictValues,
BaseParamsMapping,
OptionalListOr,
)

Expand Down Expand Up @@ -76,18 +77,20 @@ def to_params(self, endpoint: str) -> BaseParamsDictValues:
"""
exclude: Final[Set[str]] = {"filters", "fields", "max_pages"}

filters: BaseParamsDict = {}
filters: BaseParamsMapping = {}
fields: BaseMutableParamsMapping = {}

if isinstance(self.filters, _filters.FountFilters):
filters = self.filters.dict(by_alias=True, exclude_defaults=True)
filters = self.filters.model_dump(by_alias=True, exclude_defaults=True)
elif self.filters is not None:
filters = cast(BaseParamsDict, self.filters)
filters = to_fount_params(filters, "filter")
fields = to_fount_params(
{endpoint: self.fields} if self.fields else fields, "fields"
)

params = {
**self.dict(exclude=exclude, by_alias=True, exclude_defaults=True),
**self.model_dump(exclude=exclude, by_alias=True, exclude_defaults=True),
**filters,
**fields,
}
Expand All @@ -114,12 +117,12 @@ def with_page(self, page: int, per_page: int) -> "Query[F]":
if self.page and self.per_page:
return self

return self.__class__.construct(
self.__fields_set__.union({"page", "per_page"}),
return self.__class__.model_construct(
self.model_fields_set.union({"page", "per_page"}),
page=self.page or page,
per_page=self.per_page or per_page,
filters=self.filters, # avoid turning filters into dictionary
**self.dict(
**self.model_dump(
by_alias=True,
exclude={"page", "per_page", "filters"},
exclude_defaults=True,
Expand Down
2 changes: 1 addition & 1 deletion docs/endpoints/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ sidebar_label: Overview

# Endpoints

As of `v0.5`, there are four endpoints that have been fully implemented in `bavapi`:
As of `v0.6`, there are four endpoints that have been fully implemented in `bavapi`:

- [`audiences`](audiences.md)
- [`brands`](brands.md)
Expand Down
20 changes: 1 addition & 19 deletions docs/extra.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,4 @@ a.autorefs-external::after {

a.autorefs-external:hover::after {
background-color: var(--md-accent-fg-color);
}
/*
#logo_light_mode {
display: var(--md-footer-logo-light-mode);
}
#logo_dark_mode {
display: var(--md-footer-logo-dark-mode);
}
[data-md-color-scheme="light-mode"] {
--md-footer-logo-dark-mode: none;
--md-footer-logo-light-mode: block;
}
[data-md-color-scheme="dark-mode"] {
--md-footer-logo-dark-mode: block;
--md-footer-logo-light-mode: none;
} */
}
2 changes: 1 addition & 1 deletion docs/getting-started/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ TOKEN = os.environ["FOUNT_API_TOKEN"] # (2)
```

1. Load variables from `.env` into the system's environment
2. Assign "FOUNT_API_TOKEN" environment variable to `TOKEN`
2. Assign the `"FOUNT_API_TOKEN"` environment variable to our `TOKEN` local variable

Now you can use `TOKEN` in your API requests:

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/reference-classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ These classes are automatically generated by a console command that becomes avai
!!! info "Protected Access"
A Fount API token is required to generate reference files. See the [Authentication](authentication.md) section for more information and instructions for using `.env` files.

As of `v0.5` the following reference classes will be generated in a folder named `bavapi_refs`:
As of `v0.6` the following reference classes will be generated in a folder named `bavapi_refs`:

- `Audiences`: encodes audience IDs
- `Countries`: encodes country IDs
Expand Down
5 changes: 0 additions & 5 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
---
hide:
- navigation
---

# BAV API Python SDK - `bavapi`

[![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)
Expand Down
14 changes: 14 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Release Notes

## Version 0.6

### Version 0.6.0 (July 13th, 2023)

#### Internal

- :rocket: Upgraded [`pydantic`](https://pypi.org/project/pydantic/) to v2. Use `bavapi` v0.5 for compatibility with `pydantic` v1.

#### Typing

- :bug: Fixed use of `type` in type hints not compatible with Python. 3.8
- :broom: Cleaned up type hints in tests.
22 changes: 22 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# `bavapi` Roadmap

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:
- Strict `mypy` support with [PEP 692](https://docs.python.org/3.12/whatsnew/3.12.html#whatsnew312-pep692) `Unpack` and `TypedDict`

## New fully-supported endpoints

Eventually, the plan is to support all endpoints. This is the current priority list:

1. Categories
2. Collections
3. Brand Metrics
4. Sectors
5. Brand Metric Groups

## Stretch goals

- Smarter flattening of JSON responses, possibly through `pandas.json_normalize`.
50 changes: 50 additions & 0 deletions docs/usage/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,35 @@ These functions will return a list of JSON dictionaries, one for each entry retr
!!! tip
These methods are meant to be used for custom processing of data (not resulting in a `pandas` DataFrame), but it is also possible to use some of the parsing functions available in [bavapi.parsing.responses][parsing.responses].

## The `Query` class

[`bavapi.Query`][query.Query] is a `pydantic`-powered class that holds and validates all the common (aside from endpoint-specific filters) query parameters to pass to the Fount API.

The default values for the class are the same as the default values in the Fount API itself, so an empty `Query` object can be used to get all entries for a specific endpoint:

```py
query = bavapi.Query()

async with bavapi.Client("TOKEN") as fount:
res = fount.raw_query("brand-metrics", query) # (1)
```

1. :material-expand-all: Returns all entries for `brand-metrics`. Similar to making a `GET` request with no parameters.

`Query` can be used to set limits on the number of pages retrieved, or to request a specific page from a query:

```py
bavapi.Query(
per_page = 200, # (1)
max_pages = 50,
... # Other params
)
```

1. !!! tip "Stick with defaults"

The default `per_page` value (`100`) has been set after testing various options for the best download speed. :rocket:

### `Query` parameters

All Fount queries performed with [`bavapi.Query`][query.Query] support the following parameters:
Expand All @@ -92,3 +121,24 @@ All Fount queries performed with [`bavapi.Query`][query.Query] support the follo
- `updated_since`: Only return items that have been updated since this timestamp.

For more information on the behavior of each of these parameters, see the [Fount API docs](https://developer.wppbav.com/docs/2.x/customizing/fields).

### Raw parameter dictionary

The `to_params` method can be used to parse the parameters into a dictionary of what will be sent to the Fount API:

```py
>>> bavapi.Query(
... filters=BrandscapeFilters(
... brand_name="Facebook",
... year_numbers=[2012, 2013, 2014, 2015]
... ),
... include=["company"]
... ).to_params(endpoint="brandscape-data")
{
"include[brandscape-data]": "company", # (1)
"filter[brand_name]": "Facebook",
"year_numbers": "2012,2013,2014,2015",
}
```

1. :bulb: Parses `filters` and `include` into the correct format for the Fount API, and parses all elements in lists of parameters to their string representation.
Loading

0 comments on commit 3ac85e1

Please sign in to comment.