Initial setup
This commit is contained in:
parent
07bae48180
commit
410c5e44e8
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
|
|
30
README.md
30
README.md
|
@ -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.
|
||||
|
|
|
@ -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);
|
|
@ -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())
|
|
@ -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 = ['.']
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
content: ["./app/templates/**/*.html"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
|
@ -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()
|
|
@ -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,
|
||||
)
|
||||
)
|
|
@ -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()
|
|
@ -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)
|
|
@ -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;
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
|
@ -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>
|
|
@ -0,0 +1,2 @@
|
|||
<footer>
|
||||
</footer>
|
|
@ -0,0 +1,2 @@
|
|||
<header>
|
||||
</header>
|
|
@ -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 %}
|
|
@ -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
|
Loading…
Reference in New Issue