Initial setup

This commit is contained in:
Barbagus42 2023-11-07 08:40:43 +01:00
parent 07bae48180
commit 410c5e44e8
20 changed files with 1104 additions and 0 deletions

21
.cspell.json Normal file
View File

@ -0,0 +1,21 @@
// cSpell Settings
{
// Version of the setting file. Always 0.2
"version": "0.2",
// language - current active spelling language
"language": "en",
// words - list of words to be always considered correct
"words": [
],
// flagWords - list of words to be always considered incorrect
// This is useful for offensive words and common spelling errors.
// For example "hte" should be "the"
"flagWords": [
],
"overrides": [
{
"language": "fr-FR",
"filename": "**.md"
}
]
}

3
.gitignore vendored
View File

@ -160,3 +160,6 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
data/*.sqlite
data/*.zip
.vscode

View File

@ -1,2 +1,32 @@
# TER
Interface web exploitant le dataset [sncf-ter-gtfs@datasncf](sncf-ter-gtfs@datasncf) (horaires des lignes TER).
Pour le moment c'est juste une excuse pour découvrir [htmx](https://htmx.org/).
Toutes propositions de fonctionnalité sont les bienvenues !
## Développement
### Installer le package en édition
`$ pip install -e .[dev]`
### Mettre a jour la base de données
Le fichier de données [GTFS](https://gtfs.org/schedule/reference/) est mis à
jour quotidiennement par le SNCF. Pour travailler sur les dernières données
disponibles:
`$ python3 data/update.py`
### Recompilation continue du fichier `css`
`$ tailwindcss -i app/static/src/tw.css -o app/static/css/main.css --watch`
### Démarrage du serveur de développement
`$ uvicorn ter.main:app --reload`
### Lancer la suite de tests
`$ pytest`
## Notes
Les fichiers de code (`.py`, `.sql`) et les `commits` sont écrits (et documentés)
en anglais.

84
data/schema.sql Normal file
View File

@ -0,0 +1,84 @@
CREATE TABLE
agency (
agency_id INT NOT NULL,
agency_name TEXT NOT NULL,
agency_url TEXT NOT NULL,
agency_timezone TEXT NOT NULL,
agency_lang TEXT NOT NULL,
PRIMARY KEY (agency_id)
);
CREATE TABLE
calendar_dates (
service_id INT,
date TEXT,
exception_type INT
);
CREATE UNIQUE INDEX calendar_dates__by_service_id
ON calendar_dates (service_id, date);
CREATE UNIQUE INDEX calendar_dates__by_date
ON calendar_dates (date, service_id);
CREATE TABLE
stops (
stop_id INT NOT NULL,
stop_name TEXT NOT NULL,
stop_lat REAL NOT NULL,
stop_lon REAL NOT NULL,
location_type INT NOT NULL,
parent_station INT,
PRIMARY KEY (stop_id),
FOREIGN KEY (parent_station) REFERENCES stops (location_type)
);
CREATE INDEX stops__parent_station
ON stops (parent_station);
CREATE TABLE
routes (
route_id INT NOT NULL,
agency_id INT NOT NULL,
route_short_name TEXT NOT NULL,
route_long_name TEXT NOT NULL,
route_type INT NOT NULL,
route_color TEXT,
route_text_color TEXT,
PRIMARY KEY (route_id),
FOREIGN KEY (agency_id) REFERENCES agency (agency_id)
);
CREATE INDEX routes__by_agency_id
ON routes (agency_id);
CREATE TABLE
trips (
route_id INT NOT NULL,
service_id INT NOT NULL,
trip_id INT NOT NULL,
trip_headsign TEXT NOT NULL,
direction_id INT,
block_id INT NOT NULL,
PRIMARY KEY (trip_id),
FOREIGN KEY (route_id) REFERENCES routes (route_id),
FOREIGN KEY (service_id) REFERENCES calendar_dates (service_id)
);
CREATE INDEX trips__by_route_id
ON trips (route_id);
CREATE INDEX trips__by_service_id
ON trips (service_id);
CREATE TABLE
stop_times (
trip_id INT NOT NULL,
arrival_time TEXT NOT NULL,
departure_time TEXT NOT NULL,
stop_id INT NOT NULL,
stop_sequence INT NOT NULL,
pickup_type INT NOT NULL,
drop_off_type INT NOT NULL,
PRIMARY KEY (trip_id, stop_sequence),
FOREIGN KEY (trip_id) REFERENCES trips (trip_id),
FOREIGN KEY (stop_id) REFERENCES stops (stop_id)
);
CREATE INDEX stop_times__by_trip_id
ON stop_times (trip_id);
CREATE INDEX stop_times__by_stop_id
ON stop_times (stop_id);

247
data/update.py Normal file
View File

@ -0,0 +1,247 @@
import zipfile
import csv
import io
import sqlite3
import urllib.request as request
import os.path
import math
import contextlib as ctx
# GTFS reference:
# https://gtfs.org/schedule/reference/
# SNCF/TER dataset information:
# https://data.opendatasoft.com/explore/dataset/sncf-ter-gtfs%40datasncf/information/
# SNCF/TER daily updated dataset:
GTFS_URL = "https://eu.ftp.opendatasoft.com/sncf/gtfs/export-ter-gtfs-last.zip"
PKS: dict[str, dict[str, int]] = {}
def primary_key(table):
assert table not in PKS
PKS[table] = {}
def map(v):
PKS[table][v] = len(PKS[table]) + 1
return len(PKS[table])
return map
def foreign_key(table):
def map(v):
return PKS[table][v]
return map
def optional(f):
def map(v):
return None if v == "" else f(v)
return map
CSV_FIELDS = (
(
"agency.txt",
(
("agency_id", primary_key("agency")),
("agency_name", str),
("agency_url", str),
("agency_timezone", str),
("agency_lang", str),
),
),
(
"calendar_dates.txt",
(
("service_id", int),
("date", str),
("exception_type", int),
),
),
(
"routes.txt",
(
("route_id", primary_key("routes")),
("agency_id", foreign_key("agency")),
("route_short_name", str),
("route_long_name", str),
("route_desc", None),
("route_type", int),
("route_url", None),
("route_color", optional(str)),
("route_text_color", optional(str)),
),
),
(
"trips.txt",
(
("route_id", foreign_key("routes")),
("service_id", int),
("trip_id", primary_key("trips")),
("trip_headsign", str),
("direction_id", optional(int)),
("block_id", int),
("shape_id", None),
),
),
(
"stops.txt",
(
("stop_id", primary_key("stops")),
("stop_name", str),
("stop_desc", None),
("stop_lat", float),
("stop_lon", float),
("zone_id", None),
("stop_url", None),
("location_type", int),
("parent_station", optional(foreign_key("stops"))),
),
),
(
"stop_times.txt",
(
("trip_id", foreign_key("trips")),
("arrival_time", str),
("departure_time", str),
("stop_id", foreign_key("stops")),
("stop_sequence", int),
("stop_headsign", None),
("pickup_type", int),
("drop_off_type", int),
("shape_dist_traveled", None),
),
),
("feed_info.txt", None),
("transfers.txt", None),
)
def _get_file_names(etag):
dir = os.path.dirname(__file__)
return (
os.path.join(dir, etag + ".zip"),
os.path.join(dir, etag + ".sqlite"),
os.path.join(dir, "schema.sql"),
os.path.join(dir, "db.sqlite"),
)
def _fetch_dataset(response, dataset_file):
print("Fetching dataset...")
content_length = int(response.getheader("Content-Length"))
with open(dataset_file, "wb") as zip_out:
while True:
bytes = response.read(102400)
zip_out.write(bytes)
if not bytes:
break
progress = math.floor(100 * zip_out.tell() / content_length)
print(f"Fetched: {zip_out.tell()}/{content_length} {progress}%")
def _check_dataset_files(zip_in):
csv_files = list(sorted(zip_in.namelist()))
expected = list(sorted(csv_file for csv_file, _ in CSV_FIELDS))
assert len(expected) == len(csv_files), csv_files
assert all(a == b for a, b in zip(csv_files, expected, strict=True)), csv_files
def _check_csv_headers(csv_headers, fields):
expected = list(header for header, _mapper in fields)
assert len(expected) == len(csv_headers), csv_headers
assert all(a == b for a, b in zip(csv_headers, expected)), csv_headers
def _create_schema(db, schema_file):
with db:
with open(schema_file) as sql_in:
db.executescript(sql_in.read())
def _load_data(zip_in, db):
for csv_file, fields in CSV_FIELDS:
if fields is None:
continue
table = csv_file[:-4]
print(f"Loading table {table!r}")
with zip_in.open(csv_file, "r") as csv_in:
reader = iter(
csv.reader(
io.TextIOWrapper(
csv_in,
encoding="utf-8",
newline="",
)
)
)
_check_csv_headers(next(reader), fields)
place_holders = ",".join(
"?" for _field, mapper in fields if mapper is not None
)
with db:
db.executemany(
f"INSERT INTO {table} VALUES ({place_holders})",
(
[
mapper(value)
for (_field, mapper), value in zip(fields, row)
if mapper is not None
]
for row in reader
),
)
def _load_database(dataset_file, database_tempfile, schema_file):
print("Loading database...")
with zipfile.ZipFile(dataset_file) as zip_in:
_check_dataset_files(zip_in)
with ctx.closing(sqlite3.connect(database_tempfile)) as db:
_create_schema(db, schema_file)
_load_data(zip_in, db)
print("Done")
def main():
print("Checking dataset")
response = request.urlopen(GTFS_URL)
if response.status != 200:
raise RuntimeError("Could not fetch the dataset")
etag = response.getheader("ETag")[1:-1]
(dataset_file, database_tempfile, schema_file, database_file) = _get_file_names(
etag
)
if os.path.isfile(dataset_file):
print("Dataset is up to date")
response.close()
else:
_fetch_dataset(response, dataset_file)
response.close()
if os.path.isfile(database_tempfile):
os.unlink(database_tempfile)
_load_database(dataset_file, database_tempfile, schema_file)
if os.path.isfile(database_file):
os.unlink(database_file)
os.rename(database_tempfile, database_file)
if __name__ == "__main__":
exit(main())

27
pyproject.toml Normal file
View File

@ -0,0 +1,27 @@
[project]
name = "TER"
version = "0.0.1"
description = ""
authors = [
{name = "Etienne Zind", email = "etienne.zind@proton.me"},
]
dependencies = [
"fastapi[all]",
"pydantic-settings",
"jinja2",
"jinja2-fragments",
]
requires-python = ">=3.11"
[project.optional-dependencies]
dev = [
"pytest",
"pytailwindcss",
]
[tool.setuptools]
packages = ["ter"]
[tool.pytest.ini_options]
addopts = "-ra"
testpaths = ["tests"]
pythonpath = ['.']

7
tailwind.config.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
content: ["./app/templates/**/*.html"],
theme: {
extend: {},
},
plugins: [],
}

