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

[client] add dynamic endpoints support #88

Merged
merged 3 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions bin/qcc
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash

set -eo pipefail

declare DOCKER_USER
DOCKER_UID="$(id -u)"
DOCKER_GID="$(id -g)"
DOCKER_USER="${DOCKER_UID}:${DOCKER_GID}"

DOCKER_USER=${DOCKER_USER} \
DOCKER_UID=${DOCKER_UID} \
DOCKER_GID=${DOCKER_GID} \
docker compose run --rm \
client \
qcc "$@"
4 changes: 4 additions & 0 deletions src/api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to

## [Unreleased]

### Changed

- API dynamique bulk requests now returns the number of created items

## [0.9.0] - 2024-06-11

### Added
Expand Down
16 changes: 13 additions & 3 deletions src/api/qualicharge/api/v1/routers/dynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from annotated_types import Len
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Security
from fastapi import status as fa_status
from pydantic import PastDatetime, StringConstraints
from pydantic import BaseModel, PastDatetime, StringConstraints
from sqlalchemy import func
from sqlalchemy.schema import Column as SAColumn
from sqlmodel import Session, join, select
Expand Down Expand Up @@ -44,6 +44,12 @@
]


class DynamiqueItemsCreatedResponse(BaseModel):
"""API response model used when dynamic items are created."""

size: int


@router.get("/status/", tags=["Status"])
async def list_statuses(
user: Annotated[User, Security(get_user, scopes=[ScopesEnum.DYNAMIC_READ.value])],
Expand Down Expand Up @@ -319,7 +325,7 @@ async def create_status_bulk(
user: Annotated[User, Security(get_user, scopes=[ScopesEnum.DYNAMIC_CREATE.value])],
statuses: BulkStatusCreateList,
session: Session = Depends(get_session),
) -> None:
) -> DynamiqueItemsCreatedResponse:
"""Create a statuses batch."""
for status in statuses:
if not is_pdc_allowed_for_user(status.id_pdc_itinerance, user):
Expand Down Expand Up @@ -359,6 +365,8 @@ async def create_status_bulk(
session.add_all(db_statuses)
session.commit()

return DynamiqueItemsCreatedResponse(size=len(db_statuses))


@router.post("/session/", status_code=fa_status.HTTP_201_CREATED, tags=["Session"])
async def create_session(
Expand Down Expand Up @@ -395,7 +403,7 @@ async def create_session_bulk(
user: Annotated[User, Security(get_user, scopes=[ScopesEnum.DYNAMIC_CREATE.value])],
sessions: BulkSessionCreateList,
db_session: Session = Depends(get_session),
) -> None:
) -> DynamiqueItemsCreatedResponse:
"""Create a sessions batch."""
for session in sessions:
if not is_pdc_allowed_for_user(session.id_pdc_itinerance, user):
Expand Down Expand Up @@ -433,3 +441,5 @@ async def create_session_bulk(
db_qc_sessions.append(db_qc_session)
db_session.add_all(db_qc_sessions)
db_session.commit()

return DynamiqueItemsCreatedResponse(size=len(db_qc_sessions))
8 changes: 4 additions & 4 deletions src/api/tests/api/v1/routers/test_dynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,7 @@ def test_create_status_bulk_for_superuser(db_session, client_auth):
json=[json.loads(s.model_dump_json()) for s in qc_statuses],
)
assert response.status_code == status.HTTP_201_CREATED
assert response.json() is None
assert response.json() == {"size": 3}

# Check created statuses
db_statuses = db_session.exec(select(Status)).all()
Expand Down Expand Up @@ -1023,7 +1023,7 @@ def test_create_status_bulk_for_user(db_session, client_auth):
json=[json.loads(s.model_dump_json()) for s in qc_statuses],
)
assert response.status_code == status.HTTP_201_CREATED
assert response.json() is None
assert response.json() == {"size": 3}

# Check created statuses
db_statuses = db_session.exec(select(Status)).all()
Expand Down Expand Up @@ -1280,7 +1280,7 @@ def test_create_session_bulk_for_superuser(db_session, client_auth):
json=[json.loads(s.model_dump_json()) for s in qc_sessions],
)
assert response.status_code == status.HTTP_201_CREATED
assert response.json() is None
assert response.json() == {"size": 3}

# Check created statuses
db_qc_sessions = db_session.exec(select(Session)).all()
Expand Down Expand Up @@ -1391,7 +1391,7 @@ def test_create_session_bulk_for_user(db_session, client_auth):
json=[json.loads(s.model_dump_json()) for s in qc_sessions],
)
assert response.status_code == status.HTTP_201_CREATED
assert response.json() is None
assert response.json() == {"size": 3}

# Check created statuses
db_qc_sessions = db_session.exec(select(Session)).all()
Expand Down
4 changes: 3 additions & 1 deletion src/client/qcc/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@

from ..client import QCC
from ..conf import settings
from . import auth, static
from . import auth, session, static, status

app = typer.Typer(name="qcc", no_args_is_help=True)
app.add_typer(auth.app)
app.add_typer(static.app)
app.add_typer(status.app)
app.add_typer(session.app)


@app.callback()
Expand Down
21 changes: 21 additions & 0 deletions src/client/qcc/cli/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""QualiCharge API client CLI: api."""

from typing import Any

import typer
from anyio import run
from rich import print

from ..exceptions import APIRequestError
from .codes import QCCExitCodes


def async_run_api_query(*args) -> Any:
"""An anyio.run wrapper to handle APIRequestError."""
try:
return_value = run(*args)
except APIRequestError as err:
print("[red]An error occurred while querying the API! More details follow.")
print(err.args[0])
raise typer.Exit(QCCExitCodes.API_EXCEPTION) from err
return return_value
59 changes: 59 additions & 0 deletions src/client/qcc/cli/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""QualiCharge API client CLI: statuc."""

