Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix list footprints route #20

Merged
merged 8 commits into from
Mar 15, 2024
12 changes: 9 additions & 3 deletions apis/version1/route_product_footprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ def list_footprints(db: Session = Depends(get_db), current_user: User = Depends(
release needs to happen sooner rather than later.
"""
product_footprints = list_product_footprints(db=db)
if not product_footprints or len(product_footprints) == 0:
raise NoSuchFootprintException

return paginate(product_footprints, transformer=transformer)


def transformer(product_footprints):
product_footprint_schemas: list[ProductFootprintSchema] = []
for product_footprint in product_footprints:
secondary_emission_factor_sources: list[EmissionFactorDS] = []
Expand Down Expand Up @@ -197,6 +204,5 @@ def list_footprints(db: Session = Depends(get_db), current_user: User = Depends(
extensions=product_footprint.extensions
)
product_footprint_schemas.append(product_footprint_schema)
if not product_footprints:
raise NoSuchFootprintException
return paginate(product_footprint_schemas)

return product_footprint_schemas
3 changes: 1 addition & 2 deletions core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ class Settings:
"PGPORT", 5432
) # default postgres port is 5432
POSTGRES_DB: str = os.getenv("PGDATABASE")
DATABASE_URL = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_SERVER}:{POSTGRES_PORT}/{POSTGRES_DB}"

DATABASE_URL = os.getenv("DATABASE_URL")
SECRET_KEY: str = os.getenv("SECRET_KEY")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # in mins
Expand Down
50 changes: 10 additions & 40 deletions core/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
from typing import Any, Generic, Optional, Sequence, TypeVar
from typing_extensions import Self

from fastapi import Query
from fastapi_pagination.bases import AbstractPage, AbstractParams, RawParams
from pydantic import BaseModel
from fastapi_pagination.bases import AbstractPage
from fastapi_pagination.limit_offset import OptionalLimitOffsetParams
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request

Expand All @@ -24,30 +23,6 @@
"""


class JSONAPIParams(BaseModel, AbstractParams):
"""
Defines pagination parameters compliant with the JSON:API specification.
Inherits from AbstractParams (provided by the 'fastapi-pagination' library).

Attributes:
offset (int): Starting index for data retrieval (defaults to 1, minimum 1).
limit (int): Maximum number of items to fetch per page (defaults to 10, between 1 and 100).

Testing Notes:
* Test this model directly with unit tests. Verify that different
combinations of 'offset' and 'limit' are constructed and validated correctly.
"""
offset: int = Query(1, ge=1)
limit: int = Query(10, ge=1, le=100)

def to_raw_params(self) -> RawParams:
"""
Converts JSONAPIParams into a simpler 'RawParams' format used internally
by the pagination library.
"""
return RawParams(limit=self.limit, offset=self.offset)


T = TypeVar("T")


Expand All @@ -69,34 +44,28 @@ class JSONAPIPage(AbstractPage[T], Generic[T]):
* If/when 'meta' attributes are implemented, ensure they are serialized as expected.
"""
data: Sequence[T]
meta: dict[str, int] = {}

__params_type__ = JSONAPIParams
__params_type__ = OptionalLimitOffsetParams

@classmethod
def create(
cls,
items: Sequence[T],
params: AbstractParams,
params: OptionalLimitOffsetParams = None,
*,
total: Optional[int] = None,
**kwargs: Any,
) -> Self:
assert isinstance(params, JSONAPIParams)
assert isinstance(params, OptionalLimitOffsetParams)
assert total is not None

page_data = cls._get_page_data(items=items, params=params)

return cls(
data=page_data,
data=items,
meta={"total": total},
**kwargs,
)

@staticmethod
def _get_page_data(*, items: Sequence[T], params: JSONAPIParams) -> Sequence[T]:
start_index: int = params.offset - 1
finish_index: int = start_index + params.limit
return items[start_index: finish_index]


class PaginationMiddleware(BaseHTTPMiddleware):
"""
Expand All @@ -121,7 +90,8 @@ async def dispatch(self, request: Request, call_next):
product_footprint_count = count_product_footprints(db=db)

limit = request.query_params.get('limit')
offset = request.query_params.get('offset', 1)
print(f"we have a limit here: {limit}")
offset = request.query_params.get('offset')
if limit:
# Get current URL, adjust for next page
http_url = request.url
Expand Down
4 changes: 3 additions & 1 deletion db/repository/product_footprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,10 @@ def retrieve_product_footprint(id: str, db: Session):
return item


def list_product_footprints(db: Session):
def list_product_footprints(db: Session) -> list[ProductFootprintModel] | None:
product_footprints = db.query(ProductFootprintModel).all()
if len(product_footprints) == 0:
return None
return product_footprints


Expand Down
12 changes: 6 additions & 6 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ readme = "README.md"

[tool.poetry.dependencies]
python = "^3.10"
fastapi-pagination = "^0.12.4"
fastapi-pagination = "^0.12.19"
fastapi = {version = "0.110.0", extras = ["all"]}
SQLAlchemy = "^1.4.41"
pydantic = {extras = ["email"], version = "2.6.3"}
Expand Down
1 change: 1 addition & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[pytest]
env =
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=yourpasswordhere
POSTGRES_SERVER=localhost
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def start_application():

SQLALCHEMY_DATABASE_URL = "postgresql://postgres:postgres@localhost:5432/postgres"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, echo=True, future=True
SQLALCHEMY_DATABASE_URL, future=True
)
# Use connect_args parameter only with sqlite
SessionTesting = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Expand Down
89 changes: 0 additions & 89 deletions tests/test_core/test_pagination.py

This file was deleted.

10 changes: 10 additions & 0 deletions tests/test_repository/test_product_footprints_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ def test_retrieve_product_footprint_not_found(db_session):
assert item is None


def test_list_product_footprints_empty_returns_none(db_session):
"""
Tests that `list_product_footprints` returns the correct number of results
and that the data is as expected.
"""
product_footprints = list_product_footprints(db_session)

assert product_footprints is None


@pytest.mark.parametrize(
"product_footprint_id",
["90163d8f-8465-4a6f-9e43-a58d68bef72f", "80b79a90-0dbc-48d0-b910-551c09037d61"]) # Extract IDs from fixture
Expand Down
41 changes: 32 additions & 9 deletions tests/test_routes/test_product_footprints_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ def valid_json_product_footprint(valid_carbon_footprint_data):
"pcf": valid_carbon_footprint_data
}