0
ter/__init__.py Normal file
View File

26
ter/config.py Normal file
View File

@ -0,0 +1,26 @@
import os.path as path
from fastapi.responses import HTMLResponse
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
APP_DIR: str = path.dirname(__file__)
STATIC_DIR: str = path.join(APP_DIR, "static")
TEMPLATE_DIR: str = path.join(APP_DIR, "templates")
DATA_DIR: str = path.join(path.dirname(APP_DIR), "data")
FASTAPI_PROPERTIES: dict = {
"title": "TER",
"description": "",
"version": "0.0.1",
"default_response_class": HTMLResponse, # Change default from JSONResponse
"openapi_url": None,
"openapi_prefix": None,
"docs_url": None,
"redoc_url": None,
}
settings = Settings()

16
ter/helpers.py Normal file
View File

@ -0,0 +1,16 @@
import sqlite3
import os.path as path
import contextlib as ctx
from ter.config import Settings
settings = Settings()
def connect_db():
return ctx.closing(
sqlite3.connect(
f"file:{path.join(settings.DATA_DIR, 'db.sqlite')}?mode=ro",
uri=True,
)
)

18
ter/main.py Normal file
View File

@ -0,0 +1,18 @@
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from ter.config import Settings
from ter.routes import router
settings = Settings()
def get_app() -> FastAPI:
app = FastAPI(**settings.FASTAPI_PROPERTIES)
app.mount("/static", StaticFiles(directory=settings.STATIC_DIR), name="static")
app.include_router(router)
return app
app = get_app()

