Skip to content

Commit

Permalink
fix factories and fixtures
Browse files Browse the repository at this point in the history
  • Loading branch information
jrycw committed Mar 6, 2024
1 parent 8e44e65 commit e3a5527
Show file tree
Hide file tree
Showing 19 changed files with 474 additions and 243 deletions.
41 changes: 0 additions & 41 deletions app/_fixtures.py

This file was deleted.

36 changes: 36 additions & 0 deletions app/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import datetime
import random

from faker import Faker
from pydantic import BaseModel, Field

faker = Faker()


def gen_event():
"""To address randomness, we need to make this special fixture-like function."""

class FakeEventData(BaseModel):
"""
The "address" and "schedule" fields are optional,
meaning they can be set to `None`, and adding randomness
may be beneficial for testing purposes.
"""

name: str = Field(default_factory=lambda: faker.text(max_nb_chars=20))
address: str | None = Field(
default_factory=random.choice([faker.street_address, lambda: None])
)
schedule: str | None = Field(
default_factory=random.choice(
[
lambda: faker.future_datetime(tzinfo=datetime.timezone.utc)
.replace(microsecond=random.randint(0, 999999))
.isoformat(),
lambda: None,
]
)
)
host_name: str = Field(default_factory=faker.name, max_length=50)

return FakeEventData()
37 changes: 37 additions & 0 deletions app/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import json
from http import HTTPStatus
from typing import Annotated

import svcs
from edgedb.asyncio_client import AsyncIOClient
from fastapi import APIRouter, Body

from .factories import gen_event
from .models import DevDataCreate
from .queries import create_event_async_edgeql as create_event_qry
from .queries import prepare_dev_data_async_edgeql as prepare_dev_data_qry

router = APIRouter(include_in_schema=False)


@router.post(
"/fixtures",
status_code=HTTPStatus.OK,
response_model=list[create_event_qry.CreateEventResult],
tags=["fixtures"],
)
async def prepare_dev_data(
services: svcs.fastapi.DepContainer,
dev_data: Annotated[DevDataCreate, Body()] = DevDataCreate(),
):
"""
The endpoint is enabled for dev purpose only.
`prepare_dev_data_qry.prepare_dev_data` will **DELETE ALL USERS AND EVENTS**!!!
Well, after all, this is a setup for dev.
"""
client = await services.aget(AsyncIOClient)
data = [
gen_event().model_dump(include={"name", "address", "schedule", "host_name"})
for _ in range(dev_data.n)
]
return await prepare_dev_data_qry.prepare_dev_data(client, data=json.dumps(data))
25 changes: 18 additions & 7 deletions app/lifespan.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,43 @@
import svcs
from edgedb.asyncio_client import AsyncIOClient
from fastapi import FastAPI
from httpx import AsyncClient # noqa: F401

from ._fixtures import add_events, add_users
from .config import settings
from .queries import ping_db_async_edgeql as ping_db_qry


async def _lifespan(app: FastAPI, registry: svcs.Registry, *, prefill: bool):
# EdgeDB client
db_client = edgedb.create_async_client()

async def create_db_client():
"""only 1 db_client"""
yield db_client

async def ping_db_callable(_db_client):
return await _db_client.query("select 1;")
return ping_db_qry

registry.register_factory(
AsyncIOClient,
create_db_client,
ping=ping_db_callable,
)

# Add users and events for dev
if prefill:
await add_users(db_client)
await add_events(db_client)
# Web client
# if prefill:
# http_client = AsyncClient(
# base_url=f"{settings.backend_schema}://{settings.backend_host}:{settings.backend_port}"
# )

# async def create_http_client():
# yield http_client

# async def ping_http_callable(_http_client):
# return lambda _http_client: _http_client.get("/")

# registry.register_factory(
# AsyncClient, create_http_client, ping=ping_http_callable
# )

yield

Expand Down
9 changes: 8 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# import sys # noqa: F401
# sys.path.append(os.getcwd())
# import uvicorn # noqa: F401
import httpx # noqa: F401
from asgi_correlation_id import CorrelationIdMiddleware
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware

from app import common, events, health, users
from app import common, events, fixtures, health, users # noqa: F401
from app.config import settings
from app.lifespan import lifespan
from app.logging import setup_logging
Expand Down Expand Up @@ -36,6 +37,12 @@ def make_app(lifespan):
app.include_router(events.router)
app.include_router(health.router)
app.include_router(common.router)
app.include_router(fixtures.router)

# base_url = f"{settings.backend_schema}://{settings.backend_host}:{settings.backend_port}"
# with httpx.Client(base_url=base_url) as client:
# r = client.post("/fixtures", json={})
# print(f"{r=}")

return app

Expand Down
7 changes: 7 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,10 @@ class EventFull(Auditable, EventCreate, EventID):
################################
class HealthOut(BaseModel):
ok: list[str] = Field(default_factory=list)


################################
# Dev data
################################
class DevDataCreate(BaseModel):
n: int = 5
1 change: 1 addition & 0 deletions app/queries/ping_db.edgeql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
select 1;
16 changes: 16 additions & 0 deletions app/queries/ping_db_async_edgeql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# AUTOGENERATED FROM 'app/queries/ping_db.edgeql' WITH:
# $ edgedb-py


