owe 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
owe-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 - 2026 Ruoyu Zhong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
owe-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,208 @@
1
+ Metadata-Version: 2.4
2
+ Name: owe
3
+ Version: 0.1.0
4
+ Summary: Bill splitter and tracker.
5
+ Keywords: bill tracking,bill splitting,bill tracker,bill splitter
6
+ Author: Ruoyu Zhong
7
+ Author-email: Ruoyu Zhong <zhongruoyu@outlook.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Environment :: Web Environment
13
+ Classifier: Framework :: Flask
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
23
+ Classifier: Topic :: Utilities
24
+ Requires-Dist: flask>=3.1.3
25
+ Requires-Dist: flask-cors>=6.0.2
26
+ Requires-Dist: gunicorn>=26.0.0
27
+ Requires-Dist: requests>=2.34.2
28
+ Requires-Python: >=3.10
29
+ Project-URL: Homepage, https://github.com/ZhongRuoyu/owe
30
+ Project-URL: Repository, https://github.com/ZhongRuoyu/owe.git
31
+ Description-Content-Type: text/markdown
32
+
33
+ # Owe
34
+
35
+ Owe is a simple web application for splitting bills and tracking who owes whom
36
+ how much.
37
+
38
+ ## Overview
39
+
40
+ Owe lets a group of users record debts and payments between each other.
41
+ The Summary tab shows the minimum set of transactions needed to settle all
42
+ outstanding balances, and provides a one-click Settle button for each.
43
+
44
+ Core concepts:
45
+
46
+ - Users - participants identified by email address.
47
+ - Records - individual transactions of type `DEBT` (someone owes money) or
48
+ `PAYMENT` (money has been physically transferred).
49
+ Records can be cancelled (invalidated) if entered by mistake.
50
+ - Summary - the net balances across all active records, reduced to the minimum
51
+ number of settlement transactions using
52
+ [a simple greedy algorithm](https://stackoverflow.com/a/15723286).
53
+
54
+ ## Requirements
55
+
56
+ - Python 3.10 or later
57
+ - [uv](https://github.com/astral-sh/uv) (recommended) or pip
58
+ - (Optional) A Telegram bot token and chat ID for record notifications.
59
+
60
+ ## Setup
61
+
62
+ You can get started with uv:
63
+
64
+ ```sh
65
+ # Sync dependencies
66
+ uv sync
67
+ # Run the development server
68
+ uv run flask --app owe run
69
+ # Or run the production server with gunicorn
70
+ uv run gunicorn "owe:create_app()"
71
+ ```
72
+
73
+ Or if you prefer pip:
74
+
75
+ ```sh
76
+ # Create a virtual environment
77
+ python3 -m venv .venv
78
+ # Activate the virtual environment
79
+ source .venv/bin/activate
80
+ # Install dependencies
81
+ pip install .
82
+ # Run the development server
83
+ flask --app owe run
84
+ # Or run the production server with gunicorn
85
+ gunicorn "owe:create_app()"
86
+ ```
87
+
88
+ ### App factory customization
89
+
90
+ The application exposes an app factory, `create_app`, with two optional
91
+ keyword arguments:
92
+
93
+ - `url_prefix`: Prefix all registered routes (e.g. `"/owe"`).
94
+ - `api_only`: Register only the API blueprint (skip the bundled UI).
95
+
96
+ For example, to run API-only routes under `/owe`:
97
+
98
+ ```sh
99
+ # With Flask
100
+ uv run flask --app "owe:create_app(api_only=True, url_prefix='/owe')" run
101
+
102
+ # With gunicorn
103
+ uv run gunicorn "owe:create_app(api_only=True, url_prefix='/owe')"
104
+ ```
105
+
106
+ With this configuration, API endpoints are served under `/owe/...`.
107
+
108
+ ### Environment variables
109
+
110
+ The application can be configured with the following environment variables:
111
+
112
+ - `OWE_LOG_LEVEL`: Python logging level (`DEBUG`, `INFO`, `WARNING`, `ERROR`,
113
+ `CRITICAL`).
114
+ Default: `INFO`.
115
+ - `OWE_DATABASE`: Path to the SQLite database file.
116
+ Default: `owe.db`.
117
+ - `OWE_CURRENCY`: ISO 4217 currency code displayed in the UI.
118
+ Default: `USD`.
119
+ - `OWE_REQUEST_EMAIL_HEADER`: HTTP header to trust for the authenticated user's
120
+ email, to be used as the creator of records.
121
+ When run behind Cloudflare Access, this can be set to
122
+ `cf-access-authenticated-user-email` so that the application can identify the
123
+ user by their email address.
124
+ When unset, the remote IP address will be used to identify the user instead.
125
+ Default: unset.
126
+ - `OWE_TELEGRAM_BOT_TOKEN`: Telegram bot token for notifications.
127
+ Default: unset.
128
+ - `OWE_TELEGRAM_CHAT_ID`: Telegram chat ID for notifications.
129
+ Default: unset.
130
+
131
+ ## Docker
132
+
133
+ Docker images are available on Docker Hub as
134
+ [`zhongruoyu/owe`](https://hub.docker.com/r/zhongruoyu/owe),
135
+ and on GitHub Container Registry as
136
+ [`ghcr.io/zhongruoyu/owe`](https://ghcr.io/zhongruoyu/owe).
137
+ The `main` tag tracks the latest commit on the main branch.
138
+
139
+ You may run the application with Docker as follows:
140
+
141
+ ```sh
142
+ docker run -p 8000:8000 \
143
+ -v /path/to/data:/data \
144
+ -e OWE_DATABASE=/data/owe.db \
145
+ -e OWE_CURRENCY=USD \
146
+ zhongruoyu/owe:main --bind "0.0.0.0:8000"
147
+ ```
148
+
149
+ The container image runs gunicorn and accepts its command-line arguments, so you
150
+ can customize the server configuration (e.g. `--bind` for socket binding) using
151
+ standard gunicorn options; run with `--help` for details.
152
+
153
+ ## API Endpoints
154
+
155
+ The server exposes the following API endpoints:
156
+
157
+ - `GET /api/config`: Get application configuration (e.g. currency).
158
+ - `GET /api/users`: List all users.
159
+ - `GET /api/records`: List all active and inactive records.
160
+ - `POST /api/records`: Create a new record.
161
+ Request body should be JSON with the following fields:
162
+ - `type`: "DEBT" or "PAYMENT"
163
+ - `lender`: email of the lender
164
+ - `borrowers`: list of emails of the borrowers
165
+ - `amount`: total amount (will be split evenly among borrowers)
166
+ - `remarks`: optional remarks for the record
167
+ - `PATCH /api/records/status`: Update the status of records.
168
+ Request body should be JSON with the following fields:
169
+ - `ids`: list of record IDs to update
170
+ - `active`: boolean indicating whether the records should be active or
171
+ inactive
172
+ - `GET /api/summary`:
173
+ Get the minimum set of settlement transactions.
174
+
175
+ ## Utilities
176
+
177
+ The package also comes with two command-line utilities:
178
+
179
+ - `owe-dump`: Dump all records in the database as CSV.
180
+ Use `--output` to set the output file path (default: `records.csv`);
181
+ use `--database` to choose the SQLite database file (default: `owe.db`).
182
+ - `owe-users`: Manage users from the command line.
183
+ Use `--database` to choose the SQLite database file (default: `owe.db`).
184
+ Usage:
185
+ - `owe-users [--database <path>] create <email> <name>`:
186
+ Add a user with the given email and name.
187
+ - `owe-users [--database <path>] list`: List all users.
188
+ - `owe-users [--database <path>] activate <email>`:
189
+ Activate the user with the given email.
190
+ - `owe-users [--database <path>] deactivate <email>`:
191
+ Deactivate the user with the given email.
192
+
193
+ To run these utilities:
194
+
195
+ ```sh
196
+ # With uv
197
+ uv run owe-dump --output records.csv
198
+ uv run owe-users --database owe.db list
199
+
200
+ # With pip, after `pip install .`
201
+ owe-dump --output records.csv
202
+ owe-users --database owe.db list
203
+ ```
204
+
205
+ ## License
206
+
207
+ This project is licensed under the MIT License.
208
+ See the [LICENSE](LICENSE) file for details.
owe-0.1.0/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # Owe
2
+
3
+ Owe is a simple web application for splitting bills and tracking who owes whom
4
+ how much.
5
+
6
+ ## Overview
7
+
8
+ Owe lets a group of users record debts and payments between each other.
9
+ The Summary tab shows the minimum set of transactions needed to settle all
10
+ outstanding balances, and provides a one-click Settle button for each.
11
+
12
+ Core concepts:
13
+
14
+ - Users - participants identified by email address.
15
+ - Records - individual transactions of type `DEBT` (someone owes money) or
16
+ `PAYMENT` (money has been physically transferred).
17
+ Records can be cancelled (invalidated) if entered by mistake.
18
+ - Summary - the net balances across all active records, reduced to the minimum
19
+ number of settlement transactions using
20
+ [a simple greedy algorithm](https://stackoverflow.com/a/15723286).
21
+
22
+ ## Requirements
23
+
24
+ - Python 3.10 or later
25
+ - [uv](https://github.com/astral-sh/uv) (recommended) or pip
26
+ - (Optional) A Telegram bot token and chat ID for record notifications.
27
+
28
+ ## Setup
29
+
30
+ You can get started with uv:
31
+
32
+ ```sh
33
+ # Sync dependencies
34
+ uv sync
35
+ # Run the development server
36
+ uv run flask --app owe run
37
+ # Or run the production server with gunicorn
38
+ uv run gunicorn "owe:create_app()"
39
+ ```
40
+
41
+ Or if you prefer pip:
42
+
43
+ ```sh
44
+ # Create a virtual environment
45
+ python3 -m venv .venv
46
+ # Activate the virtual environment
47
+ source .venv/bin/activate
48
+ # Install dependencies
49
+ pip install .
50
+ # Run the development server
51
+ flask --app owe run
52
+ # Or run the production server with gunicorn
53
+ gunicorn "owe:create_app()"
54
+ ```
55
+
56
+ ### App factory customization
57
+
58
+ The application exposes an app factory, `create_app`, with two optional
59
+ keyword arguments:
60
+
61
+ - `url_prefix`: Prefix all registered routes (e.g. `"/owe"`).
62
+ - `api_only`: Register only the API blueprint (skip the bundled UI).
63
+
64
+ For example, to run API-only routes under `/owe`:
65
+
66
+ ```sh
67
+ # With Flask
68
+ uv run flask --app "owe:create_app(api_only=True, url_prefix='/owe')" run
69
+
70
+ # With gunicorn
71
+ uv run gunicorn "owe:create_app(api_only=True, url_prefix='/owe')"
72
+ ```
73
+
74
+ With this configuration, API endpoints are served under `/owe/...`.
75
+
76
+ ### Environment variables
77
+
78
+ The application can be configured with the following environment variables:
79
+
80
+ - `OWE_LOG_LEVEL`: Python logging level (`DEBUG`, `INFO`, `WARNING`, `ERROR`,
81
+ `CRITICAL`).
82
+ Default: `INFO`.
83
+ - `OWE_DATABASE`: Path to the SQLite database file.
84
+ Default: `owe.db`.
85
+ - `OWE_CURRENCY`: ISO 4217 currency code displayed in the UI.
86
+ Default: `USD`.
87
+ - `OWE_REQUEST_EMAIL_HEADER`: HTTP header to trust for the authenticated user's
88
+ email, to be used as the creator of records.
89
+ When run behind Cloudflare Access, this can be set to
90
+ `cf-access-authenticated-user-email` so that the application can identify the
91
+ user by their email address.
92
+ When unset, the remote IP address will be used to identify the user instead.
93
+ Default: unset.
94
+ - `OWE_TELEGRAM_BOT_TOKEN`: Telegram bot token for notifications.
95
+ Default: unset.
96
+ - `OWE_TELEGRAM_CHAT_ID`: Telegram chat ID for notifications.
97
+ Default: unset.
98
+
99
+ ## Docker
100
+
101
+ Docker images are available on Docker Hub as
102
+ [`zhongruoyu/owe`](https://hub.docker.com/r/zhongruoyu/owe),
103
+ and on GitHub Container Registry as
104
+ [`ghcr.io/zhongruoyu/owe`](https://ghcr.io/zhongruoyu/owe).
105
+ The `main` tag tracks the latest commit on the main branch.
106
+
107
+ You may run the application with Docker as follows:
108
+
109
+ ```sh
110
+ docker run -p 8000:8000 \
111
+ -v /path/to/data:/data \
112
+ -e OWE_DATABASE=/data/owe.db \
113
+ -e OWE_CURRENCY=USD \
114
+ zhongruoyu/owe:main --bind "0.0.0.0:8000"
115
+ ```
116
+
117
+ The container image runs gunicorn and accepts its command-line arguments, so you
118
+ can customize the server configuration (e.g. `--bind` for socket binding) using
119
+ standard gunicorn options; run with `--help` for details.
120
+
121
+ ## API Endpoints
122
+
123
+ The server exposes the following API endpoints:
124
+
125
+ - `GET /api/config`: Get application configuration (e.g. currency).
126
+ - `GET /api/users`: List all users.
127
+ - `GET /api/records`: List all active and inactive records.
128
+ - `POST /api/records`: Create a new record.
129
+ Request body should be JSON with the following fields:
130
+ - `type`: "DEBT" or "PAYMENT"
131
+ - `lender`: email of the lender
132
+ - `borrowers`: list of emails of the borrowers
133
+ - `amount`: total amount (will be split evenly among borrowers)
134
+ - `remarks`: optional remarks for the record
135
+ - `PATCH /api/records/status`: Update the status of records.
136
+ Request body should be JSON with the following fields:
137
+ - `ids`: list of record IDs to update
138
+ - `active`: boolean indicating whether the records should be active or
139
+ inactive
140
+ - `GET /api/summary`:
141
+ Get the minimum set of settlement transactions.
142
+
143
+ ## Utilities
144
+
145
+ The package also comes with two command-line utilities:
146
+
147
+ - `owe-dump`: Dump all records in the database as CSV.
148
+ Use `--output` to set the output file path (default: `records.csv`);
149
+ use `--database` to choose the SQLite database file (default: `owe.db`).
150
+ - `owe-users`: Manage users from the command line.
151
+ Use `--database` to choose the SQLite database file (default: `owe.db`).
152
+ Usage:
153
+ - `owe-users [--database <path>] create <email> <name>`:
154
+ Add a user with the given email and name.
155
+ - `owe-users [--database <path>] list`: List all users.
156
+ - `owe-users [--database <path>] activate <email>`:
157
+ Activate the user with the given email.
158
+ - `owe-users [--database <path>] deactivate <email>`:
159
+ Deactivate the user with the given email.
160
+
161
+ To run these utilities:
162
+
163
+ ```sh
164
+ # With uv
165
+ uv run owe-dump --output records.csv
166
+ uv run owe-users --database owe.db list
167
+
168
+ # With pip, after `pip install .`
169
+ owe-dump --output records.csv
170
+ owe-users --database owe.db list
171
+ ```
172
+
173
+ ## License
174
+
175
+ This project is licensed under the MIT License.
176
+ See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,20 @@
1
+ from flask import Flask
2
+ from flask_cors import CORS
3
+
4
+ from . import app as owe_app
5
+ from .config import load_env_config
6
+
7
+
8
+ def create_app(
9
+ *,
10
+ url_prefix: str | None = None,
11
+ api_only: bool = False,
12
+ ) -> Flask:
13
+ """Create and configure the Flask application instance."""
14
+ app = Flask(__name__, static_folder=None)
15
+ app.config.update(load_env_config())
16
+ owe_app.init(app)
17
+ bp = owe_app.api if api_only else owe_app.app
18
+ app.register_blueprint(bp, url_prefix=url_prefix)
19
+ CORS(app)
20
+ return app
owe-0.1.0/owe/app.py ADDED
@@ -0,0 +1,204 @@
1
+ import logging
2
+ import sqlite3
3
+ from html import escape
4
+ from pathlib import Path
5
+ from typing import Any, cast
6
+
7
+ from flask import Blueprint, Flask, Response, current_app, request
8
+
9
+ from . import database, owe
10
+ from .config import AppConfigItems
11
+ from .record import AggregatedRecord
12
+
13
+ API_PREFIX = "/api"
14
+ STATIC_DIR = Path(__file__).resolve().parent / "static"
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def app_config() -> AppConfigItems:
20
+ """Return typed app config values for the current request context."""
21
+ return cast("AppConfigItems", current_app.config)
22
+
23
+
24
+ def init(app: Flask) -> None:
25
+ """Initialize logging and database schema for the app."""
26
+ config = cast("AppConfigItems", app.config)
27
+ logging.basicConfig(
28
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
29
+ level=getattr(logging, config["LOG_LEVEL"], logging.INFO),
30
+ )
31
+ database.init(config["DATABASE"])
32
+
33
+
34
+ def get_requester() -> str:
35
+ """Return requester identity from configured header or remote address."""
36
+ request_email_header = app_config()["REQUEST_EMAIL_HEADER"]
37
+ if request_email_header:
38
+ email = request.headers.get(request_email_header)
39
+ if email:
40
+ return email
41
+ return request.remote_addr or "unknown"
42
+
43
+
44
+ app = Blueprint(
45
+ "owe",
46
+ __name__,
47
+ url_prefix="",
48
+ static_folder=STATIC_DIR,
49
+ static_url_path="",
50
+ )
51
+
52
+
53
+ @app.after_request
54
+ def add_csp(response: Response) -> Response:
55
+ """Attach a strict content security policy to outgoing responses."""
56
+ response.headers["Content-Security-Policy"] = (
57
+ "default-src 'self'; "
58
+ "script-src 'self' https://cdnjs.cloudflare.com; "
59
+ "style-src 'self' https://cdnjs.cloudflare.com; "
60
+ "img-src 'self' data:; "
61
+ )
62
+ return response
63
+
64
+
65
+ @app.route("/")
66
+ def index() -> Response:
67
+ """Serve the single-page application entry point."""
68
+ return app.send_static_file("index.html")
69
+
70
+
71
+ api = Blueprint("api", __name__, url_prefix=API_PREFIX)
72
+ app.register_blueprint(api)
73
+
74
+
75
+ @api.route("/config")
76
+ def get_config() -> dict[str, Any]:
77
+ """Return client-facing configuration values."""
78
+ return {"currency": app_config()["CURRENCY"]}
79
+
80
+
81
+ @api.route("/users")
82
+ def get_users() -> list[dict[str, Any]]:
83
+ """Return active users for UI selection."""
84
+ users = owe.get_active_users()
85
+ return [user.asdict() for user in users]
86
+
87
+
88
+ @api.route("/records")
89
+ def get_records() -> dict[str, dict[str, Any]]:
90
+ """Return all records keyed by record ID as strings."""
91
+ records = owe.get_records()
92
+ return {str(record.id): record.asdict() for record in records}
93
+
94
+
95
+ def validate_add_records_request( # noqa: PLR0911
96
+ req: dict[str, Any],
97
+ valid_emails: set[str],
98
+ ) -> tuple[bool, str]:
99
+ """Validate the add-record request body and referenced user emails."""
100
+ for key in ["type", "lender", "borrowers", "amount", "remarks"]:
101
+ if key not in req:
102
+ return False, f"Missing field: {key}"
103
+
104
+ req_type = req["type"]
105
+ if not isinstance(req_type, str) or req_type not in {"DEBT", "PAYMENT"}:
106
+ return False, 'type must be either "DEBT" or "PAYMENT"'
107
+
108
+ lender = req["lender"]
109
+ if not isinstance(lender, str):
110
+ return False, "lender must be a string"
111
+
112
+ borrowers = req["borrowers"]
113
+ if (
114
+ not isinstance(borrowers, list)
115
+ or not borrowers
116
+ or not all(isinstance(borrower, str) for borrower in borrowers)
117
+ ):
118
+ return False, "borrowers must be a non-empty list of strings"
119
+
120
+ amount = req["amount"]
121
+ if not isinstance(amount, (int, float)) or amount <= 0:
122
+ return False, "amount must be a positive number"
123
+
124
+ remarks = req["remarks"]
125
+ if not isinstance(remarks, str) and remarks is not None:
126
+ return False, "remarks must be a string or null"
127
+
128
+ if lender not in valid_emails:
129
+ return False, f"Unknown lender: {escape(lender)}"
130
+
131
+ if unknown_borrowers := set(borrowers) - valid_emails:
132
+ borrowers_str = escape(
133
+ # https://github.com/astral-sh/ty/issues/521
134
+ ", ".join(email for email in unknown_borrowers), # ty:ignore[no-matching-overload]
135
+ )
136
+ return False, f"Unknown borrower(s): {borrowers_str}"
137
+
138
+ return True, ""
139
+
140
+
141
+ @api.route("/records", methods=["POST"])
142
+ def add_records() -> tuple[dict[str, Any], int]:
143
+ """Create an aggregated record and persist its split entries."""
144
+ req = request.get_json()
145
+ if not req:
146
+ return {"success": False, "error": "Request body must be JSON"}, 400
147
+
148
+ users = owe.get_active_users()
149
+ valid_emails = {u.email for u in users}
150
+ valid, error = validate_add_records_request(req, valid_emails)
151
+ if not valid:
152
+ return {"success": False, "error": error}, 400
153
+
154
+ record = AggregatedRecord(
155
+ type=req["type"],
156
+ lender=req["lender"],
157
+ borrowers=req["borrowers"],
158
+ amount=req["amount"],
159
+ created_by=get_requester(),
160
+ remarks=req["remarks"],
161
+ )
162
+ try:
163
+ owe.add_records(record)
164
+ except sqlite3.Error:
165
+ logger.exception("Database error in add_records")
166
+ return {"success": False, "error": "Database error"}, 500
167
+
168
+ return {"success": True}, 200
169
+
170
+
171
+ @api.route("/records/status", methods=["PATCH"])
172
+ def set_records_active() -> tuple[dict[str, Any], int]:
173
+ """Update the active flag for a batch of records."""
174
+ req = request.get_json()
175
+ if not req:
176
+ return {"success": False, "error": "Request body must be JSON"}, 400
177
+
178
+ ids = req.get("ids", [])
179
+ active = req.get("active")
180
+ if not ids or not all(isinstance(i, int) for i in ids):
181
+ return {
182
+ "success": False,
183
+ "error": "ids must be a non-empty list of integers",
184
+ }, 400
185
+ if not isinstance(active, bool):
186
+ return {"success": False, "error": "active must be a boolean"}, 400
187
+
188
+ try:
189
+ owe.set_records_active(
190
+ ids,
191
+ active=active,
192
+ requester=get_requester(),
193
+ )
194
+ except sqlite3.Error:
195
+ logger.exception("Database error in set_records_active")
196
+ return {"success": False, "error": "Database error"}, 500
197
+
198
+ return {"success": True}, 200
199
+
200
+
201
+ @api.route("/summary")
202
+ def summary() -> list[owe.SummaryTransaction]:
203
+ """Return settlement transactions computed from net balances."""
204
+ return owe.get_summary()
@@ -0,0 +1,24 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import TypedDict
4
+
5
+
6
+ class AppConfigItems(TypedDict):
7
+ LOG_LEVEL: str
8
+ DATABASE: Path
9
+ CURRENCY: str
10
+ REQUEST_EMAIL_HEADER: str | None
11
+ TELEGRAM_BOT_TOKEN: str | None
12
+ TELEGRAM_CHAT_ID: str | None
13
+
14
+
15
+ def load_env_config() -> AppConfigItems:
16
+ """Load application config values from environment variables."""
17
+ return {
18
+ "LOG_LEVEL": os.getenv("OWE_LOG_LEVEL", "INFO").upper(),
19
+ "DATABASE": Path(os.getenv("OWE_DATABASE", "owe.db")),
20
+ "CURRENCY": os.getenv("OWE_CURRENCY", "USD"),
21
+ "REQUEST_EMAIL_HEADER": os.getenv("OWE_REQUEST_EMAIL_HEADER"),
22
+ "TELEGRAM_BOT_TOKEN": os.getenv("OWE_TELEGRAM_BOT_TOKEN"),
23
+ "TELEGRAM_CHAT_ID": os.getenv("OWE_TELEGRAM_CHAT_ID"),
24
+ }