From baa0c9fabe32d4baa4971647c35b88d4d67148a6 Mon Sep 17 00:00:00 2001 From: Jeltz Date: Fri, 15 Aug 2025 14:37:10 +0200 Subject: [PATCH] Commit initial --- README.md | 3 + brassage/__init__.py | 1 + brassage/__main__.py | 28 ++++++++ brassage/config.py | 33 +++++++++ brassage/static/style.css | 111 +++++++++++++++++++++++++++++++ brassage/switch.py | 11 +++ brassage/templates/base.html.j2 | 23 +++++++ brassage/templates/room.html.j2 | 30 +++++++++ brassage/templates/rooms.html.j2 | 12 ++++ brassage/web.py | 85 +++++++++++++++++++++++ examples/config.toml | 33 +++++++++ pyproject.toml | 53 +++++++++++++++ 12 files changed, 423 insertions(+) create mode 100644 README.md create mode 100644 brassage/__init__.py create mode 100644 brassage/__main__.py create mode 100644 brassage/config.py create mode 100644 brassage/static/style.css create mode 100644 brassage/switch.py create mode 100644 brassage/templates/base.html.j2 create mode 100644 brassage/templates/room.html.j2 create mode 100644 brassage/templates/rooms.html.j2 create mode 100644 brassage/web.py create mode 100644 examples/config.toml create mode 100644 pyproject.toml diff --git a/README.md b/README.md new file mode 100644 index 0000000..80c9cf3 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Brassage + +Application web de brassage multi-opérateurs. diff --git a/brassage/__init__.py b/brassage/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/brassage/__init__.py @@ -0,0 +1 @@ + diff --git a/brassage/__main__.py b/brassage/__main__.py new file mode 100644 index 0000000..4ec4385 --- /dev/null +++ b/brassage/__main__.py @@ -0,0 +1,28 @@ +from argparse import ArgumentParser, BooleanOptionalAction +from pathlib import Path + +from uvicorn import run + +from brassage.config import read_config +from brassage.web import make_app + + +def main() -> None: + parser = ArgumentParser("brassage") + parser.add_argument("-c", "--config", type=Path, default="config.toml") + parser.add_argument( + "-d", "--debug", action=BooleanOptionalAction, default=False + ) + + args = parser.parse_args() + config = read_config(args.config) + + run( + make_app(config, args.debug), + port=5000, + log_level="debug" if args.debug else "info", + ) + + +if __name__ == "__main__": + main() diff --git a/brassage/config.py b/brassage/config.py new file mode 100644 index 0000000..f054989 --- /dev/null +++ b/brassage/config.py @@ -0,0 +1,33 @@ +from pathlib import Path +from tomllib import load + +from pydantic import BaseModel + + +class Switch(BaseModel): + url: str + user: str + password: str + + +class State(BaseModel): + untag: int | None = None + tag: set[int] = [] + + +class Room(BaseModel): + switch: str + port: str + labels: list[str] + states: dict[str, State] = {} + + +class Config(BaseModel): + switchs: dict[str, Switch] + rooms: dict[str, Room] + + +def read_config(path: Path) -> Config: + with path.open("rb") as f: + config = load(f) + return Config.model_validate(config) diff --git a/brassage/static/style.css b/brassage/static/style.css new file mode 100644 index 0000000..322b626 --- /dev/null +++ b/brassage/static/style.css @@ -0,0 +1,111 @@ +*, *::before, *::after { + box-sizing: border-box; +} + +* { + margin: 0; +} + +img, picture, video, canvas, svg { + display: block; + max-width: 100%; +} + +input, button, textarea, select { + font: inherit; +} + +p, h1, h2, h3, h4, h5, h6 { + overflow-wrap: break-word; +} + +p { + text-wrap: pretty; +} + +h1, h2, h3, h4, h5, h6 { + text-wrap: balance; +} + +html { + font-family: system-ui, sans-serif; +} + +body { + display: grid; + grid-template-columns: + 1fr + [start] min(980px, 100vw) + [end] 1fr; + grid-template-rows: + [header] auto + [main] 1fr + [footer] auto; + min-height: 100vh; + width: 100vw; + line-height: 1.5; +} + +header { + grid-column: start / end; + grid-row: header; + display: flex; + align-items: center; + padding: 2rem 0; + border-bottom: 1px solid grey; + + a.title { + flex: 1; + font-size: 1.5rem; + font-weight: normal; + text-decoration: none; + color: inherit; + } + + nav a { + text-decoration: none; + color: inherit; + padding: .5rem 1rem; + background-color: lightgrey; + + &:hover { + color: white; + background-color: grey; + } + } +} + +main { + grid-column: start / end; + grid-row: main; + padding: 2rem 0; + + h1 { + margin: 0 0 1rem 0; + font-size: 2rem; + font-weight: bold; + } +} + +footer { + grid-column: start / end; + grid-row: footer; + padding: 2rem 0; + border-top: 1px solid grey; +} + +nav.rooms { + display: flex; + flex-direction: column; + + a { + text-decoration: none; + font-size: 1.25rem; + padding: .5rem; + color: blue; + + &:hover:before { + content: "> "; + } + } +} \ No newline at end of file diff --git a/brassage/switch.py b/brassage/switch.py new file mode 100644 index 0000000..4216999 --- /dev/null +++ b/brassage/switch.py @@ -0,0 +1,11 @@ +from brassage.config import State, Switch + + +class Interface: + def __init__(self, switch: Switch) -> None: + self._switch = switch + + async def fetch(self, port: str, timeout: int = 2) -> State | None: + return State(untag=102, tag=[120, 404, 505]) + + async def update(self, port: str, state: State) -> None: ... diff --git a/brassage/templates/base.html.j2 b/brassage/templates/base.html.j2 new file mode 100644 index 0000000..ab1333a --- /dev/null +++ b/brassage/templates/base.html.j2 @@ -0,0 +1,23 @@ + + + + + {% block title %}{% endblock %} + + + +
+ Brassage + +
+
+ {% block main %} + {% endblock %} +
+ + + \ No newline at end of file diff --git a/brassage/templates/room.html.j2 b/brassage/templates/room.html.j2 new file mode 100644 index 0000000..bd1290b --- /dev/null +++ b/brassage/templates/room.html.j2 @@ -0,0 +1,30 @@ +{% extends "base.html.j2" %} + +{% block title %}{{ room.labels | join(' > ') }}{% endblock %} + +{% block main %} +

