Skip to content

Commit a7486bc

Browse files
authored
Merge pull request #1 from geopozo/andrew/api_refactor
Andrew/api refactor
2 parents b078702 + 924b430 commit a7486bc

28 files changed

Lines changed: 1035 additions & 737 deletions

.github/workflows/ruff.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,3 @@ jobs:
77
steps:
88
- uses: actions/checkout@v4
99
- uses: chartboost/ruff-action@v1
10-
with:
11-
src: './logistro'

.pre-commit-config.yaml

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# See https://pre-commit.com for more information
2+
# See https://pre-commit.com/hooks.html for more hooks
3+
%YAML 1.2
4+
---
5+
exclude: 'site/.*'
6+
repos:
7+
- repo: https://github.com/pre-commit/pre-commit-hooks
8+
rev: v6.0.0
9+
hooks:
10+
- id: trailing-whitespace
11+
- id: end-of-file-fixer
12+
- id: check-yaml
13+
- id: check-added-large-files
14+
- id: check-case-conflict
15+
- id: check-merge-conflict
16+
- id: check-toml
17+
- id: debug-statements
18+
- repo: https://github.com/asottile/add-trailing-comma
19+
rev: v4.0.0
20+
hooks:
21+
- id: add-trailing-comma
22+
- repo: https://github.com/astral-sh/ruff-pre-commit
23+
# Ruff version.
24+
rev: v0.14.3
25+
hooks:
26+
# Run the linter.
27+
- id: ruff
28+
types_or: [python, pyi]
29+
# Run the formatter.
30+
- id: ruff-format
31+
types_or: [python, pyi]
32+
# options: ignore one line things [E701]
33+
- repo: https://github.com/adrienverge/yamllint
34+
rev: v1.37.1
35+
hooks:
36+
- id: yamllint
37+
name: yamllint
38+
description: This hook runs yamllint.
39+
entry: yamllint
40+
language: python
41+
types: [file, yaml]
42+
args: [
43+
'-d',
44+
"{ extends: default, rules: { colons: { max-spaces-after: -1 } } }",
45+
]
46+
- repo: https://github.com/rhysd/actionlint
47+
rev: v1.7.8
48+
hooks:
49+
- id: actionlint
50+
name: Lint GitHub Actions workflow files
51+
description: Runs actionlint to lint GitHub Actions workflow files
52+
language: golang
53+
types: ["yaml"]
54+
files: ^\.github/workflows/
55+
entry: actionlint
56+
- repo: https://github.com/jorisroovers/gitlint
57+
rev: v0.19.1
58+
hooks:
59+
- id: gitlint
60+
name: gitlint
61+
description: Checks your git commit messages for style.
62+
language: python
63+
additional_dependencies: ["./gitlint-core[trusted-deps]"]
64+
entry: gitlint
65+
args: [--staged, --msg-filename]
66+
stages: [commit-msg]
67+
- repo: https://github.com/crate-ci/typos
68+
rev: v1
69+
hooks:
70+
- id: typos
71+
- repo: https://github.com/Yelp/detect-secrets
72+
rev: v1.5.0
73+
hooks:
74+
- id: detect-secrets
75+
name: Detect secrets
76+
language: python
77+
entry: detect-secrets-hook
78+
args: ['']
79+
- repo: https://github.com/rvben/rumdl-pre-commit
80+
rev: v0.0.170 # Use the latest release tag
81+
hooks:
82+
- id: rumdl
83+
# To only check (default):
84+
# args: []
85+
# To automatically fix issues:
86+
# args: [--fix]
87+
- repo: https://github.com/RobertCraigie/pyright-python
88+
rev: v1.1.407 # pin a tag; latest as of 2025-10-01
89+
hooks:
90+
- id: pyright

CHANGELOG.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
v0.2.0
2+
- Refactor to improve internal/external API

README.md

Lines changed: 104 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,112 @@
1-
Later todo
2-
- [ ] Add checking if git dirty or not pushed
3-
- [ ] Add some log statistics
4-
- [ ] Add some firewall statistics
5-
- [ ] Add updates
6-
- [ ] Add changelog (linux)
7-
- [ ] Add news, linux, hackernews?
8-
- [ ] Add website status check
9-
- [ ] Add google analytics check
10-
- [ ] But also constant ingress and event push (tetsuya)
11-
- [ ] Add something that checks on any request
12-
- [ ] Check itself
13-
14-
15-
# Get it up on systemd
1+
# tetsuya
162