22
ter/routes.py Normal file
View File

@ -0,0 +1,22 @@
from fastapi import APIRouter, Request
from jinja2_fragments.fastapi import Jinja2Blocks
from ter.config import Settings
from ter.helpers import connect_db
settings = Settings()
router = APIRouter()
templates = Jinja2Blocks(settings.TEMPLATE_DIR)
@router.get("/")
def index(request: Request):
"""Home page."""
with connect_db() as db:
agencies = db.execute("SELECT * FROM agency").fetchall()
context = {"request": request, "agencies": agencies}
return templates.TemplateResponse("index.html", context)

539
ter/static/css/main.css Normal file
View File

@ -0,0 +1,539 @@
/*
! tailwindcss v3.3.5 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
*/
html {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font family by default.
2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-feature-settings: inherit;
/* 1 */
font-variation-settings: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
.block {
display: block;
}

1
ter/static/js/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

3
ter/static/src/tw.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

27
ter/templates/_base.html Normal file
View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
<title>TER</title>
<!-- CSS -->
<link rel="stylesheet" href="/static/css/main.css" type="text/css" />
<!-- htmx -->
<script src="/static/js/htmx.min.js"></script>
</head>
<body>
{% block header %}
{% include "/_header.html" %}
{% endblock %}
{% block content %}
{% endblock %}
{% block footer %}
{% include "/_footer.html" %}
{% endblock %}
</body>
</html>

View File

@ -0,0 +1,2 @@
<footer>
</footer>

View File

@ -0,0 +1,2 @@
<header>
</header>

19
ter/templates/index.html Normal file
View File

@ -0,0 +1,19 @@
{% extends "/_base.html" %}
{% block content %}
<h1>Agences<h2>
<table>
<thead>
<th>ID</th>
<th>Nom</th>
</thead>
<tbody>
{% for agency in agencies %}
<tr>
<td>{{agency[0]}}</td>
<td><a href="{{agency[2]}}">{{agency[1]}}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

10
tests/conftest.py Normal file
View File

@ -0,0 +1,10 @@
import pytest
from fastapi.testclient import TestClient
from ter.main import app
@pytest.fixture()
def client():
with TestClient(app) as client:
yield client