Compare commits

..

2 commits

37 changed files with 816 additions and 536 deletions

14
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,14 @@
name: docker
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build the Docker image
run: |
echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin docker.io
docker build . --file Dockerfile --tag docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest
docker push docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest

4
.gitignore vendored
View file

@ -15,5 +15,5 @@ blog.sublime-project
blog.sublime-workspace
ssl/
.python-version
.local
uv.lock
poetry.toml
.local

19
.travis.yml Normal file
View file

@ -0,0 +1,19 @@
language: python
python:
- "2.7"
- "3.3"
- "3.4"
- "3.5"
- "3.6"
install:
- pip install commonmark coverage coveralls
script:
- python -m unittest discover -bv
- coverage run --branch --source=. -m unittest discover -bv
- coverage report -m
after_success:
- coveralls

View file

@ -1,12 +1,35 @@
FROM nginx:1.27.4-alpine-slim
FROM nginx:1.19.0-alpine
RUN apk update && apk add --no-cache bash git python3 make tzdata curl py3-pip musl-locales
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && mv /root/.local/bin/uv /usr/local/bin
RUN apk update
RUN apk add --no-cache build-base bash git python3 make tzdata curl py3-pip libressl-dev musl-dev libffi-dev python3-dev cargo
RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.4.0
RUN poetry config virtualenvs.create false
# install poetry
#ENV POETRY_HOME=/opt/poetry
#RUN python3 -m venv $POETRY_HOME
#RUN $POETRY_HOME/bin/pip install --upgrade pip
#RUN $POETRY_HOME/bin/pip install setuptools_rust poetry==1.4.0
#RUN $POETRY_HOME/bin/poetry --version
COPY docker/nginx.conf /etc/nginx/nginx.conf
# install locales
ENV MUSL_LOCALE_DEPS cmake make musl-dev gcc gettext-dev libintl
ENV MUSL_LOCPATH /usr/share/i18n/locales/musl
RUN apk add --no-cache \
$MUSL_LOCALE_DEPS \
&& wget https://gitlab.com/rilian-la-te/musl-locales/-/archive/master/musl-locales-master.zip \
&& unzip musl-locales-master.zip \
&& cd musl-locales-master \
&& cmake -DLOCALE_PROFILE=OFF -D CMAKE_INSTALL_PREFIX:PATH=/usr . && make && make install \
&& cd .. && rm -r musl-locales-master
# set timezone and locale
RUN cp /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo "Europe/Paris" > /etc/timezone
RUN cp /usr/share/zoneinfo/Europe/Paris /etc/localtime
RUN echo "Europe/Paris" > /etc/timezone
ENV TZ Europe/Paris
ENV LANG fr_FR.UTF-8
ENV LANGUAGE fr_FR.UTF-8

View file

@ -1,6 +1,5 @@
# Makefile
.PHONY: build
# Makefile
#
# if a file .local exists run site locally
ifeq ($(wildcard .local),)
@ -9,7 +8,7 @@ else
TARGET = site_local
endif
site: site_local
site: $(TARGET)
echo $(TARGET)
site_remote:
@ -18,14 +17,43 @@ site_remote:
systemctl reload nginx
site_local:
uv run python makesite.py --params params-local.json
rye run python makesite.py --params params-local.json
cd _site && python -m SimpleHTTPServer 2> /dev/null || python3 -m http.server
# docker build
build:
docker build -t source.madyanne.fr/yax/blog .
# docker publish
publish:
docker push source.madyanne.fr/yax/blog
dock: site_local
$(shell docker start --interactive bloglocal || docker run --name bloglocal -p 80:80 -p 443:443 -v `pwd`/_site:/usr/share/nginx/html:ro -v `pwd`/nginx/nginx.conf:/etc/nginx/nginx.conf:ro -v `pwd`/nginx/dhparam.pem:/etc/nginx/dhparam.pem:ro -v `pwd`/ssl:/etc/nginx/ssl:ro nginx)
undock:
docker stop bloglocal
certs:
mkdir -p ssl
cd ssl ; wget -N https://traefik.me/cert.pem
cd ssl ; wget -N https://traefik.me/chain.pem
cd ssl ; wget -N https://traefik.me/fullchain.pem
cd ssl ; wget -N https://traefik.me/privkey.pem
venv2:
virtualenv ~/.venv/makesite
echo . ~/.venv/makesite/bin/activate > venv
. ./venv && pip install commonmark coverage
venv: FORCE
python3 -m venv ~/.venv/makesite
echo . ~/.venv/makesite/bin/activate > venv
. ./venv && pip install commonmark coverage
test: FORCE
. ./venv && python -m unittest -bv
coverage:
. ./venv && coverage run --branch --source=. -m unittest discover -bv; :
. ./venv && coverage report -m
. ./venv && coverage html
clean:
find . -name "__pycache__" -exec rm -r {} +
find . -name "*.pyc" -exec rm {} +
rm -rf .coverage htmlcov
FORCE:

View file

@ -7,4 +7,3 @@ This static blog generator code is under MIT license.
"Blog du Yax" content is under [CC-BY-NC-SA](https://creativecommons.org/licenses/by-nc-sa/3.0)
Moved as private repository to gitea.zaclys.net on 2025-03-29.

2
build.sh Executable file
View file

@ -0,0 +1,2 @@
#!/bin/sh
pyinstaller makesite.py --onefile

View file

@ -1,19 +1,20 @@
#!/bin/bash
python -V
#export POETRY_HOME=/opt/poetry
# clone and build blog
cd /
rm -rf /blog
git clone https://gitea.zaclys.com/yannic/blog.git
git clone https://github.com/kianby/blog.git
cd /blog
uv python pin 3.12.9
uv sync
uv run python ./makesite.py
#$POETRY_HOME/bin/poetry install
#$POETRY_HOME/bin/poetry run make
poetry install
poetry run make
# nginx serve
#nginx -g 'daemon off;'
nginx
# exit on change in stacosys or Git repo
uv run python monitor.py
$POETRY_HOME/bin/poetry run python3 monitor.py

View file

@ -45,15 +45,6 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# redirections to preserve inbound links
rewrite ^/migration-du-blog-sous-pelican.html$ https://blogduyax.madyanne.fr/2013/migration-du-blog-sous-pelican/ redirect;
rewrite ^/2017/nextcloud-securite/$ https://blogduyax.madyanne.fr/2017/securite-des-donnees-focus-sur-nextcloud/ redirect;
rewrite ^/installation-de-shinken.html$ https://blogduyax.madyanne.fr/2014/installation-de-shinken/ redirect;
rewrite ^/retour-dexperience-ubuntu-touch.html$ https://blogduyax.madyanne.fr/2015/retour-dexperience-ubuntu-touch/ redirect;
rewrite ^/haute-disponibilite-avec-corosync-et-pacemaker.html$ https://blogduyax.madyanne.fr/2013/haute-disponibilite-avec-corosync-et-pacemaker/ redirect;
rewrite ^/feeds/all.atom.xml$ https://blogduyax.madyanne.fr/rss.xml redirect;
rewrite ^/feed.xml$ https://blogduyax.madyanne.fr/rss.xml redirect;
}
}
}