17-
```
3+
tetsuya collects information and offers it up as JSON packets and pretty
4+
print strings.
5+
6+
It's *very* easy to extend:
7+
8+
- Write a function (a *service*) that collects information from web/system.
9+
- It takes a `dataclass` as a *config* object (can be empty).
10+
- It returns another `dataclass` as a *report*.
11+
12+
tetsuya offers the *report* object to your user (and your user only) as a JSON
13+
endpoint available via REST and command-line interface.
14+
15+
You will also define two functions on your *report* dataclass: `short()` and
16+
`long()` which can pretty-print the JSON.
17+
18+
It does some helpful stuff automatically:
19+
20+
1. tetsuya can cache your new *service* for you and auto-refresh the cache: all
21+
tetsuya service configs have a cachelife integer and autorefresh boolean.
22+
23+
2. tetsuya will derive the default config from your `dataclass`, but will read a
24+
.toml if you want to change it.
25+
26+
3. tetsuya runs in a client-server model, with a background daemon doing the
27+
work, and a CLI interface for basic control.
28+
29+
Try `uvx tetsuya --help-tree=ascii` to see the whole interface.
30+
31+
## Installation
32+
33+
### Daemon on systemd
34+
35+
Theres a *tetsuya.service* file in the repository.
36+
37+
```bash
1838
mkdir -p ~/.config/systemd/user
1939

2040
systemctl --user daemon-reload
21-
systemctl --user enable --now "$(realpath tetsuya.service)"
41+
systemctl --user enable --now "$(realpath ./tetsuya.service)"
2242

23-
loginctl enable-linger "$USER" # (allow it to start at boot)
43+
loginctl enable-linger "$USER" # (allow it to start at boot even if logged out)
2444

