Skip to content

Commit e1f1e52

Browse files
committed
Python 3.12 support, adapt to Headers withut pop
1 parent 06dcb2c commit e1f1e52

File tree

2 files changed

+128
-80
lines changed

2 files changed

+128
-80
lines changed

README.md

Lines changed: 120 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,52 @@
11
# Pycolytics
2-
A tiny webservice for logging software analytics events. It takes HTTP requests, and puts them into an SQLite database,
32

4-
The goal of this library is to be easy to set up and easy to use. Minimal dependencies, no complicated database setups.
3+
A tiny webservice for logging software analytics events.
4+
It takes HTTP requests, and puts them into an SQLite database.
55

6-
- Easy setup on any linux machine, or in wsl:
6+
The goal of this library is to be easy to set up and easy to use.
7+
Minimal dependencies, no complicated database setups.
78

8-
```
9-
git clone [email protected]:KerekesDavid/pycolytics.git
10-
cd pycolytics
11-
pip install -r requirements.txt
12-
```
9+
- Easy setup on any linux machine, or in WSL:
1310

14-
- Launches out of the box:
15-
16-
```
17-
fastapi dev
18-
```
19-
20-
- After launch, API docs are available at: http://127.0.0.1:8000/docs
21-
22-
## How We Got Here
11+
```sh
12+
git clone [email protected]:KerekesDavid/pycolytics.git
13+
cd pycolytics
14+
pip install -r requirements.txt
15+
```
2316