from typing import Annotated, Optional

import click
import typer
from rich import print

from ..client import QCC
from .api import async_run_api_query
from .utils import parse_input_json_lines, parse_json_parameter

app = typer.Typer(name="session", no_args_is_help=True)


@app.command()
def create(
ctx: typer.Context,
session: Optional[str] = None,
interactive: Annotated[
bool, typer.Option(help="Read session from standard input (JSON string)")
] = True,
):
"""Create a charging point session.

You can submit your session entry to create as a JSON string argument for
the `--status` option. Without `--status` option (but with `--interactive`)
the command will read and parse the standard input as a JSON string.

Note that when using the `--interactive` option (active by default), the command
expects your JSON string on a single row.
"""
client: QCC = ctx.obj
data = parse_json_parameter("session", session, interactive) # type: ignore[arg-type]
created = async_run_api_query(client.session.create, data)
print("[green]Created session successfully.[/green]")
print(created)


@app.command()
def bulk(
ctx: typer.Context,
chunk_size: int = 10,
ignore_errors: bool = False,
):
"""Bulk create new sessions.

Sessions will be read from the standard input (one JSON per line).
"""
client: QCC = ctx.obj

n_created = async_run_api_query(
client.session.bulk,
parse_input_json_lines(click.get_text_stream("stdin"), ignore_errors),
chunk_size,
ignore_errors,
)

print(f"[green]Created {n_created} sessions successfully.[/green]")
68 changes: 8 additions & 60 deletions src/client/qcc/cli/static.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,24 @@
"""QualiCharge API client CLI: static."""

import json
from typing import Any, Generator, Optional
from typing import Optional

import click
import typer
from anyio import run
from rich import print
from typing_extensions import Annotated

from ..client import QCC
from ..exceptions import APIRequestError
from .api import async_run_api_query
from .codes import QCCExitCodes
from .utils import parse_input_json_lines, parse_json_parameter

app = typer.Typer(name="static", no_args_is_help=True)


def async_run_api_query(*args) -> Any:
"""An anyio.run wrapper to handle APIRequestError."""
try:
return_value = run(*args)
except APIRequestError as err:
print("[red]An error occurred while querying the API! More details follow.")
print(err.args[0])
raise typer.Exit(QCCExitCodes.API_EXCEPTION) from err
return return_value


@app.command()
def list(ctx: typer.Context):
"""Get all static entries."""
"""Get all statique entries."""
client: QCC = ctx.obj

async def statiques():
Expand Down Expand Up @@ -57,30 +46,15 @@ def create(
expects your JSON string on a single row.
"""
client: QCC = ctx.obj
if not statique and interactive:
statique = click.get_text_stream("stdin").readline()

if statique is None:
print(
"[red]A statique object is required either from stdin or as an option[/red]"
)
raise typer.Exit(QCCExitCodes.PARAMETER_EXCEPTION)

try:
data = json.loads(statique)
except json.JSONDecodeError as err:
print("[red]Invalid JSON input string[/red]")
raise typer.Exit(QCCExitCodes.PARAMETER_EXCEPTION) from err

data = parse_json_parameter("statique", statique, interactive) # type: ignore[arg-type]
created = async_run_api_query(client.static.create, data)

print("[green]Created statique successfully.[/green]")
print(created)


@app.command()
def read(ctx: typer.Context, id_pdc_itinerance: str):
"""Get all static entries."""
"""Read a statique entry."""
client: QCC = ctx.obj

read = async_run_api_query(client.static.read, id_pdc_itinerance)
Expand All @@ -105,20 +79,7 @@ def update(
expects your JSON string on a single row.
"""
client: QCC = ctx.obj
if not statique and interactive:
statique = click.get_text_stream("stdin").readline()

if statique is None:
print(
"[red]A statique object is required either from stdin or as an option[/red]"
)
raise typer.Exit(QCCExitCodes.PARAMETER_EXCEPTION)

try:
data = json.loads(statique)
except json.JSONDecodeError as err:
print("[red]Invalid JSON input string[/red]")
raise typer.Exit(QCCExitCodes.PARAMETER_EXCEPTION) from err
data = parse_json_parameter("statique", statique, interactive) # type: ignore[arg-type]

if "id_pdc_itinerance" not in data:
print("[red]Statique object requires an `id_pdc_itinerance` field[/red]")
Expand All @@ -142,22 +103,9 @@ def bulk(
"""
client: QCC = ctx.obj

def parse_input_json_lines(lines) -> Generator[dict, None, None]:
"""Read and JSON parse stdin line by line."""
for statique in lines:
try:
data = json.loads(statique)
except json.JSONDecodeError as err:
if ignore_errors:
print(f"[orange]Ignored invalid line:[/orange]\n{statique}")
continue
print("[red]Invalid JSON input string[/red]")
raise typer.Exit(QCCExitCodes.PARAMETER_EXCEPTION) from err
yield data

n_created = async_run_api_query(
client.static.bulk,
parse_input_json_lines(click.get_text_stream("stdin")),
parse_input_json_lines(click.get_text_stream("stdin"), ignore_errors),
chunk_size,
ignore_errors,
)
Expand Down
Loading