2545
journalctl --user -u tetsuya -f
2646
```
47+
48+
## Roadmap
49+
50+
- [ ] Some early modules:
51+
- [ ] Do a Basic 200 is it good thing
52+
- [ ] Check domains for email record
53+
- [ ] Check SLL
54+
- [ ] Active sessions on linux
55+
- [ ] Updates available
56+
- [ ] Changelog on kernel
57+
58+
```bash
59+
if which yay 1> /dev/null; then
60+
(
61+
set -e
62+
yay -Qu
63+
yay -Pw
64+
checkupdates
65+
)
66+
fi
67+
```
68+
69+
- [ ] Any errors on systemd + --kernel
70+
- [ ] Monarch Money from that guy? - MoneyFlown and redeploy
71+
- [ ] Improve naming and arguments, flags, etc.
72+
- [ ] -> Services to Client
73+
- [ ] Names of services = arguments
74+
- [ ] Add help descriptions
75+
- [ ] Turn off options like --thing, --no-thing, if --no-thing is default?
76+
- [ ] Go back to CLI and do the formatting better
77+
- [ ] Document throughout code
78+
- [ ] If we want to instantiate multiple instances of one service class:
79+
- [ ] Inspect:
80+
- [ ] all instances of get_name()
81+
- [ ] how services are registered
82+
- [ ] Consider Dictionary Storage and Access Here:
83+
- [ ] `_config.config_data`
84+
- [ ] `_timer.timer_tasks`
85+
- [ ] `service manager.??` (Not written at this time)
86+
- [ ] Will have to pass both service obj + class to _config, _timer
87+
- [ ] Make room for customed name in config + `.get_name()`
88+
- [ ] Start will have change (calls a lot of this stuff)
89+
- [ ] Don't enable service until it has a config
90+
- [ ] Allow per app config default generation
91+
- [ ] Upon reload, recalculate active services
92+
- [ ] Config API:
93+
- [ ] Separate Touch and Dump
94+
- [ ] Create persistence:
95+
- [ ] Roundtrip reports upon registering (json and back)
96+
- [ ] Save reports upon running
97+
- [ ] Load reports upong starting
98+
- [ ] Services can subscribe to changes of other services
99+
- [ ] Importing modules dynamically from a subfoler
100+
101+
### Desired Modules
102+
103+
- [ ] Google drive auditor, google accounts/emails
104+
- [ ] Check agreements (modules subscribed to other modules)
105+
- [ ] are we checking all domains that namecheap lists
106+
- [ ] does google list all
107+
- [ ] Analytics summary + link
108+
109+
- [ ] Git status
110+
- [ ] Scrape forum posts
111+
- [ ] Firewall stats
112+
- [ ] process accounting?

pyproject.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ maintainers = [
2626
]
2727

2828
dependencies = [
29+
# "cli-tree[typer]",
2930
"fastapi>=0.118.0",
3031
"httpx>=0.28.1",
3132
"logistro>=1.1.0",
@@ -37,10 +38,13 @@ dependencies = [
3738
"uvicorn>=0.37.0",
3839
]
3940

41+
[tool.uv.sources]
42+
#cli-tree = { path = "../../utilities/cli-tree.git", editable = true }
43+
4044
[project.urls]
4145

4246
[project.scripts]
43-
tetsuya = "tetsuya.app:main"
47+
tetsuya = "tetsuya.service_manager:start_client"
4448

4549
[dependency-groups]
4650
dev = [
@@ -106,3 +110,7 @@ help = "Run test by test, slowly, quitting after first error"
106110
[tool.poe.tasks.filter-test]
107111
cmd = "pytest --log-level=1 -W error -vvvx -rA --capture=no --show-capture=no"
108112
help = "Run any/all tests one by one with basic settings: can include filename and -k filters"
113+
114+
[tool.pyright]
115+
venvPath = "."
116+
venv = ".venv"

src/tetsuya/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
"""Provides a server and client app for checking in on user-space services."""
1+
"""Tetsuya collects information turns it into REST/cli endpoints."""

src/tetsuya/_config.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Tools for managing the global config."""
2+
3+
from __future__ import annotations
4+
5+
import tomllib
6+
from pathlib import Path
7+
from typing import TYPE_CHECKING
8+
9+
import logistro
10+
import platformdirs
11+
import tomli_w
12+
13+
if TYPE_CHECKING:
14+
from typing import Any
15+
16+
from .services._base import Han, Settei
17+
18+
_logger = logistro.getLogger(__name__)
19+
20+
config_file = (
21+
Path(platformdirs.user_config_dir("tetsuya", "pikulgroup")) / "config.toml"
22+
)
23+
24+
config_data: dict[Any, Any] = {}
25+
26+
27+
def load_config() -> bool:
28+
if config_file.is_file():
29+
with config_file.open("rb") as f:
30+
config_data.clear()
31+
config_data.update(tomllib.load(f))
32+
return True
33+
else:
34+
_logger.info("No config file found.")
35+
return False
36+
37+
38+
def get_active_config(service_def: Han) -> Settei | None:
39+
# could cache
40+
han = service_def
41+
_d = config_data.get(han.service.get_name())
42+
return han.config(**_d) if _d else None
43+
44+
45+
def set_default_config(service_def: Han, *, overwrite: bool = False) -> bool:
46+
key = service_def.service.get_name()
47+
if key in config_data and not overwrite:
48+
return False
49+
config_data[key] = service_def.config.default_config()
50+
return True
51+
52+
53+
def write_config():
54+
with config_file.open("wb") as f:
55+
tomli_w.dump(config_data, f)

src/tetsuya/_process.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Server implements logic for starting and verifying server."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
import sys
7+
from http import HTTPStatus
8+
from typing import TYPE_CHECKING
9+
10+
import httpx
11+
import logistro
12+
import uvicorn
13+
14+
from tetsuya.core import daemon
15+
16+
from .core.utils import get_http_client, uds_path
17+
18+
if TYPE_CHECKING:
19+
from pathlib import Path
20+
21+
_logger = logistro.getLogger(__name__)
22+
23+
24+
def is_server_alive(uds_path: Path) -> bool:
25+
"""Check if server is running."""
26+
if not uds_path.exists():
27+
return False
28+
client: None | httpx.Client = None
29+
try:
30+
client = get_http_client(uds_path, defer_close=False)
31+
r = client.get("/ping")
32+
if r.status_code == HTTPStatus.OK:
33+
_logger.info("Socket ping returned OK- server alive.")
34+
return True
35+
else:
36+
_logger.info(
37+
f"Socket ping returned {r.status_code}, removing socket.",
38+
)
39+
uds_path.unlink()
40+
return False
41+
except httpx.TransportError:
42+
_logger.info("Transport error in socket, removing socket.")
43+
uds_path.unlink()
44+
return False
45+
finally:
46+
if client:
47+
client.close()
48+
49+
50+
async def start():
51+
if not is_server_alive(p := uds_path()):
52+
os.umask(0o077)
53+
_logger.info("Starting server.")
54+
server = uvicorn.Server(
55+
uvicorn.Config(
56+
daemon,
57+
uds=str(p),
58+
loop="asyncio",
59+
lifespan="on",
60+
reload=False,
61+
),
62+
)
63+
64+
await server.serve() # calling the shortcut run would actually block the thread
65+
else:
66+
print("Server already running.", file=sys.stderr) # noqa: T201
67+
sys.exit(1)

0 commit comments

Comments
 (0)