Skip to content

Commit 0ac44e6

Browse files
committed
✨(client) add dynamic endpoints and CLI commands
We are now able to query /dynamique API endpoints using the client but also the CLI, yay!
1 parent 3074249 commit 0ac44e6

File tree

15 files changed

+1160
-159
lines changed

15 files changed

+1160
-159
lines changed

src/client/qcc/cli/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77

88
from ..client import QCC
99
from ..conf import settings
10-
from . import auth, static
10+
from . import auth, session, static, status
1111

1212
app = typer.Typer(name="qcc", no_args_is_help=True)
1313
app.add_typer(auth.app)
1414
app.add_typer(static.app)
15+
app.add_typer(status.app)
16+
app.add_typer(session.app)
1517

1618

1719
@app.callback()

src/client/qcc/cli/api.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""QualiCharge API client CLI: api."""
2+
3+
from typing import Any
4+
5+
import typer
6+
from anyio import run
7+
from rich import print
8+
9+
from ..exceptions import APIRequestError
10+
from .codes import QCCExitCodes
11+
12+
13+
def async_run_api_query(*args) -> Any:
14+
"""An anyio.run wrapper to handle APIRequestError."""
15+
try:
16+
return_value = run(*args)
17+
except APIRequestError as err:
18+
print("[red]An error occurred while querying the API! More details follow.")
19+
print(err.args[0])
20+
raise typer.Exit(QCCExitCodes.API_EXCEPTION) from err
21+
return return_value

src/client/qcc/cli/session.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""QualiCharge API client CLI: statuc."""
2+
3+
from typing import Annotated, Optional
4+
5+
import click
6+
import typer
7+
from rich import print
8+
9+
from ..client import QCC
10+
from .api import async_run_api_query
11+
from .utils import parse_input_json_lines, parse_json_parameter
12+
13+
app = typer.Typer(name="session", no_args_is_help=True)
14+
15+
16+
@app.command()
17+
def create(
18+
ctx: typer.Context,
19+
session: Optional[str] = None,
20+
interactive: Annotated[
21+
bool, typer.Option(help="Read session from standard input (JSON string)")
22+
] = True,
23+
):
24+
"""Create a charging point session.
25+
26+
You can submit your session entry to create as a JSON string argument for
27+
the `--status` option. Without `--status` option (but with `--interactive`)
28+
the command will read and parse the standard input as a JSON string.
29+
30+
Note that when using the `--interactive` option (active by default), the command
31+
expects your JSON string on a single row.
32+
"""
33+
client: QCC = ctx.obj
34+
data = parse_json_parameter("session", session, interactive) # type: ignore[arg-type]
35+
created = async_run_api_query(client.session.create, data)
36+
print("[green]Created session successfully.[/green]")
37+
print(created)
38+
39+
40+
@app.command()
41+
def bulk(
42+
ctx: typer.Context,
43+
chunk_size: int = 10,
44+
ignore_errors: bool = False,
45+
):
46+
"""Bulk create new sessions.
47+
48+
Sessions will be read from the standard input (one JSON per line).
49+
"""
50+
client: QCC = ctx.obj
51+
52+
n_created = async_run_api_query(
53+
client.session.bulk,
54+
parse_input_json_lines(click.get_text_stream("stdin"), ignore_errors),
55+
chunk_size,
56+
ignore_errors,
57+
)
58+
59+
print(f"[green]Created {n_created} sessions successfully.[/green]")

src/client/qcc/cli/static.py

Lines changed: 8 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,24 @@
11
"""QualiCharge API client CLI: static."""
22

33
import json
4-
from typing import Any, Generator, Optional
4+
from typing import Optional
55

66
import click
77
import typer
8-
from anyio import run
98
from rich import print
109
from typing_extensions import Annotated
1110

1211
from ..client import QCC
13-
from ..exceptions import APIRequestError
12+
from .api import async_run_api_query
1413
from .codes import QCCExitCodes
14+
from .utils import parse_input_json_lines, parse_json_parameter
1515

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

1818

19-
def async_run_api_query(*args) -> Any:
20-
"""An anyio.run wrapper to handle APIRequestError."""
21-
try:
22-
return_value = run(*args)
23-
except APIRequestError as err:
24-
print("[red]An error occurred while querying the API! More details follow.")
25-
print(err.args[0])
26-
raise typer.Exit(QCCExitCodes.API_EXCEPTION) from err
27-
return return_value
28-
29-
3019
@app.command()
3120
def list(ctx: typer.Context):
32-
"""Get all static entries."""
21+
"""Get all statique entries."""
3322
client: QCC = ctx.obj
3423