from __future__ import annotations
import edgedb


async def ping_db(
executor: edgedb.AsyncIOExecutor,
) -> int:
return await executor.query_single(
"""\
select 1;\
""",
)
11 changes: 11 additions & 0 deletions app/queries/prepare_dev_data.edgeql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
delete Event;
delete User;
with data := <json>$data,
for item in json_array_unpack(data) union (
select (insert Event { name := <str>item['name'],
address := <optional str>item['address'] ?? <str>{},
schedule := <datetime>(<optional str>item['schedule'] ?? <str>{}),
host := (insert User {name:= <str>item['host_name']})
}
) {name, address, schedule, host_name:=.host.name}
);
57 changes: 57 additions & 0 deletions app/queries/prepare_dev_data_async_edgeql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# AUTOGENERATED FROM 'app/queries/prepare_dev_data.edgeql' WITH:
# $ edgedb-py


from __future__ import annotations
import dataclasses
import datetime
import edgedb
import uuid


class NoPydanticValidation:
@classmethod
def __get_pydantic_core_schema__(cls, _source_type, _handler):
# Pydantic 2.x
from pydantic_core.core_schema import any_schema
return any_schema()

@classmethod
def __get_validators__(cls):
# Pydantic 1.x
from pydantic.dataclasses import dataclass as pydantic_dataclass
pydantic_dataclass(cls)
cls.__pydantic_model__.__get_validators__ = lambda: []
return []


@dataclasses.dataclass
class PrepareDevDataResult(NoPydanticValidation):
id: uuid.UUID
name: str
address: str | None
schedule: datetime.datetime | None
host_name: str


async def prepare_dev_data(
executor: edgedb.AsyncIOExecutor,
*,
data: str,
) -> list[PrepareDevDataResult]:
return await executor.query(
"""\
delete Event;
delete User;
with data := <json>$data,
for item in json_array_unpack(data) union (
select (insert Event { name := <str>item['name'],
address := <optional str>item['address'] ?? <str>{},
schedule := <datetime>(<optional str>item['schedule'] ?? <str>{}),
host := (insert User {name:= <str>item['host_name']})
}
) {name, address, schedule, host_name:=.host.name}
);\
""",
data=data,
)
67 changes: 32 additions & 35 deletions fastui_app/lifespan.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

from .clients import (
BackendAsyncClient,
FrontendGetAsyncClient,
FrontendPostPutDeleteAsyncClient,
FrontendGetAsyncClient, # noqa: F401
FrontendPostPutDeleteAsyncClient, # noqa: F401
)
from .config import settings

Expand All @@ -18,39 +18,36 @@ async def creat_backend_client():
"""1 backend web client"""
yield backend_client

registry.register_factory(
BackendAsyncClient,
creat_backend_client,
)

front_get_client = BackendAsyncClient(
base_url=f"{settings.frontend_schema}://{settings.frontend_host}:{settings.frontend_port}"
)

async def create_frontend_get_client():
"""1 frontent web GET client"""
yield front_get_client

registry.register_factory(
FrontendGetAsyncClient,
create_frontend_get_client,
)

async def create_frontend_post_put_delete_client():
"""For every post/put/delete, we request 1 specialized web client"""
base_url = f"{settings.frontend_schema}://{settings.frontend_host}:{settings.frontend_port}"
async with FrontendPostPutDeleteAsyncClient(base_url=base_url) as client:
csrftoken = (await client.get("/")).cookies.get("csrftoken")
# extra_headers = (
# {"headers": {"x-csrftoken": csrftoken}} if csrftoken is not None else {}
# )
csrftoken_dict = {"x-csrftoken": csrftoken} if csrftoken is not None else {}
yield client, csrftoken_dict

registry.register_factory(
FrontendPostPutDeleteAsyncClient,
create_frontend_post_put_delete_client,
)
registry.register_factory(BackendAsyncClient, creat_backend_client)

# frontend_get_client = FrontendGetAsyncClient(
# base_url=f"{settings.frontend_schema}://{settings.frontend_host}:{settings.frontend_port}"
# )

# async def create_frontend_get_client():
# """1 frontent web GET client"""
# yield frontend_get_client

# registry.register_factory(
# FrontendGetAsyncClient,
# create_frontend_get_client,
# )

# async def create_frontend_post_put_delete_client():
# """For every post/put/delete, we request 1 specialized web client"""
# base_url = f"{settings.frontend_schema}://{settings.frontend_host}:{settings.frontend_port}"
# async with FrontendPostPutDeleteAsyncClient(base_url=base_url) as client:
# csrftoken = (await client.get("/")).cookies.get("csrftoken")
# # extra_headers = (
# # {"headers": {"x-csrftoken": csrftoken}} if csrftoken is not None else {}
# # )
# csrftoken_dict = {"x-csrftoken": csrftoken} if csrftoken is not None else {}
# yield client, csrftoken_dict

# registry.register_factory(
# FrontendPostPutDeleteAsyncClient,
# create_frontend_post_put_delete_client,
# )

yield

Expand Down
Loading

0 comments on commit e3a5527

Please sign in to comment.