View file

@ -60,6 +60,7 @@ class HighlightRenderer(mistune.HTMLRenderer):
return highlight(code, lexer, formatter)
return '<pre><code>' + mistune.escape(code) + '</code></pre>'
markdown = mistune.create_markdown(renderer=HighlightRenderer())
@ -79,9 +80,9 @@ def fwrite(filename, text):
f.write(text)
def log(msg, *args):
def log(msg, *log_args):
"""Log message with specified arguments."""
sys.stderr.write(msg.format(*args) + "\n")
sys.stderr.write(msg.format(*log_args) + "\n")
def truncate(text, words=25):
@ -99,10 +100,12 @@ def read_headers(text):
def rfc_2822_format(date_str):
"""Convert yyyy-mm-dd date string to RFC 2822 format date string."""
# Use datetime.datetime.strptime() with the correct format string
d = datetime.datetime.strptime(date_str, "%Y-%m-%d")
dtuple = d.timetuple()
dtimestamp = time.mktime(dtuple)
return utils.formatdate(dtimestamp)
# Convert the timetuple to a timestamp
timestamp = time.mktime(d.timetuple())
# Return the formatted timestamp
return utils.formatdate(timestamp)
def slugify(value):
@ -111,11 +114,10 @@ def slugify(value):
underscores) and converts spaces to hyphens. Also strips leading and
trailing whitespace.
"""
value = (
unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii")
)
value = re.sub("[^\w\s-]", "", value).strip().lower()
return re.sub("[-\s]+", "-", value)
value = unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii")
value = re.sub(r"[^\w\s-]", "", value) # Remove non-word characters and spaces
value = re.sub(r"\s+", "-", value) # Replace multiple spaces with a single hyphen
return value.lower() # Convert to lowercase
def read_content(filename):

44
makesite.spec Normal file
View file

@ -0,0 +1,44 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
['makesite.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='makesite',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

View file

@ -12,10 +12,6 @@ Supprimer les dépendances des paquets orphelins
pacman -Rsn $(pacman -Qtdq)
Nettoyage du cache pacman des archives de +90 jours
find ./ -maxdepth 1 -type f -mtime +90 -print0 | xargs -0 sudo /bin/rm -f
## *Downgrader* des paquets
Récupérer la liste des upgraded ([source](https://wiki.archlinux.org/title/Downgrading_packages))
@ -40,7 +36,6 @@ for i in $(cat /tmp/packages); do sudo pacman --noconfirm -U "$i"*.zst; done
(potentiel problème d'ordre, réarranger le fichier /tmp/packages en fonction des dépendances entre les paquets)
# Fedora
Historique des transactions **dnf**

View file

@ -1,11 +0,0 @@
<!-- title: Thème : cap au Nord -->
<!-- category: GNU/Linux -->
Je ne pratique pas le *ricing* et je me contente d'un thème par défaut sur mes environnements de bureau, que ce soit Gnome, Mate ou XFCE, surtout que les thèmes fournis par défaut sont généralement bien soignés de nos jours. Mais il y a un domaine où j'ai mes exigences, c'est le terminal : c'est important de ne pas s'esquinter les yeux. Or une palette de couleur mal choisie peut compliquer la vie avec la coloration syntaxique à gogo. D'abord séduit par le classieux Solaris Dark je me suis tourné depuis deux ans vers le thème Nord. Moins contrasté et très homogène, je l'ai progressivement intégré à mes applications quotidiennes : Alacritty, Tmux, NeoVim, IntelliJ, Firefox, la page de recherche DuckDuckGo, le thème GTK. Bref je ne fais pas de *ricing* mais j'ai déjà abordé quelques applications de bureau.
Mes sources d'informations sont les suivantes :
- Sven Greb, celui par qui [tout a commencé](https://github.com/orgs/nordtheme/discussions/183) et son site officiel [NordTheme](https://www.nordtheme.com/)
- la communauté sur Reddit : [r/nordtheme](https://www.reddit.com/r/nordtheme/)
et la configuration de mes postes (toujours gérée par [ChezMoi](https://www.chezmoi.io/)) est sur [mon GitHub](https://github.com/kianby/dotfiles) et répliquée sur [mon Gitea](https://source.madyanne.fr/yax/dotfiles).

View file

@ -1,22 +0,0 @@
<!-- title: Retour en auto-hébergement -->
<!-- category: Hébergement -->
Après une dizaine d'années à héberger mes services chez différents hébergeurs je reviens à de l'auto-hébergement sur du matériel à la maison. Et quelle évolution au niveau matériel : en 2013 c'était un portable Céléron peut véloce dans un placard. En cette fin d'année j'ai acquis un mini-pc sur les conseils avisés de [MiniMachines](https://www.minimachines.net) : un Beelink Mini S12 Pro: pour la taille d'un Rubik's Cube on a un processeur Intel N100 (de la famille Alder Lake) appuyé par 16 Go de RAM et 512 Go de stockage en M.2. Ça coûte un peu plus de 200 euros et c'est très raisonnable par rapport à ce qu'on obtient en retour : une machine avec une consommation électrique sobre, quasi-silencieuse et des performances suffisantes pour des usages standards en auto-hébergement.
Pour moi l'objectif est double :
- financier d'abord avec l'opportunité de réduire mes factures bien que je sois très satisfait de mes services externes : résilier le cloud Infomaniak et le petit VPS me fera économiser d'une bonne centaine d'euros par an.
- apprendre en s'amusant : refaire de l'administration système, découvrir des nouveaux services à héberger, ça ouvre plein de possibilités.
Joli projet sur le papier mais moins simple qu'en 2012 car l'Internet est sacrément plus hostile et une grosse réflexion sur la sécurité s'est imposée avant douvrir quoi que ce soit sur Internet. Finalement la solution sera basée sur l'hyperviseur Proxmox qui apporte une souplesse sur les types de déploiement en permettant de mixer des conteneurs LXC et des machines virtuelles KVM et d'apporter une brique de sécurité avec un pare-feu à multiple niveaux. Le but étant d'isoler autant que possible les parties exposées du réseau domestique. Les machines virtuelles [regrouperont des services Docker](https://github.com/kianby/selfhosting/tree/config-vm1) exposés indirectement par un proxy NginX.
![Proxmox](/images/2024/proxmox.svg)
Le proxy NginX est directement exposé sur Internet via une redirection des ports HTTP / HTTPs depuis la box internet. C'est un container LXC Alpine avec une installation de [Nginx Proxy Manager](https://nginxproxymanager.com/) modifiée pour que les services ne s'exécutent par avec le super-utilisateur *root*. Le minimum de paquets est installé (surtout pas de service SSHD ni de SSH) et il s'administre par l'interface Web de Nginx Proxy Manager depuis le réseau local qui n'est évidemment pas exposée à l'extérieur. Il a deux cartes réseau virtuelles : une adresse sur le réseau local et l'autre sur le réseau privé constitué des machines virtuelles exécutant les services. Le proxy sert aussi de passerelle de sortie aux machines virtuelles : le routage est activé entre les deux interfaces et l'interface du réseau privé est *bridgée* sur l'interface réseau locale.
Les machines virtuelles exécutant les services appartiennent au réseau privé et le pare-feu de l'hyperviseur bloque le trafic pour qu'elles ne puisse communiquer qu'avec la passerelle. En cas de compromission elles n'ont accès ni au réseau local ni au bastion. Chaque machine virtuelle est accessible par SSH depuis le bastion, qui est un simple conteneur LXC Alpine avec deux cartes réseau qui permet par rebond d'accéder aux machines virtuelles depuis un PC du réseau local. Excepté la console Web de Proxmox c'est le seul moyen d'accéder aux machines virtuelles. Les accès SSH sont protégés par échange de clefs (aucun accès autorisé par mot de passe) et seul le bastion est autorisé. Corollaire : n'importe quelle machine du réseau local ayant un accès SSH au bastion peut accéder aux machines virtuelles.
![Archi réseau](/images/2024/archi-lan.svg)
Voilà c'est sûrement perfectible mais j'ai jugé la solution suffisamment sécurisée pour la mettre en service. La machine "vm2" avec Nextcloud et Immlich est encore un projet mais la machine "vm1" exécutant tous mes services de base est déjà opérationnelle.

View file

@ -1,27 +0,0 @@
<!-- title: Analyse de logs -->
<!-- category: Hébergement -->
Après quelques semaines [sur ma nouvelle installation de serveur](/2024/retour-en-auto-hebergement/) j'ai eu besoin de visibilité sur les visites pour estimer si la sécurité devait être renforcée. De longue date j'avais mis en favori l'outil [GoAccess](https://goaccess.io) et il semblait correspondre à ce que je cherchais, à savoir un outil passif de génération de rapports basé sur les logs d'accès du serveur HTTP : pas d'analyse des IP source mais des statistiques d'accès par site, une répartition des *User Agent* utilisés avec une reconnaissance des bots (ou crawlers) qui représentent le plus gros du trafic, les erreurs HTTP, les URI les plus recherchées ...
A ma grande satisfaction j'ai mis en place GoAccess en moins de deux heures avec deux étapes :
1. l'installation sur le container LXC Alpine. GoAccess est écrit en langage C et a peu de dépendances pour la compilation. Avec deux ou trois dépendances de librairies ajoutées, la compilation sur le container a été simple.
2. le travail principal consiste à décrire le format du log à analyser dans le formalisme de GoAccess
Nginx Proxy Manager utilise un format de log commun pour tous les proxy ce qui m'a simplifié la tâche. La documentation de GoAccess est exemplaire, notamment le formalisme des logs (https://goaccess.io/man#custom-log).
Pour ce format de log Nginx :
```
log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] [Sent-to $server] "$http_user_agent" "$http_referer"';
```
j'ai défini ces paramètres de lancement :
```
--datetime-format='%d/%b/%Y:%H:%M:%S %z'
--log-format='[%x] - %^ %s - %m %^ %v "%U" [Client %h] [Length %b] [Gzip %^] [Sent-to %^] "%u" "%R"'
```
GoAccess peut générer son rapport HTML à partir d'une liste de fichiers de logs à la demande ou en temps-réel avec un lancement en tâche de fond (option --real-time-html --daemonize). L'option temps-réel est sympa avec une web socket qui rafraîchit la page du navigateur automatiquement mais c'est consommateur en CPU sur ce type d'installation en mini-pc où l'un des objectifs est la sobriété énergétique. J'ai préféré opter pour une génération horaire par une tâche planifiée avec CRON.
GoAccess répond à mon besoin et j'ai commencé à analyser les données pour préparer une phase de renforcement de la sécurité.

View file

@ -1,8 +0,0 @@
<!-- title: Est-ce que tout est politique ? -->
<!-- category: Humeur -->
Le retour en auto-hébergement avec [l'achat d'un mini-pc](/2024/retour-en-auto-hebergement/) a été ma meilleure idée de fin d'année. Et ce n'est pas suffisant car j'ai encore beaucoup de dépendance à des services U.S. Même si je voyais d'un œil inquiet la réélection de Donald (pas le canard rigolo qui a bercé mon enfance... l'autre), jamais je n'aurais imaginé le système fasciste qui se met en place depuis le début d'année. Naïf, je pensais qu'il y avait des garde-fous, qu'on ne pouvait pas poignarder un système démocratique sans qu'une instance siffle un arrêt de jeu, mais je me trompais lourdement. Je ne rentrerais pas dans le détail des mesures appliquées par ce nouveau gouvernement sinon cet article va devenir trop triste ; j'ai juste le cœur qui saigne pour tous ceux qui en souffrent, sûrement une vilaine faiblesse de mon côté woke ;-)
Depuis tant d'années nous baignons dans ce monde du partage communautaire et multiculturel autour de Linux, des projets libres et opensource. Alors est-ce qu'on fait de la politique ? Pas forcément consciemment mais nous essayons de vivre en accord avec nos valeur et nos goûts reflètent une vision du monde (pas tel qu'il est malheureusement mais plutôt comme nous le rêvons). Le durcissement des gouvernements au détriment de l'humain (en piétinant d'abord les minorités) et de la nature, le repli en mode écureuil (tiens si je prends le Groenland je pourrais assurer le même train de vie pendant encore quelques dizaines d'année et faire croire que tout va bien ?) nous expose peu à peu. Si on n'est pas d'accord avec cela car nous avons d'autres valeurs plus humanistes alors je crains que nous ayons une posture politique à l'insu de nous-mêmes même :-) Le fait qu'on ait jamais voté ou qu'on soit fidèle aux urnes ne change rien à l'affaire : il y a une majorité silencieuse qui n'approuve pas, ne défile pas, du moins j'aime à le croire. Quelle est la limite qui fait basculer de l'indignation à l'action, je me pose la question... à moi-même en premier lieu.
L'année va être intéressante mais pas forcément joyeuse. Prenez soin de vous, mentalement aussi, car la période est malsaine.

View file

@ -1,20 +0,0 @@
<!-- title: Migration Git et optimisation Docker -->
<!-- category: Hébergement -->
J'ai quitté GitHub (entre autres) ! Enfin presque car je n'ai pas encore recensé les sites où mon compte GitHub sert d'authentification mais j'ai fait le plus dur : supprimer tous mes projets et ne migrer que ceux encore actifs ; un peu dur car supprimer des projets, mêmes archivés, pour un développeur c'est un crève-cœur mais le spleen est vite passé avec l'engouement de repartir à zéro sur une plate-forme plus conforme à mes valeurs, la forge de [Zaclys](https://www.zaclys.com) et je les remercie grandement d'avoir ajouté ce service à leur offre "Famille".
Après migration, il me reste quatre projets actifs (hé oui il ne reste que ça) à savoir mes configurations (dotfiles), le déploiement par docker compose (selfhosting), le gestionnaire de commentaires (stacosys) et le blog. Vous avez noté la subtilité pour être retrouvé sur la [forge de Zaclys](https://gitea.zaclys.com) sans donner explicitement le lien de mon espace dans l'article ;-)
Sur mon infrastructure domestique (le mini-pc Beelink propulsé par l'hyperviseur Proxmox) j'ai déjà une instance Gitea qui réplique mes projets à l'extérieur, ça me garantit d'avoir une copie à jour en cas de gros pépin. Bref après avoir migré mes projets je me suis trouvé devant la problématique de construire et publier les images Docker de stacosys et du blog puisque cette tâche était dévolue aux actions GitHub. Je ne sais pas si la forge Zaclys propose un équivalent mais je trouve inefficace de publier ailleurs des images prêtes à l'emploi qui ne servent qu'à moi, pour ensuite les rapatrier en local. En plus dans la foulée de GitHub j'avais bien apprécié de supprimer mon compte Docker Hub enfin désactiver car la suppression ne semble pas possible :-(
En cherchant une alternative pour construire mes images localement et les transférer vers mon infra, j'ai pensé à Gitea qui a aussi la fonction de "docker registry" appelée *packages*. Je suis donc parti là dessus, une publication à la demande, pas de CI ; vu le peu d'activité des projets ça fait parfaitement l'affaire.
> C'est quand on paye de sa poche qu'on prend conscience de la valeur des choses
Après avoir mis ça en place... un changement d'URL dans 2 projets j'ai trouvé assez long la publication de l'image du blog dans Gitea alors que tout est sur le LAN. Et pour cause, elle faisait 1,2 Go alors que celle de stacosys tient dans 74 Mo. Évidemment, en tout automatisé dans une CI ça ne m'avait pas alerté auparavant. Me voilà donc à m'intéresser à un génial outil d'analyse d'images [Dive](https://github.com/wagoodman/dive) qu'on peut même exécuter en container,
alias dive="docker run -ti --rm -v /var/run/docker.sock:/var/run/docker.sock docker.io/wagoodman/dive"
à lire un peu de littérature sur les build multi-stages et à analyser où ça pêche... Bon, ça pêche à plusieurs endroits : trop de couches, installation d'outils de développement pour construire les locales MUSL de la distribution Alpine alors que sur les versions récentes elles sont proposés dans les dépôts. Après une phase d'optimisation j'ai pu ramener l'image à 128 Mo, honnête à mon sens pour un container avec Nginx, Python et des traitements Bash qui font du "git clone".

View file

@ -1,19 +1,28 @@
[project]
name = "blog"
version = "1.2"
version = "1.1"
description = "Blog du Yax"
readme = "README.md"
authors = [
{ name = "Yax" }
]
requires-python = ">=3.12.9"
readme = "README.md"
requires-python = ">= 3.8"
dependencies = [
"requests>=2.31.0",
"pygments>=2.17.1",
"mistune>=3.0.2",
"pygments>=2.18.0",
"requests>=2.32.3",
]
[dependency-groups]
dev = [
"black>=24.10.0",
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.rye]
managed = true
dev-dependencies = [
"black>=23.10.1",
"pyinstaller>=6.1.0",
]
[tool.hatch.metadata]
allow-direct-references = true

29
requirements-dev.lock Normal file
View file

@ -0,0 +1,29 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: false
-e file:.
altgraph==0.17.4
black==23.11.0
certifi==2023.11.17
charset-normalizer==3.3.2
click==8.1.7
idna==3.4
mistune==3.0.2
mypy-extensions==1.0.0
packaging==23.2
pathspec==0.11.2
platformdirs==4.0.0
pygments==2.17.1
pyinstaller==6.2.0
pyinstaller-hooks-contrib==2023.10
requests==2.31.0
tomli==2.0.1
typing-extensions==4.8.0
urllib3==2.1.0
# The following packages are considered to be unsafe in a requirements file:
setuptools==68.2.2

16
requirements.lock Normal file
View file

@ -0,0 +1,16 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: false
-e file:.
certifi==2023.11.17
charset-normalizer==3.3.2
idna==3.4
mistune==3.0.2
pygments==2.17.1
requests==2.31.0
urllib3==2.1.0

View file

@ -1,47 +0,0 @@
vars: {
d2-config: {
layout-engine: elk
# Terminal theme code
theme-id: 300
}
}
dmz: {
nginx proxy: {
icon: https://icons.terrastruct.com/dev%2Fnginx.svg
shape: rectangle
}
}
private network: {
label.near: bottom-center
virtual machines: {
icon: https://icons.terrastruct.com/gcp%2FProducts%20and%20services%2FNetworking%2FVirtual%20Private%20Cloud.svg
style.multiple: true
containers: {
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
style.multiple: true
}
}
}
lan: {
bastion: {
icon: https://icons.terrastruct.com/dev%2Fssh.svg
}
devices: {
icon: https://icons.terrastruct.com/tech%2Flaptop.svg
style.multiple: true
}
}
internet box: {
icon: https://icons.terrastruct.com/tech%2Frouter.svg
width: 130
}
internet box -> dmz.nginx proxy: http/https
lan.bastion -> private network.virtual machines: ssh
dmz.nginx proxy -> private network.virtual machines: http

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 50 KiB

View file

@ -1,76 +0,0 @@
vars: {
d2-config: {
layout-engine: elk
# Terminal theme code
theme-id: 4
}
}
Proxmox: {
icon: https://www.svgrepo.com/download/331552/proxmox.svg
label: "Hyperviseur Proxmox"
Containers: {
label: "Containers LXC"
Nginx_Proxy: {
icon: https://upload.wikimedia.org/wikipedia/commons/d/dd/Linux_Containers_logo.svg
label: "Nginx Proxy"
}
Bastion: {
icon: https://upload.wikimedia.org/wikipedia/commons/d/dd/Linux_Containers_logo.svg
label: "Bastion"
}
}
vm1: {
icon : https://upload.wikimedia.org/wikipedia/commons/thumb/7/70/Kvmbanner-logo2_1.png/320px-Kvmbanner-logo2_1.png
label: "vm1"
Heimdall: {
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
label: "Heimdall"
}
Blog: {
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
label: "Blog"
}
Selfoss: {
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
label: "Selfoss"
}
Wallabag: {
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
label: "Wallabag"
}
Shaarli: {
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
label: "Shaarli"
}
}
vm2: {
icon : https://upload.wikimedia.org/wikipedia/commons/thumb/7/70/Kvmbanner-logo2_1.png/320px-Kvmbanner-logo2_1.png
label: "vm2"
style: {
stroke-dash: 3
}
Nextcloud: {
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
label: "Nextcloud"
}
Immich: {
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
label: "Immich"
}
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 177 KiB

0
test/__init__.py Normal file
View file

16
test/path.py Normal file
View file

@ -0,0 +1,16 @@
import os
import tempfile
import shutil
def temppath(*paths):
return os.path.join(tempfile.gettempdir(), *paths)
def move(src, dst):
if os.path.isfile(dst):
os.remove(dst)
elif os.path.isdir(dst):
shutil.rmtree(dst)
if os.path.exists(src):
os.rename(src, dst)

104
test/test_content.py Normal file
View file

@ -0,0 +1,104 @@
import unittest
import shutil
import os
import makesite
from test import path
class ContentTest(unittest.TestCase):
def setUp(self):
self.blog_path = path.temppath('blog')
self.undated_path = os.path.join(self.blog_path, 'foo.txt')
self.dated_path = os.path.join(self.blog_path, '2018-01-01-foo.txt')
self.normal_post_path = os.path.join(self.blog_path, 'baz.txt')
self.md_post_path = os.path.join(self.blog_path, 'qux.md')
self.no_md_post_path = os.path.join(self.blog_path, 'qux.txt')
os.makedirs(self.blog_path)
with open(self.undated_path, 'w') as f:
f.write('hello world')
with open(self.dated_path, 'w') as f:
f.write('hello world')
with open(self.normal_post_path, 'w') as f:
f.write('<!-- a: 1 -->\n<!-- b: 2 -->\nFoo')
with open(self.md_post_path, 'w') as f:
f.write('*Foo*')
with open(self.no_md_post_path, 'w') as f:
f.write('*Foo*')
def tearDown(self):
shutil.rmtree(self.blog_path)
# Rudimentary mock because unittest.mock is unavailable in Python 2.7.
def mock(self, *args):
self.mock_args = args
def test_content_content(self):
content = makesite.read_content(self.undated_path)
self.assertEqual(content['content'], 'hello world')
def test_content_date(self):
content = makesite.read_content(self.dated_path)
self.assertEqual(content['date'], '2018-01-01')
def test_content_date_missing(self):
content = makesite.read_content(self.undated_path)
self.assertEqual(content['date'], '1970-01-01')
def test_content_slug_dated(self):
content = makesite.read_content(self.dated_path)
self.assertEqual(content['slug'], 'foo')
def test_content_slug_undated(self):
content = makesite.read_content(self.undated_path)
self.assertEqual(content['slug'], 'foo')
def test_content_headers(self):
content = makesite.read_content(self.normal_post_path)
self.assertEqual(content['a'], '1')
self.assertEqual(content['b'], '2')
self.assertEqual(content['content'], 'Foo')
def test_markdown_rendering(self):
content = makesite.read_content(self.md_post_path)
self.assertEqual(content['content'], '<p><em>Foo</em></p>\n')
def test_markdown_import_error(self):
makesite._test = 'ImportError'
original_log = makesite.log
makesite.log = self.mock
self.mock_args = None
content = makesite.read_content(self.md_post_path)
makesite._test = None
makesite.log = original_log
self.assertEqual(content['content'], '*Foo*')
self.assertEqual(self.mock_args,
('WARNING: Cannot render Markdown in {}: {}',
self.md_post_path, 'Error forced by test'))
def test_no_markdown_rendering(self):
content = makesite.read_content(self.no_md_post_path)
self.assertEqual(content['content'], '*Foo*')
def test_no_markdown_import_error(self):
makesite._test = 'ImportError'
original_log = makesite.log
makesite.log = self.mock
self.mock_args = None
content = makesite.read_content(self.no_md_post_path)
makesite._test = None
makesite.log = original_log
self.assertEqual(content['content'], '*Foo*')
self.assertIsNone(self.mock_args)

39
test/test_file_io.py Normal file
View file

@ -0,0 +1,39 @@
import unittest
import os
import shutil
import makesite
from test import path
class FileIOTest(unittest.TestCase):
"""Tests for file I/O functions."""
def test_fread(self):
text = 'foo\nbar\n'
filepath = path.temppath('foo.txt')
with open(filepath, 'w') as f:
f.write(text)
text_read = makesite.fread(filepath)
os.remove(filepath)
self.assertEqual(text_read, text)
def test_fwrite(self):
text = 'baz\nqux\n'
filepath = path.temppath('foo.txt')
makesite.fwrite(filepath, text)
with open(filepath) as f:
text_read = f.read()
os.remove(filepath)
self.assertEqual(text_read, text)
def test_fwrite_makedir(self):
text = 'baz\nqux\n'
dirpath = path.temppath('foo', 'bar')
filepath = os.path.join(dirpath, 'foo.txt')
makesite.fwrite(filepath, text)
with open(filepath) as f:
text_read = f.read()
self.assertTrue(os.path.isdir(dirpath))
shutil.rmtree(path.temppath('foo'))
self.assertEqual(text_read, text)

43
test/test_headers.py Normal file
View file

@ -0,0 +1,43 @@
import unittest
import makesite
class HeaderTest(unittest.TestCase):
"""Tests for read_headers() function."""
def test_single_header(self):
text = '<!-- key1: val1 -->'
headers = list(makesite.read_headers(text))
self.assertEqual(headers, [('key1', 'val1', 19)])
def test_multiple_headers(self):
text = '<!-- key1: val1 -->\n<!-- key2: val2-->'
headers = list(makesite.read_headers(text))
self.assertEqual(headers, [('key1', 'val1', 20), ('key2', 'val2', 38)])
def test_headers_and_text(self):
text = '<!-- a: 1 -->\n<!-- b: 2 -->\nFoo\n<!-- c: 3 -->'
headers = list(makesite.read_headers(text))
self.assertEqual(headers, [('a', '1', 14), ('b', '2', 28)])
def test_headers_and_blank_line(self):
text = '<!-- a: 1 -->\n<!-- b: 2 -->\n\n<!-- c: 3 -->\n'
headers = list(makesite.read_headers(text))
self.assertEqual(headers, [('a', '1', 14),
('b', '2', 29),
('c', '3', 43)])
def test_multiline_header(self):
text = '<!--\na: 1 --><!-- b:\n2 -->\n<!-- c: 3\n-->'
headers = list(makesite.read_headers(text))
self.assertEqual(headers, [('a', '1', 13),
('b', '2', 27),
('c', '3', 40)])
def test_no_header(self):
headers = list(makesite.read_headers('Foo'))
self.assertEqual(headers, [])
def test_empty_string(self):
headers = list(makesite.read_headers(''))
self.assertEqual(headers, [])

46
test/test_list.py Normal file
View file

@ -0,0 +1,46 @@
import unittest
import shutil
import os
import makesite
from test import path
class PagesTest(unittest.TestCase):
def setUp(self):
self.site_path = path.temppath('site')
def tearDown(self):
shutil.rmtree(self.site_path)
def test_list(self):
posts = [{'content': 'Foo'}, {'content': 'Bar'}]
dst = os.path.join(self.site_path, 'list.txt')
list_layout = '<div>{{ content }}</div>'
item_layout = '<p>{{ content }}</p>'
makesite.make_list(posts, dst, list_layout, item_layout)
with open(os.path.join(self.site_path, 'list.txt')) as f:
self.assertEqual(f.read(), '<div><p>Foo</p><p>Bar</p></div>')
def test_list_params(self):
posts = [{'content': 'Foo', 'title': 'foo'},
{'content': 'Bar', 'title': 'bar'}]
dst = os.path.join(self.site_path, 'list.txt')
list_layout = '<div>{{ key }}:{{ title }}:{{ content }}</div>'
item_layout = '<p>{{ key }}:{{ title }}:{{ content }}</p>'
makesite.make_list(posts, dst, list_layout, item_layout,
key='val', title='lorem')
with open(os.path.join(self.site_path, 'list.txt')) as f:
text = f.read()
self.assertEqual(text,
'<div>val:lorem:<p>val:foo:Foo</p><p>val:bar:Bar</p></div>')
def test_dst_params(self):
posts = [{'content': 'Foo'}, {'content': 'Bar'}]
dst = os.path.join(self.site_path, '{{ key }}.txt')
list_layout = '<div>{{ content }}</div>'
item_layout = '<p>{{ content }}</p>'
makesite.make_list(posts, dst, list_layout, item_layout, key='val')
expected_path = os.path.join(self.site_path, 'val.txt')
self.assertTrue(os.path.isfile(expected_path))
with open(expected_path) as f:
self.assertEqual(f.read(), '<div><p>Foo</p><p>Bar</p></div>')

73
test/test_main.py Normal file
View file

@ -0,0 +1,73 @@
import unittest
import makesite
import os
import shutil
import json
from test import path
class MainTest(unittest.TestCase):
def setUp(self):
path.move('_site', '_site.backup')
path.move('params.json', 'params.json.backup')
def tearDown(self):
path.move('_site.backup', '_site')
path.move('params.json.backup', 'params')
def test_site_missing(self):
makesite.main()
def test_site_exists(self):
os.mkdir('_site')
with open('_site/foo.txt', 'w') as f:
f.write('foo')
self.assertTrue(os.path.isfile('_site/foo.txt'))
makesite.main()
self.assertFalse(os.path.isfile('_site/foo.txt'))
def test_default_params(self):
makesite.main()
with open('_site/blog/proin-quam/index.html') as f:
s1 = f.read()
with open('_site/blog/rss.xml') as f:
s2 = f.read()
shutil.rmtree('_site')
self.assertIn('<a href="/">Home</a>', s1)
self.assertIn('<title>Proin Quam - Lorem Ipsum</title>', s1)
self.assertIn('Published on 2018-01-01 by <b>Admin</b>', s1)
self.assertIn('<link>http://localhost:8000/</link>', s2)
self.assertIn('<link>http://localhost:8000/blog/proin-quam/</link>', s2)
def test_json_params(self):
params = {
'base_path': '/base',
'subtitle': 'Foo',
'author': 'Bar',
'site_url': 'http://localhost/base'
}
with open('params.json', 'w') as f:
json.dump(params, f)
makesite.main()
with open('_site/blog/proin-quam/index.html') as f:
s1 = f.read()
with open('_site/blog/rss.xml') as f:
s2 = f.read()
shutil.rmtree('_site')
self.assertIn('<a href="/base/">Home</a>', s1)
self.assertIn('<title>Proin Quam - Foo</title>', s1)
self.assertIn('Published on 2018-01-01 by <b>Bar</b>', s1)
self.assertIn('<link>http://localhost/base/</link>', s2)
self.assertIn('<link>http://localhost/base/blog/proin-quam/</link>', s2)

127
test/test_pages.py Normal file
View file

@ -0,0 +1,127 @@
import unittest
import os
import shutil
import makesite
from test import path
class PagesTest(unittest.TestCase):
def setUp(self):
self.blog_path = path.temppath('blog')
self.site_path = path.temppath('site')
os.makedirs(self.blog_path)
with open(os.path.join(self.blog_path, 'foo.txt'), 'w') as f:
f.write('Foo')
with open(os.path.join(self.blog_path, 'bar.txt'), 'w') as f:
f.write('Bar')
with open(os.path.join(self.blog_path, '2018-01-01-foo.txt'), 'w') as f:
f.write('Foo')
with open(os.path.join(self.blog_path, '2018-01-02-bar.txt'), 'w') as f:
f.write('Bar')
with open(os.path.join(self.blog_path, 'header-foo.txt'), 'w') as f:
f.write('<!-- tag: foo -->Foo')
with open(os.path.join(self.blog_path, 'header-bar.txt'), 'w') as f:
f.write('<!-- title: bar -->Bar')
with open(os.path.join(self.blog_path, 'placeholder-foo.txt'), 'w') as f:
f.write('<!-- title: foo -->{{ title }}:{{ author }}:Foo')
with open(os.path.join(self.blog_path, 'placeholder-bar.txt'), 'w') as f:
f.write('<!-- title: bar --><!-- render: yes -->{{ title }}:{{ author }}:Bar')
def tearDown(self):
shutil.rmtree(self.blog_path)
shutil.rmtree(self.site_path)
def test_pages_undated(self):
src = os.path.join(self.blog_path, '[fb]*.txt')
dst = os.path.join(self.site_path, '{{ slug }}.txt')
tpl = '<div>{{ content }}</div>'
makesite.make_pages(src, dst, tpl)
with open(os.path.join(self.site_path, 'foo.txt')) as f:
self.assertEqual(f.read(), '<div>Foo</div>')
with open(os.path.join(self.site_path, 'bar.txt')) as f:
self.assertEqual(f.read(), '<div>Bar</div>')
def test_pages_dated(self):
src = os.path.join(self.blog_path, '2*.txt')
dst = os.path.join(self.site_path, '{{ slug }}.txt')
tpl = '<div>{{ content }}</div>'
makesite.make_pages(src, dst, tpl)
with open(os.path.join(self.site_path, 'foo.txt')) as f:
self.assertEqual(f.read(), '<div>Foo</div>')
with open(os.path.join(self.site_path, 'bar.txt')) as f:
self.assertEqual(f.read(), '<div>Bar</div>')
def test_pages_layout_params(self):
src = os.path.join(self.blog_path, '2*.txt')
dst = os.path.join(self.site_path, '{{ slug }}.txt')
tpl = '<div>{{ slug }}:{{ title }}:{{ date }}:{{ content }}</div>'
makesite.make_pages(src, dst, tpl, title='Lorem')
with open(os.path.join(self.site_path, 'foo.txt')) as f:
self.assertEqual(f.read(), '<div>foo:Lorem:2018-01-01:Foo</div>')
with open(os.path.join(self.site_path, 'bar.txt')) as f:
self.assertEqual(f.read(), '<div>bar:Lorem:2018-01-02:Bar</div>')
def test_pages_return_value(self):
src = os.path.join(self.blog_path, '2*.txt')
dst = os.path.join(self.site_path, '{{ slug }}.txt')
tpl = '<div>{{ content }}</div>'
posts = makesite.make_pages(src, dst, tpl)
self.assertEqual(len(posts), 2)
self.assertEqual(posts[0]['date'], '2018-01-02')
self.assertEqual(posts[1]['date'], '2018-01-01')
def test_content_header_params(self):
# Test that header params from one post is not used in another
# post.
src = os.path.join(self.blog_path, 'header*.txt')
dst = os.path.join(self.site_path, '{{ slug }}.txt')
tpl = '{{ title }}:{{ tag }}:{{ content }}'
makesite.make_pages(src, dst, tpl)
with open(os.path.join(self.site_path, 'header-foo.txt')) as f:
self.assertEqual(f.read(), '{{ title }}:foo:Foo')
with open(os.path.join(self.site_path, 'header-bar.txt')) as f:
self.assertEqual(f.read(), 'bar:{{ tag }}:Bar')
def test_content_no_rendering(self):
# Test that placeholders are not populated in content rendering
# by default.
src = os.path.join(self.blog_path, 'placeholder-foo.txt')
dst = os.path.join(self.site_path, '{{ slug }}.txt')
tpl = '<div>{{ content }}</div>'
makesite.make_pages(src, dst, tpl, author='Admin')
with open(os.path.join(self.site_path, 'placeholder-foo.txt')) as f:
self.assertEqual(f.read(), '<div>{{ title }}:{{ author }}:Foo</div>')
def test_content_rendering_via_kwargs(self):
# Test that placeholders are populated in content rendering when
# requested in make_pages.
src = os.path.join(self.blog_path, 'placeholder-foo.txt')
dst = os.path.join(self.site_path, '{{ slug }}.txt')
tpl = '<div>{{ content }}</div>'
makesite.make_pages(src, dst, tpl, author='Admin', render='yes')
with open(os.path.join(self.site_path, 'placeholder-foo.txt')) as f:
self.assertEqual(f.read(), '<div>foo:Admin:Foo</div>')
def test_content_rendering_via_header(self):
# Test that placeholders are populated in content rendering when
# requested in content header.
src = os.path.join(self.blog_path, 'placeholder-bar.txt')
dst = os.path.join(self.site_path, '{{ slug }}.txt')
tpl = '<div>{{ content }}</div>'
makesite.make_pages(src, dst, tpl, author='Admin')
with open(os.path.join(self.site_path, 'placeholder-bar.txt')) as f:
self.assertEqual(f.read(), '<div>bar:Admin:Bar</div>')
def test_rendered_content_in_summary(self):
# Test that placeholders are populated in summary if and only if
# content rendering is enabled.
src = os.path.join(self.blog_path, 'placeholder*.txt')
post_dst = os.path.join(self.site_path, '{{ slug }}.txt')
list_dst = os.path.join(self.site_path, 'list.txt')
post_layout = ''
list_layout = '<div>{{ content }}</div>'
item_layout = '<p>{{ summary }}</p>'
posts = makesite.make_pages(src, post_dst, post_layout, author='Admin')
makesite.make_list(posts, list_dst, list_layout, item_layout)
with open(os.path.join(self.site_path, 'list.txt')) as f:
self.assertEqual(f.read(), '<div><p>{{ title }}:{{ author }}:Foo</p><p>bar:Admin:Bar</p></div>')

78
test/test_path.py Normal file
View file

@ -0,0 +1,78 @@
import unittest
import os
import shutil
from test import path
class PathTest(unittest.TestCase):
def test_temppath(self):
self.assertTrue(path.temppath())
def test_move_existing_file(self):
src = os.path.join(path.temppath(), 'foo.txt')
dst = os.path.join(path.temppath(), 'bar.txt')
with open(src, 'w') as f:
f.write('foo')
path.move(src, dst)
self.assertFalse(os.path.isfile(src))
self.assertTrue(os.path.isfile(dst))
with open(dst) as f:
text = f.read()
os.remove(dst)
self.assertEqual(text, 'foo')
def test_move_missing_file(self):
src = os.path.join(path.temppath(), 'foo.txt')
dst = os.path.join(path.temppath(), 'bar.txt')
path.move(src, dst)
self.assertFalse(os.path.isfile(src))
self.assertFalse(os.path.isfile(dst))
def test_move_file_cleanup(self):
src = os.path.join(path.temppath(), 'foo.txt')
dst = os.path.join(path.temppath(), 'bar.txt')
with open(dst, 'w') as f:
f.write('foo')
path.move(src, dst)
self.assertFalse(os.path.isfile(src))
self.assertFalse(os.path.isfile(dst))
def test_move_existing_dir(self):
src = os.path.join(path.temppath(), 'foo')
srcf = os.path.join(src, 'foo.txt')
dst = os.path.join(path.temppath(), 'bar')
dstf = os.path.join(dst, 'foo.txt')
os.makedirs(src)
with open(srcf, 'w') as f:
f.write('foo')
path.move(src, dst)
self.assertFalse(os.path.isdir(src))
self.assertTrue(os.path.isdir(dst))
with open(dstf) as f:
text = f.read()
shutil.rmtree(dst)
self.assertEqual(text, 'foo')
def test_move_missing_dir(self):
src = os.path.join(path.temppath(), 'foo')
dst = os.path.join(path.temppath(), 'bar')
path.move(src, dst)
self.assertFalse(os.path.isdir(src))
self.assertFalse(os.path.isdir(dst))
def test_move_dir_cleanup(self):
src = os.path.join(path.temppath(), 'foo')
dst = os.path.join(path.temppath(), 'bar')
os.makedirs(dst)
path.move(src, dst)
self.assertFalse(os.path.isdir(src))
self.assertFalse(os.path.isdir(dst))

25
test/test_render.py Normal file
View file

@ -0,0 +1,25 @@
import unittest
import makesite
class RenderTest(unittest.TestCase):
"""Tests for render() function."""
def test_oneline_template(self):
tpl = 'foo {{ key1 }} baz {{ key2 }}'
out = makesite.render(tpl, key1='bar', key2='qux')
self.assertEqual(out, 'foo bar baz qux')
def test_multiline_template(self):
tpl = 'foo {{ key1 }}\nbaz {{ key1 }}'
out = makesite.render(tpl, key1='bar')
self.assertEqual(out, 'foo bar\nbaz bar')
def test_repeated_key(self):
tpl = 'foo {{ key1 }} baz {{ key1 }}'
out = makesite.render(tpl, key1='bar')
self.assertEqual(out, 'foo bar baz bar')
def test_multiline_placeholder(self):
tpl = 'foo {{\nkey1\n}} baz {{\nkey2\n}}'
out = makesite.render(tpl, key1='bar', key2='qux')
self.assertEqual(out, 'foo bar baz qux')

View file

@ -0,0 +1,13 @@
import unittest
import makesite
class RFC822DateTest(unittest.TestCase):
def test_epoch(self):
self.assertEqual(makesite.rfc_2822_format('1970-01-01'),
'Thu, 01 Jan 1970 00:00:00 +0000')
def test_2018_06_16(self):
self.assertEqual(makesite.rfc_2822_format('2018-06-16'),
'Sat, 16 Jun 2018 00:00:00 +0000')

13
test/test_slugify.py Normal file
View file

@ -0,0 +1,13 @@
import unittest
import makesite
class SlugifyTest(unittest.TestCase):
def test_slugify(self):
self.assertEqual(makesite.slugify('NginX est brillant'), 'nginx-est-brillant')
self.assertEqual(makesite.slugify('Bilan hébergement 2023'), 'bilan-hebergement-2023')
self.assertEqual(makesite.slugify('Sécurisation Docker : des pistes'), 'securisation-docker-des-pistes')
self.assertEqual(makesite.slugify('Il court, il court, le furet'), 'il-court-il-court-le-furet')
self.assertEqual(makesite.slugify('De GNU/Linux à gnuSystemlinuxdGnomeOs'), 'de-gnulinux-a-gnusystemlinuxdgnomeos')
self.assertEqual(makesite.slugify('Au fait... mon téléphone'), 'au-fait-mon-telephone')

9
test/test_truncate.py Normal file
View file

@ -0,0 +1,9 @@
import unittest
import makesite
class TruncateTest(unittest.TestCase):
def test_truncate(self):
long_text = ' \n'.join('word' + str(i) for i in range(50))
expected_text = ' '.join('word' + str(i) for i in range(25))
self.assertEqual(makesite.truncate(long_text), expected_text)