Initial commit with current state

This commit is contained in:
Stuce 2025-11-16 10:56:49 +01:00
commit ca6bdb8890
7 changed files with 294 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
__pycache__
result
config.ini

33
Readme.md Normal file
View file

@ -0,0 +1,33 @@
## Goals
The goal is to make a service between a m5paper and my radicale (caldav) server.
The service will query the caldav server and modify the data in an easy to digest csv that can be sent back and forth without issues.
## Problems to solve
Caldav format too hard to parse. Prefer to use a high level library. High level library can't run on esp32. Need a third party.
## Justification of choices
The time invested in a proper caldav format parser would take too much time reading a specification, and not enough enjoying/learning programming.
An intermediary could prove interesting as I want to use a self-signed cert to authenticate, which I never did (everything goes trough ldap or authelia behind a reverse proxy atm).
It also forces me to learn enough about caldav to be interesting yet not cumbersome. (The rfc doc is very long, and does feel like a chore to read).
Using python is kinda in line with having a radicale server, and shouldn't add too many additional moving pieces. (Few new libraries, and they are generally stable)
Json would be the more portable/self documented choice, but I want to do the parser myself, and a correct json parser is way harder than a csv one. (Especially since it will use c language.)
For simplicity the client can only fetch from a hard coded calendar, this choice is justified by needing to specify the certificate anyway manually, so there is no point in doing a config-free interface.
Always returning the whole list even on a simple post is deemed good practice to have a restfull api that is simple, and we don't care about power consumption/ressource usage of esp32 during post, as thoses are rare and good practices on idle behavior are deemed more important.
Allows a call to fetch the specified caldav (specified in the config, not via a request, as usage)
Nix will be used as it is planned to be deployed on nixos like the rest of my web infrastructure (simply the best to not get overwhelmed by the amount of tweaking I do)
## client facing interface specification
GET / -> gets 10 next tasks of calendar data as a csv containing an id, a boolean value (0 or 1), and a string (terminated by a \n, no carriage return windows bullshit)
POST /id -> checks the item -> returns same as GET /
## parent server facing interface specification
GET -> find a way to get the list of VTODO on the specified calendar.
POST -> find post the updated item
## internal logic
Filter requests by due date and truncate them.
Update an item with respecting repeating events
parse to csv
update list according to csv
conflict management policy

8
default_config.ini Normal file
View file

@ -0,0 +1,8 @@
[calDAV]
address = localhost:5232
username = username
password = password
passwordFile = /path/to/password
[server]
port = 8000

61
flake.lock generated Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1762977756,
"narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

100
flake.nix Normal file
View file

@ -0,0 +1,100 @@
{
description =
"A flake for calDAVtoCSV, a middleman program between my Radicale server and my ESP32";
inputs = {
nixpkgs.url =
"github:nixos/nixpkgs/nixos-unstable"; # Specify the version or channel as needed
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = nixpkgs.legacyPackages.${system};
in {
packages.default = pkgs.python3.pkgs.buildPythonApplication rec {
pname = "calDAVtoCSV";
version = "0.1.0";
pyproject = true;
src = ./.;
build-system = with pkgs.python3.pkgs; [ setuptools ];
dependencies = with pkgs.python3.pkgs; [ caldav flask gunicorn ];
meta = {
description =
"middleman program between my radicale server and my esp32";
license = pkgs.lib.licenses.mit;
maintainers = with pkgs.lib.maintainers; [ stuce-bot ];
};
};
nixosModules.caldavToCsv = { config, lib, ... }:
let cfg = config.services.caldavToCsv;
in {
options.calDavtoCSV = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable myService";
};
config = lib.mkOption {
type = lib.types.attrs;
default = {
calDAV = {
address = "localhost:5232";
username = "username";
password = "password";
passwordFile = "/path/to/password";
};
server = { port = 8000; };
};
description = "User-defined configuration for the service";
};
};
# options.NixosModule = {
# enable = lib.mkEnableOption "Enable calDAVtoCSV service";
# port = lib.mkOption {
# type = lib.types.int;
# default = 8000;
# description = "Port on which calDAVtoCSV will listen";
# };
# url = lib.mkOption {
# type = lib.types.str;
# description = "url of the calendar";
# };
# calendarUsername = lib.mkOption {
# type = lib.types.str;
# description = "username of the calendar account";
# };
# calendarName = lib.mkOption {
# type = lib.types.str;
# description =
# "name of the calendar we will fetch the todo items from";
# };
# calendarPasswordFile = lib.mkOption {
# type = lib.types.str;
# description =
# "file where we need to look for password to connect, needs to be readeable by the service user";
# };
# };
config = lib.mkIf config.myModule.enable {
environment.etc."config.ini".text = ''
${lib.toIni config.myService.config}
'';
systemd.services.calDAVtoCSV = {
description = "calDAV to CSV Service";
after = [ "network.target" ];
serviceConfig = {
ExecStart =
"${pkgs.python3.pkgs.gunicorn}/bin/gunicorn -w 1 -b 0.0.0.0:${cfg.port} main:app";
Restart = "on-failure";
};
wantedBy = [ "multi-user.target" ];
};
};
};
});
}

77
main.py Executable file
View file

@ -0,0 +1,77 @@
#!/usr/bin/env python
from flask import Flask
from caldav import DAVClient, Calendar, Principal, Todo
import configparser
# TODO:
# [x] settings file to not have hardcoded credentials
# [ ] change production credentials
# [ ] nix flake with complete module
# [ ] post behavior (simple)
# [ ] post behavior (concurrent protection)
# [ ] check if better http server availible or fastcgi better suited
#!/usr/bin/env python
def parseConfig():
config = configparser.ConfigParser()
config.read("/home/stuce/calDAVtoCSV/config.ini") # TODO: change this
global calDavAddress
global calDavUsername
global calDavPassword
global calDavCalendarName
global serverPort
calDavAddress = config.get("calDAV", "address")
calDavUsername = config.get("calDAV", "username")
calDavPassword = config.get("calDAV", "password")
calDavPasswordFile = config.get("calDAV", "passwordFile")
calDavCalendarName = config.get("calDAV", "calendarName")
serverPort = config.getint("server", "port")
def fetch_10_next_todos_as_csv() -> str:
def auth() -> Principal:
# TODO: on the final version, fetch it locally
client = DAVClient(
url=calDavAddress, username=calDavUsername, password=calDavPassword
)
principal = client.principal()
return principal
def get_calendar(principal: Principal) -> Calendar:
calendar = principal.calendar(name="Ouais le ménage")
return calendar
def todos_to_csv(todos: list[Todo]) -> str:
result = "".join(
f"{todo.icalendar_component['uid']},{todo.icalendar_component['summary']}\n"
for todo in todos
)
return result
principal = auth()
calendar = get_calendar(principal)
sorted_todos = calendar.todos(sort_keys=("due"))
cut_todos = sorted_todos[:10]
csv = todos_to_csv(cut_todos)
return csv
def put(todos, uid):
item = filter(lambda todo: todo.icalendar_component["uid"] == uid, todos)
app = Flask(__name__)
@app.route("/")
def send_events():
return fetch_10_next_todos_as_csv()
if __name__ == "__main__":
parseConfig()
app.run(host="0.0.0.0", port=serverPort)

12
setup.py Normal file
View file

@ -0,0 +1,12 @@
#!/usr/bin/env python
from setuptools import setup, find_packages
setup(
name="calDAVtoCSV",
version="1.0",
# Modules to import from other scripts:
packages=find_packages(),
# Executables
scripts=["main.py"],
)