@pytest.fixture()
def seed_database(client, auth_header, valid_json_product_footprint ,num_footprints=5):
@pytest.fixture(scope="function")
def seed_database(client, auth_header, valid_json_product_footprint, num_footprints=5):
"""Seeds the database with a specified number of product footprints.

Args:
Expand Down Expand Up @@ -75,17 +75,40 @@ def test_read_product_footprint(client, auth_header):
assert response.json()["data"]["companyName"] == "Clean Product Company"


"""
This tests is currently succeeding but only because the create-product-footprint
calls are failing and so there's nothing in there, the API does actually work, but
this test does not, come back to this once the data model is in better shape.
"""
def test_read_product_footprints(client, auth_header, seed_database):
def test_read_product_footprints_no_seeding(client, auth_header):
response = client.get("/footprints/?limit=1", headers=auth_header)
assert response.status_code == 200
print(f"data = {response.json()['data']}")
assert len(response.json()["data"]) == 1

response = client.get("/footprints/?offset=1&limit=10", headers=auth_header)
def test_read_product_footprints(client, auth_header, seed_database):
response = client.get("/footprints/?offset=1", headers=auth_header)
assert response.status_code == 200
print(f"data = {response.json()['data']}")
assert len(response.json()["data"]) == 5
assert response.json()
assert response.json()["data"][1]
assert response.json()["data"][4]

def test_read_product_footprints_with_offset(client, auth_header, seed_database):
response = client.get("/footprints/", headers=auth_header)
assert response.status_code == 200
print(f"data = {response.json()['data']}")
assert len(response.json()["data"]) == 11
assert response.json()
assert response.json()["data"][1]
assert response.json()["data"][2]


def test_read_product_footprints_with_limit(client, auth_header, seed_database):
response = client.get("/footprints/?limit=3", headers=auth_header)
assert response.status_code == 200
print(f"data = {response.json()['data']}")
assert len(response.json()["data"]) == 3
assert response.json()
assert response.json()["data"][1]
assert response.json()["data"][2]


# add a test for bad requests, when i hit "/footprints/?limit=50, it returned a NoneType
# error for the product_footprint call with has no attribute, need to catch that better
Loading