3524
async def statiques():
@@ -57,30 +46,15 @@ def create(
5746
expects your JSON string on a single row.
5847
"""
5948
client: QCC = ctx.obj
60-
if not statique and interactive:
61-
statique = click.get_text_stream("stdin").readline()
62-
63-
if statique is None:
64-
print(
65-
"[red]A statique object is required either from stdin or as an option[/red]"
66-
)
67-
raise typer.Exit(QCCExitCodes.PARAMETER_EXCEPTION)
68-
69-
try:
70-
data = json.loads(statique)
71-
except json.JSONDecodeError as err:
72-
print("[red]Invalid JSON input string[/red]")
73-
raise typer.Exit(QCCExitCodes.PARAMETER_EXCEPTION) from err
74-
49+
data = parse_json_parameter("statique", statique, interactive) # type: ignore[arg-type]
7550
created = async_run_api_query(client.static.create, data)
76-
7751
print("[green]Created statique successfully.[/green]")
7852
print(created)
7953

8054

8155
@app.command()
8256
def read(ctx: typer.Context, id_pdc_itinerance: str):
83-
"""Get all static entries."""
57+
"""Read a statique entry."""
8458
client: QCC = ctx.obj
8559

8660
read = async_run_api_query(client.static.read, id_pdc_itinerance)
@@ -105,20 +79,7 @@ def update(
10579
expects your JSON string on a single row.
10680
"""
10781
client: QCC = ctx.obj
108-
if not statique and interactive:
109-
statique = click.get_text_stream("stdin").readline()
110-
111-
if statique is None:
112-
print(
113-
"[red]A statique object is required either from stdin or as an option[/red]"
114-
)
115-
raise typer.Exit(QCCExitCodes.PARAMETER_EXCEPTION)
116-
117-
try:
118-
data = json.loads(statique)
119-
except json.JSONDecodeError as err:
120-
print("[red]Invalid JSON input string[/red]")
121-
raise typer.Exit(QCCExitCodes.PARAMETER_EXCEPTION) from err
82+
data = parse_json_parameter("statique", statique, interactive) # type: ignore[arg-type]
12283

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

145-
def parse_input_json_lines(lines) -> Generator[dict, None, None]:
146-
"""Read and JSON parse stdin line by line."""
147-
for statique in lines:
148-
try:
149-
data = json.loads(statique)
150-
except json.JSONDecodeError as err:
151-
if ignore_errors:
152-
print(f"[orange]Ignored invalid line:[/orange]\n{statique}")
153-
continue
154-
print("[red]Invalid JSON input string[/red]")
155-
raise typer.Exit(QCCExitCodes.PARAMETER_EXCEPTION) from err
156-
yield data
157-
158106
n_created = async_run_api_query(
159107
client.static.bulk,
160-
parse_input_json_lines(click.get_text_stream("stdin")),
108+
parse_input_json_lines(click.get_text_stream("stdin"), ignore_errors),
161109
chunk_size,
162110
ignore_errors,
163111
)

src/client/qcc/cli/status.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""QualiCharge API client CLI: status."""
2+
3+
import json
4+
from datetime import datetime
5+
from typing import Annotated, List, Optional
6+
7+
import click
8+
import typer
9+
from rich import print
10+
11+
from ..client import QCC
12+
from .api import async_run_api_query
13+
from .utils import parse_input_json_lines, parse_json_parameter
14+
15+
app = typer.Typer(name="status", no_args_is_help=True)
16+
17+
18+
@app.command()
19+
def list(
20+
ctx: typer.Context,
21+
from_: Annotated[Optional[datetime], typer.Option("--from")] = None,
22+
pdc: Optional[List[str]] = None,
23+
station: Optional[List[str]] = None,
24+
):
25+
"""List charging points last known status."""
26+
client: QCC = ctx.obj
27+
28+
async def statuses():
29+
async for status in client.status.list(from_=from_, pdc=pdc, station=station):
30+
typer.echo(json.dumps(status))
31+
32+
async_run_api_query(statuses)
33+
34+
35+
@app.command()
36+
def create(
37+
ctx: typer.Context,
38+
status: Optional[str] = None,
39+
interactive: Annotated[
40+
bool, typer.Option(help="Read status from standard input (JSON string)")
41+
] = True,
42+
):
43+
"""Create a charging point status.
44+
45+
You can submit your status entry to create as a JSON string argument for
46+
the `--status` option. Without `--status` option (but with `--interactive`)
47+
the command will read and parse the standard input as a JSON string.
48+
49+
Note that when using the `--interactive` option (active by default), the command
50+
expects your JSON string on a single row.
51+
"""
52+
client: QCC = ctx.obj
53+
data = parse_json_parameter("status", status, interactive) # type: ignore[arg-type]
54+
created = async_run_api_query(client.status.create, data)
55+
print("[green]Created status successfully.[/green]")
56+
print(created)
57+
58+
59+
@app.command()
60+
def read(ctx: typer.Context, id_pdc_itinerance: str):
61+
"""Get charging point status."""
62+
client: QCC = ctx.obj
63+
64+
read = async_run_api_query(client.status.read, id_pdc_itinerance)
65+
typer.echo(json.dumps(read))
66+
67+
68+
@app.command()
69+
def history(
70+
ctx: typer.Context,
71+
id_pdc_itinerance: str,
72+
from_: Annotated[Optional[datetime], typer.Option("--from")] = None,
73+
):
74+
"""Get charging point history."""
75+
client: QCC = ctx.obj
76+
77+
async def statuses():
78+
async for status in client.status.history(id_pdc_itinerance, from_=from_):
79+
typer.echo(json.dumps(status))
80+
81+
async_run_api_query(statuses)
82+
83+
84+
@app.command()
85+
def bulk(
86+
ctx: typer.Context,
87+
chunk_size: int = 10,
88+
ignore_errors: bool = False,
89+
):
90+
"""Bulk create new statuses.
91+
92+
Statuses will be read from the standard input (one JSON per line).
93+
"""
94+
client: QCC = ctx.obj
95+
96+
n_created = async_run_api_query(
97+
client.status.bulk,
98+
parse_input_json_lines(click.get_text_stream("stdin"), ignore_errors),
99+
chunk_size,
100+
ignore_errors,
101+
)
102+
103+
print(f"[green]Created {n_created} statuses successfully.[/green]")

src/client/qcc/cli/utils.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""QualiCharge API client CLI: utils."""
2+
3+
import json
4+
from typing import Generator, TextIO
5+
6+
import click
7+
import typer
8+
from rich import print
9+
10+
from .codes import QCCExitCodes
11+
12+
13+
def parse_json_parameter(name: str, parameter: str, interactive: bool) -> dict:
14+
"""Read and JSON parse parameter from option or stdin."""
15+
# Get parameter value from stdin if empty
16+
if not parameter and interactive:
17+
parameter = click.get_text_stream("stdin").readline()
18+
19+
if parameter is None:
20+
print(
21+
(
22+
f"[red]A {name} object is required "
23+
"either from stdin or as an option[/red]"
24+
)
25+
)
26+
raise typer.Exit(QCCExitCodes.PARAMETER_EXCEPTION)
27+
28+
# Parse parameter as JSON
29+
try:
30+
data = json.loads(parameter)
31+
except json.JSONDecodeError as err:
32+
print("[red]Invalid JSON input string[/red]")
33+
raise typer.Exit(QCCExitCodes.PARAMETER_EXCEPTION) from err
34+
return data
35+
36+
37+
def parse_input_json_lines(
38+
lines: TextIO, ignore_errors: bool
39+
) -> Generator[dict, None, None]:
40+
"""Read and JSON parse stdin line by line."""
41+
for line in lines:
42+
try:
43+
data = json.loads(line)
44+
except json.JSONDecodeError as err:
45+
if ignore_errors:
46+
print(f"[orange]Ignored invalid line:[/orange]\n{line}")
47+
continue
48+
print("[red]Invalid JSON input string[/red]")
49+
raise typer.Exit(QCCExitCodes.PARAMETER_EXCEPTION) from err
50+
yield data

src/client/qcc/client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""QualiCharge API client."""
22

33
from .endpoints.auth import Auth
4+
from .endpoints.dynamic import Session, Status
45
from .endpoints.static import Static
56
from .exceptions import ConfigurationError
67
from .http import HTTPClient
@@ -26,3 +27,5 @@ def __init__(
2627
)
2728
self.auth = Auth(self.client)
2829
self.static = Static(self.client)
30+
self.status = Status(self.client)
31+
self.session = Session(self.client)

0 commit comments

Comments
 (0)