24-
Pycolytics is written in python, based on [SQLite](https://github.com/sqlite/sqlite) and [FastAPI](https://github.com/fastapi/fastapi), and was inspired by [Attolytics](https://github.com/ttencate/attolytics/).
25-
26-
When I was looking at Attolytics, I was too lazy to set up a rust compile environment and install postgresql for something so simple, so I spent two days writing Pycolytics instead. To help you avoid my mistake, I made it so you can just clone it and move on with your life.
17+
- Launches out of the box:
2718

28-
True to its name, Pycolytics is probably 10<sup>6</sup> times slower than Attolytics, but who cares if it still serves my entire userbase from a rasberry-pi. It does asyncio and fancy multi-worker stuff to try and compensate.
19+
```sh
20+
fastapi dev
21+
```
2922

30-
Open an issue if you wish to contribute, or buy me a coffee if you find my work useful.
31-
32-
<a href='https://ko-fi.com/E1E712JJXK' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi3.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
23+
- After launch, API docs are available at: <http://127.0.0.1:8000/docs>
3324

25+
Pycolytics is written in python, based on
26+
[SQLite](https://github.com/sqlite/sqlite) and
27+
[FastAPI](https://github.com/fastapi/fastapi), and was inspired by
28+
[Attolytics](https://github.com/ttencate/attolytics/).
3429

3530
## Client Plugin for Godot 4.2+
36-
- I wrote a plugin for Godot: just install it and call a single function to log an event! You can find it in the [asset library](https://godotengine.org/asset-library/asset/3292), or on [github](https://github.com/KerekesDavid/pycolytics-godot).
3731

38-
- If you have written clients for anything else, I would be more than happy to feature them here!
32+
- I wrote a plugin for Godot: just install it and call a single function to log
33+
an event! You can find it in the
34+
[asset library](https://godotengine.org/asset-library/asset/3292), or on
35+
[github](https://github.com/KerekesDavid/pycolytics-godot).
36+
37+
- If you have written clients for anything else, I would be more than happy to
38+
feature them here!
3939

4040
## Configuration
41+
4142
Edit the .env file, or specify these parameters as environment variables:
42-
```
43+
44+
```sh
4345
# Name of the database file to write into.
4446
SQLITE_FILE_PATH="databases/database.db"
4547

46-
# A list of secret keys. The server won't accept events that do not contain one of these keys in the request body.
48+
# A list of secret keys. The server won't accept events
49+
# that do not contain one of these keys in the request body.
4750
API_KEYS=["I-am-an-unsecure-dev-key-REPLACE_ME"]
4851

4952
# Requests from the same IP above this rate will be rejected.
@@ -52,9 +55,11 @@ RATE_LIMIT="60/minute"
5255
```
5356

5457
## API
55-
The server will listen to POST requests at `http://ip:port/v1.0/event`, and will expect a request body in the following format:
5658

57-
```
59+
The server will listen to POST requests at `http://ip:port/v1.0/event`,
60+
and will expect a request body in the following format:
61+
62+
```json
5863
{
5964
"event_type": "string",
6065
"application": "string",
@@ -65,23 +70,25 @@ The server will listen to POST requests at `http://ip:port/v1.0/event`, and will
6570
"value": {
6671
"event_description": "Life, the universe and everything.",
6772
"event_data": 42
68-
},
73+
},
6974
"api_key": "I-am-an-unsecure-dev-key-REPLACE_ME"
7075
}
7176
```
7277

73-
There is also a more performant batch interface at `http://ip:port/v1.0/events`, expecting a list of events:
74-
```
78+
There is also a more performant batch interface at
79+
`http://ip:port/v1.0/events`, expecting a list of events:
80+
81+
```json
7582
[
76-
{"event_type": ...},
83+
{"event_type": ...},
7784
{"event_type": ...},
7885
...
7986
]
8087
```
8188

8289
An example curl call for logging an event:
8390

84-
```
91+
```sh
8592
curl -X 'POST' \
8693
'http://127.0.0.1:8000/v1.0/event' \
8794
-H 'accept: */*' \
@@ -106,9 +113,11 @@ The `value` field can contain an arbitrary JSON with event details.
106113
The POST request will return `204: No Content` on successful inserts.
107114

108115
## Database
116+
109117
The database will contain an `event` table with all logged events.
110118
The columns are:
111-
```
119+
120+
```sql
112121
event_type VARCHAR NOT NULL
113122
platform VARCHAR NOT NULL
114123
version VARCHAR NOT NULL
@@ -119,82 +128,116 @@ id INTEGER NOT NULL, PRIMARY KEY
119128
time DATETIME NOT NULL
120129
```
121130

122-
It can be opened using any sqlite database browser, or in python using the built in sqlite package.
131+
It can be opened using any sqlite database browser,
132+
or in python using the built in sqlite package.
123133

124-
My personal choice for performing data analytics is a [jupyter notebook](https://jupyter.org/) using [pandas](https://pandas.pydata.org/). They have a wonerful cheat sheet [here](https://pandas.pydata.org/Pandas_Cheat_Sheet.pdf).
134+
My personal choice for performing data analytics is a
135+
[jupyter notebook](https://jupyter.org/) using
136+
[pandas](https://pandas.pydata.org/). They have a wonderful cheat sheet
137+
[here](https://pandas.pydata.org/Pandas_Cheat_Sheet.pdf).
125138

126139
## Launching a Production Server
140+
127141
Setting up a permanent server as a service is also quite simple.
128142

129-
The method I share here has some minimal extra complications, but it ensures some level of separation from other parts of the system using systemd's `DynamicUser` parameter. It might come handy in case there is a vulnerability in FastAPI.
143+
The method I share here has some minimal extra complications, but it ensures
144+
some level of separation from other parts of the system using systemd's
145+
`DynamicUser` parameter. It might come handy in case there is a vulnerability
146+
in FastAPI.
130147

131-
(Contributions to this section are very welcome, I'm barely a fledgeling server admin.)
148+
(Contributions to this section are very welcome, I'm barely a fledgelig server admin.)
132149

133150
- Install pycolytics, and set up a virtualenv.
134151
A usual place for this would be `/srv/pycolytics` for example.
135152

136153
- Create a systemd service file: `/etc/systemd/system/pycolytics.service`
137154

138-
```
139-
[Unit]
140-
Description=Uvicorn instance serving Pycolytics
141-
After=network.target
155+
```INI
156+
[Unit]
157+
Description=Uvicorn instance serving Pycolytics
158+
After=network.target
142159

143-
[Service]
144-
Type=simple
145-
DynamicUser=yes
146-
User=pycolytics
160+
[Service]
161+
Type=simple
162+
DynamicUser=yes
163+
User=pycolytics
147164

148-
WorkingDirectory=/srv/pycolytics
149-
StateDirectory=pycolytics/databases
165+
WorkingDirectory=/srv/pycolytics
166+
StateDirectory=pycolytics/databases
150167

151-
ExecStart=/srv/pycolytics/.venv/bin/uvicorn \
152-
--workers=4 \
153-
--host=0.0.0.0 \
154-
--port=8080 \
155-
app.main:app
156-
ExecReload=/bin/kill -HUP ${MAINPID}
157-
RestartSec=15
158-
Restart=always
168+
ExecStart=/srv/pycolytics/.venv/bin/uvicorn \
169+
--workers=4 \
170+
--host=0.0.0.0 \
171+
--port=8080 \
172+
app.main:app
173+
ExecReload=/bin/kill -HUP ${MAINPID}
174+
RestartSec=15
175+
Restart=always
159176

160-
[Install]
161-
WantedBy=multi-user.target
177+
[Install]
178+
WantedBy=multi-user.target
179+
180+
```
162181

163-
```
164182
- Generate an API key:
165-
```
166-
openssl rand -base64 24
167-
```
168-
This will stop random people from logging events in your database, it will not stop a script kiddie who can decompile the key from your app, or pluck it from network traffic. I'd suggest creating a new one for every version of your application, and retiring old ones after a while.
169183

170-
- Setup the .env file:
171-
- Replace `API_KEYS=["I-am-an-unsecure-dev-key-REPLACE_ME"]` with the newly generated key.
172-
- Set the database path to the systemd state directory: `SQLITE_FILE_NAME="/var/lib/pycolytics/databases/database.db"`
184+
```sh
185+
openssl rand -base64 24
186+
```
187+
188+
This will stop random people from logging events in your database, it will
189+
not stop a script kiddie who can decompile the key from your app, or pluck it
190+
from network traffic. I'd suggest creating a new one for every version of
191+
your application, and retiring old ones after a while.
192+
193+
- Set up the .env file:
194+
195+
- Replace `API_KEYS=["I-am-an-unsecure-dev-key-REPLACE_ME"]` with the
196+
newly generated key.
197+
- Set the database path to the systemd state directory:
198+
`SQLITE_FILE_NAME="/var/lib/pycolytics/databases/database.db"`
173199

174200
- Run read the new config:
175201

176-
```sudo systemctl daemon-reload```
202+
`sudo systemctl daemon-reload`
177203

178204
- Make the service start on boot:
179205

180-
```sudo systemctl enable pycolytics```
206+
`sudo systemctl enable pycolytics`
181207

182208
- Start the service:
183209

184-
```sudo systemctl start pycolytics```
210+
`sudo systemctl start pycolytics`
185211

186212
- Check for errors:
187213

188-
```sudo systemctl status pycolytics```
214+
`sudo systemctl status pycolytics`
189215

190216
- In case you need to fix configurations and restart the service use:
191217

192-
```sudo systemctl daemon-reload
193-
sudo systemctl restart pycolytics
194-
```
218+
```sudo systemctl daemon-reload
219+
sudo systemctl restart pycolytics
220+
```
195221

196-
Most online guides also recommend setting up fastapi behind an nginx reverse proxy, in case somebody tries to DDOS your server. I've never been successful enough for this to happen, so I'll leave it to you to figure out the details.
222+
Most online guides also recommend setting up fastapi behind an nginx reverse
223+
proxy, in case somebody tries to DDOS your server. I've never been successful
224+
enough for this to happen, so I'll leave it to you to figure out the details.
225+
226+
## How We Got Here
227+
228+
When I was looking at Attolytics, I was too lazy to set up a rust compile
229+
environment and install postgresql for something so simple, so I spent two days
230+
writing Pycolytics instead. To help you avoid my mistake, I made it so you can
231+
just clone it and move on with your life.
232+
233+
True to its name, Pycolytics is probably 10⁶ times slower than Attolytics, but
234+
who cares if it still serves my entire userbase from a raspberry-pi. It does
235+
asyncio and fancy multi-worker stuff to try and compensate.
236+
237+
Open an issue if you wish to contribute, or buy me a coffee if you find my work useful.
238+
239+
<a href='https://ko-fi.com/E1E712JJXK' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi3.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
197240

198241
## Planned Features
199-
- HTTPS communication for you security nerds out there
200242

243+
- HTTPS communication for you security nerds out there

app/main.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "v1.1.0"
1+
__version__ = "v1.1.1"
22
__api_version__ = "v1.0.0"
33

44
from contextlib import asynccontextmanager
@@ -27,6 +27,7 @@ async def lifespan(app: fastapi.FastAPI):
2727
await create_db_and_tables()
2828
yield
2929

30+
3031
description = f"""
3132
Running Pycolitics __{__version__}__
3233
@@ -53,10 +54,12 @@ async def log_event(
5354
session: AsyncSession = fastapi.Depends(get_session),
5455
event: EventCreate,
5556
request: fastapi.Request,
57+
response: fastapi.Response,
5658
):
5759
db_event = Event.model_validate(event)
5860
session.add(db_event)
5961
await session.commit()
62+
del response.headers["content-type"]
6063

6164

6265
@app.post("/v1.0/events", status_code=204, responses={401: {"model": HTTPError}})
@@ -66,8 +69,10 @@ async def log_events(
6669
session: AsyncSession = fastapi.Depends(get_session),
6770
events: list[EventCreate],
6871
request: fastapi.Request,
72+
response: fastapi.Response,
6973
):
7074
db_events = [Event.model_validate(event).model_dump() for event in events]
71-
# Pylance freaks out if I use exec here, says it can't take an Executable
72-
await session.execute(sqlmodel.insert(Event).values(db_events))
75+
# Pyright freaks out here, claims this can't take an Executable when it clearly can
76+
await session.exec(sqlmodel.insert(Event).values(db_events))
7377
await session.commit()
78+
del response.headers["content-type"]

0 commit comments

Comments
 (0)