{{ room.labels | join(' > ') }}

+ +

État

+ + +

Modifier

+
+
+ + +
+
+ +
+
+ +{% endblock %} \ No newline at end of file diff --git a/brassage/templates/rooms.html.j2 b/brassage/templates/rooms.html.j2 new file mode 100644 index 0000000..3ebf4fd --- /dev/null +++ b/brassage/templates/rooms.html.j2 @@ -0,0 +1,12 @@ +{% extends "base.html.j2" %} + +{% block title %}Logements{% endblock %} + +{% block main %} +

Logements

+ +{% endblock %} diff --git a/brassage/web.py b/brassage/web.py new file mode 100644 index 0000000..f1953ae --- /dev/null +++ b/brassage/web.py @@ -0,0 +1,85 @@ +from jinja2 import Environment, PackageLoader, select_autoescape +from starlette.applications import Starlette +from starlette.exceptions import HTTPException +from starlette.requests import Request +from starlette.responses import JSONResponse +from starlette.routing import Mount, Route +from starlette.staticfiles import StaticFiles +from starlette.templating import Jinja2Templates + +from brassage.config import Config, State, read_config +from brassage.switch import Interface + +templates = Jinja2Templates( + env=Environment( + loader=PackageLoader("brassage", "templates"), + autoescape=select_autoescape(), + ) +) + + +def state_str(state: State | None, states: dict[str, State]) -> str: + def extract(): + if state.untag is not None: + yield f"{state.untag}" + for tag in state.tag: + yield f"*{tag}" + + if state is None: + return "Inconnu" + for name, target in states.items(): + if state == target: + return name + return f"Invalide ({', '.join(extract())})" + + +async def rooms(request: Request): + return templates.TemplateResponse(request, "rooms.html.j2") + + +async def room(request: Request): + name = request.path_params["name"] + + try: + room = request.app.state.config.rooms[name] + except KeyError: + raise HTTPException(status_code=404) + + switch = request.app.state.config.switchs[room.switch] + interface = Interface(switch) + + if request.method == "POST": + async with request.form() as form: + try: + target = room.states[form["state"]] + except KeyError: + raise HTTPException(status_code=400) + await interface.update(room.port, target) + + state = await interface.fetch(room.port) + + return templates.TemplateResponse( + request, + "room.html.j2", + { + "name": name, + "room": room, + "state": state_str(state, room.states), + }, + ) + + +def make_app(config: Config, debug: bool) -> Starlette: + static = StaticFiles(packages=[("brassage", "static")]) + app = Starlette( + debug=debug, + routes=[ + Route("/", rooms, name="rooms"), + Route( + "/room/{name:str}", room, name="room", methods=["GET", "POST"] + ), + Mount("/static", app=static, name="static"), + ], + ) + app.state.config = config + return app diff --git a/examples/config.toml b/examples/config.toml new file mode 100644 index 0000000..60549c3 --- /dev/null +++ b/examples/config.toml @@ -0,0 +1,33 @@ +[switchs.gc-1] +url = "https://gc-1.acs.sw.infra.auro.re" +user = "brassage" +password = "password" + +[switchs.gc-2] +url = "https://gc-2.acs.sw.infra.auro.re" +user = "brassage" +password = "password" + +[rooms.gs-c-602] +switch = "gc-1" +port = "10" +labels = [ "George Sand", "Bâtiment C", "602" ] + +[rooms.gs-c-602.states.Aurore] +untag = 1004 + +[rooms.gs-c-602.states.Crous] +untag = 4004 + +[rooms.gs-c-603] +switch = "gc-2" +port = "11" +labels = [ "George Sand", "Bâtiment C", "603" ] + +[rooms.gs-c-603.states.Aurore] +untag = 1005 +tag = [ 208, 209 ] + +[rooms.gs-c-603.states.Crous] +untag = 4005 +tag = [ 208, 209 ] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..cd14b61 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "brassage" +version = "0.0.1" +description = "Application web de brassage multi-opérateurs" +readme = "README.md" +requires-python = ">=3.12" +keywords = [] +authors = [ + { name = "Jeltz", email = "jeltz@crans.org" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "starlette", + "aiohttp", + "pydantic>=2", + "uvicorn", + "jinja2", + "python-multipart", +] + +[project.scripts] +brassage = "brassage.__main__:main" + +[tool.ruff] +line-length = 79 +target-version = "py312" + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "D100", + "D101", + "D103", + "D104", + "T201", + "D203", + "D212", + "COM812", + "FBT001", + "ISC001", + "FIX", + "TD", +]