whatdidyoudo 0.1.0__py3-none-any.whl
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.
Potentially problematic release.
This version of whatdidyoudo might be problematic. Click here for more details.
- whatdidyoudo/__init__.py +0 -0
- whatdidyoudo/app.py +88 -0
- whatdidyoudo/static/style.css +103 -0
- whatdidyoudo/templates/form.html +49 -0
- whatdidyoudo-0.1.0.dist-info/METADATA +83 -0
- whatdidyoudo-0.1.0.dist-info/RECORD +7 -0
- whatdidyoudo-0.1.0.dist-info/WHEEL +4 -0
whatdidyoudo/__init__.py
ADDED
|
File without changes
|
whatdidyoudo/app.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""A Flask app that shows OSM tasks done by a user on a specific day."""
|
|
2
|
+
import datetime
|
|
3
|
+
import xml.etree.ElementTree as ET
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from flask import Flask, render_template
|
|
8
|
+
from flask_caching import Cache
|
|
9
|
+
from flask_limiter import Limiter
|
|
10
|
+
from flask_limiter.util import get_remote_address
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
app = Flask(__name__)
|
|
14
|
+
cache = Cache(app, config={"CACHE_TYPE": "SimpleCache",
|
|
15
|
+
"CACHE_DEFAULT_TIMEOUT": 60 * 60 * 24 * 7})
|
|
16
|
+
limiter = Limiter(app=app, key_func=get_remote_address)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_etree_from_url(url: str) -> ET.Element:
|
|
20
|
+
"""Fetches XML content from a URL and returns the root Element."""
|
|
21
|
+
response = requests.get(url, timeout=120)
|
|
22
|
+
response.raise_for_status() # Raise an error for bad responses
|
|
23
|
+
return ET.fromstring(response.content)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_changes(user: str, date: str):
|
|
27
|
+
"""Return a {app: num of changes} dictionary and the changesets amount."""
|
|
28
|
+
changes: defaultdict[str, int] = defaultdict(int)
|
|
29
|
+
changesets = 0
|
|
30
|
+
datetime_date = datetime.date.fromisoformat(date)
|
|
31
|
+
start_time = f"{datetime_date}T00:00:00Z"
|
|
32
|
+
end_time = f"{datetime_date + datetime.timedelta(days=1)}T00:00:00Z"
|
|
33
|
+
|
|
34
|
+
changeset_url = ("https://api.openstreetmap.org/api/0.6/changesets?"
|
|
35
|
+
f"display_name={user}&time={start_time},{end_time}")
|
|
36
|
+
root = get_etree_from_url(url=changeset_url)
|
|
37
|
+
|
|
38
|
+
for cs in root.findall("changeset"):
|
|
39
|
+
cs_id = cs.attrib["id"]
|
|
40
|
+
|
|
41
|
+
tags = {tag.attrib["k"]: tag.attrib["v"] for tag in cs.findall("tag")}
|
|
42
|
+
editor = tags.get("created_by", "")
|
|
43
|
+
changesets += 1
|
|
44
|
+
|
|
45
|
+
diff_url = ("https://api.openstreetmap.org/api/0.6/changeset/"
|
|
46
|
+
f"{cs_id}/download")
|
|
47
|
+
try:
|
|
48
|
+
root = get_etree_from_url(url=diff_url)
|
|
49
|
+
except requests.HTTPError:
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
for action in root:
|
|
53
|
+
changes[editor] += len(action)
|
|
54
|
+
return changes, changesets
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.route('/')
|
|
58
|
+
@app.route('/<user>')
|
|
59
|
+
@app.route('/<user>/<date>')
|
|
60
|
+
def whatdidyoudo(user: str | None = None, date: str | None = None) -> str:
|
|
61
|
+
"""shows OSM tasks done by a user on a specific day."""
|
|
62
|
+
changes: defaultdict[str, int] = defaultdict(int)
|
|
63
|
+
changesets = 0
|
|
64
|
+
error = ""
|
|
65
|
+
if user and date:
|
|
66
|
+
try:
|
|
67
|
+
today = datetime.date.today().isoformat()
|
|
68
|
+
if date != today:
|
|
69
|
+
cache_key = f"changes_{user}_{date}"
|
|
70
|
+
cached = cache.get(cache_key) # type: ignore
|
|
71
|
+
if cached:
|
|
72
|
+
changes, changesets = cached
|
|
73
|
+
else:
|
|
74
|
+
with limiter.limit("10 per minute"):
|
|
75
|
+
changes, changesets = get_changes(user, date)
|
|
76
|
+
cache.set(cache_key, (changes, changesets)) # type: ignore
|
|
77
|
+
else:
|
|
78
|
+
with limiter.limit("10 per minute"):
|
|
79
|
+
changes, changesets = get_changes(user, date)
|
|
80
|
+
except requests.HTTPError:
|
|
81
|
+
error = f"Can't determine changes for user {user} on {date}."
|
|
82
|
+
|
|
83
|
+
return render_template('form.html', user=user, date=date, changes=changes,
|
|
84
|
+
changesets=changesets, error=error)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
app.run(debug=True)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
body {
|
|
2
|
+
font-family: 'Segoe UI', Arial, sans-serif;
|
|
3
|
+
background: linear-gradient(120deg, #232526 0%, #414345 100%);
|
|
4
|
+
color: #f3f3f3;
|
|
5
|
+
min-height: 100vh;
|
|
6
|
+
margin: 0;
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.container {
|
|
14
|
+
background: rgba(30, 30, 30, 0.92);
|
|
15
|
+
border-radius: 16px;
|
|
16
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
|
|
17
|
+
padding: 2em 2.5em 1.5em 2.5em;
|
|
18
|
+
margin-top: 2em;
|
|
19
|
+
max-width: 600px;
|
|
20
|
+
width: 100%;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.error {
|
|
24
|
+
background: #ff4d4f;
|
|
25
|
+
color: #fff;
|
|
26
|
+
padding: 0.7em 1em;
|
|
27
|
+
border-radius: 8px;
|
|
28
|
+
margin-bottom: 1em;
|
|
29
|
+
font-weight: 600;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
form {
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: row;
|
|
35
|
+
align-items: flex-end;
|
|
36
|
+
gap: 1em;
|
|
37
|
+
width: 100%;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
label {
|
|
41
|
+
display: none;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
input[type="text"] {
|
|
45
|
+
flex: 2 1 200px;
|
|
46
|
+
padding: 0.7em 1em;
|
|
47
|
+
border-radius: 8px;
|
|
48
|
+
border: none;
|
|
49
|
+
font-size: 1.1em;
|
|
50
|
+
background: #2c2f34;
|
|
51
|
+
color: #f3f3f3;
|
|
52
|
+
outline: none;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
input[type="date"] {
|
|
56
|
+
flex: 1 1 120px;
|
|
57
|
+
padding: 0.7em 0.5em;
|
|
58
|
+
border-radius: 8px;
|
|
59
|
+
border: none;
|
|
60
|
+
font-size: 1.1em;
|
|
61
|
+
background: #2c2f34;
|
|
62
|
+
color: #f3f3f3;
|
|
63
|
+
outline: none;
|
|
64
|
+
min-width: 120px;
|
|
65
|
+
max-width: 160px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
button {
|
|
69
|
+
padding: 0.7em 1.5em;
|
|
70
|
+
border-radius: 8px;
|
|
71
|
+
border: none;
|
|
72
|
+
background: #4f8cff;
|
|
73
|
+
color: #fff;
|
|
74
|
+
font-size: 1.1em;
|
|
75
|
+
font-weight: 600;
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
transition: background 0.2s;
|
|
78
|
+
}
|
|
79
|
+
button:hover {
|
|
80
|
+
background: #2563eb;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
h2 a {
|
|
84
|
+
color: #f3f3f3;
|
|
85
|
+
text-decoration: none;
|
|
86
|
+
font-weight: 700;
|
|
87
|
+
letter-spacing: 1px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@media (max-width: 600px) {
|
|
91
|
+
.container {
|
|
92
|
+
padding: 1em 0.5em;
|
|
93
|
+
}
|
|
94
|
+
form {
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
gap: 0.7em;
|
|
97
|
+
}
|
|
98
|
+
input[type="date"] {
|
|
99
|
+
width: 100%;
|
|
100
|
+
min-width: unset;
|
|
101
|
+
max-width: unset;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>What Did You Do?</title>
|
|
6
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
|
7
|
+
<script>
|
|
8
|
+
window.onload = function() {
|
|
9
|
+
var dateInput = document.getElementById('date');
|
|
10
|
+
if (!dateInput.value) {
|
|
11
|
+
var today = new Date();
|
|
12
|
+
var yyyy = today.getFullYear();
|
|
13
|
+
var mm = String(today.getMonth() + 1).padStart(2, '0');
|
|
14
|
+
var dd = String(today.getDate()).padStart(2, '0');
|
|
15
|
+
dateInput.value = yyyy + '-' + mm + '-' + dd;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
</script>
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<div class="container">
|
|
22
|
+
<h2><a href="/">What Did You Do?</a></h2>
|
|
23
|
+
<form method="get" action="" onsubmit="event.preventDefault(); window.location.href='/' + encodeURIComponent(document.getElementById('user').value) + '/' + encodeURIComponent(document.getElementById('date').value);">
|
|
24
|
+
<label for="user">User:</label>
|
|
25
|
+
<input type="text" id="user" name="user" value="{{ user or '' }}" placeholder="insert OSM username" required>
|
|
26
|
+
|
|
27
|
+
<label for="date">Date:</label>
|
|
28
|
+
<input type="date" id="date" name="date" value="{{ date or '' }}" required>
|
|
29
|
+
|
|
30
|
+
<button type="submit">Show</button>
|
|
31
|
+
</form>
|
|
32
|
+
</div>
|
|
33
|
+
{% if user and date %}
|
|
34
|
+
{% if error %}
|
|
35
|
+
<p class=error>{{ error }}</p>
|
|
36
|
+
{% elif changes %}
|
|
37
|
+
<p>{{ user }} did some changes on {{ date }}!</p>
|
|
38
|
+
{% for app, count in changes.items() %}
|
|
39
|
+
<ul>
|
|
40
|
+
<li>{{ count }} changes in {{ app }}</li>
|
|
41
|
+
</ul>
|
|
42
|
+
{% endfor %}
|
|
43
|
+
That's a total of {{ changesets }} changesets.
|
|
44
|
+
{% else %}
|
|
45
|
+
<p>{{ user }} did not do anything on {{ date }}.</p>
|
|
46
|
+
{% endif %}
|
|
47
|
+
{% endif %}
|
|
48
|
+
</body>
|
|
49
|
+
</html>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: whatdidyoudo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A minimal Flask app that shows the amount of OpenStreetMap changes made by a user on a day.
|
|
5
|
+
Author-email: Ulf Rompe <whatdidyoudo.rompe.org@rompe.org>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: flask-caching==2.3.1
|
|
9
|
+
Requires-Dist: flask>=3.0.3
|
|
10
|
+
Requires-Dist: requests>=2.32.4
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: hatchling>=1.27.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: ruff>=0.13.2; extra == 'dev'
|
|
14
|
+
Requires-Dist: twine>=6.2.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: types-requests>=2.32.4; extra == 'dev'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# whatdidyoudo
|
|
19
|
+
|
|
20
|
+
A minimal Flask app that shows the amount of OpenStreetMap changes made by a user on a day.
|
|
21
|
+
|
|
22
|
+
## Background
|
|
23
|
+
|
|
24
|
+
I often ask myself after contributing many changes to OpenStreetMap, either by walking around
|
|
25
|
+
while extensively using StreetComplete, MapComplete or Vespucci, or by doing some tasks in iD or
|
|
26
|
+
jOSM: **How many changes did I contribute to the map today?**
|
|
27
|
+
|
|
28
|
+
I'm not the only one. I heard questions like this quite a few times:
|
|
29
|
+
**Where can I see how much I did on yesterday's mapwalk?**
|
|
30
|
+
|
|
31
|
+
Because I think that simple questions deserve simple answers, I made this tool to give exactly
|
|
32
|
+
this information and nothing else.
|
|
33
|
+
|
|
34
|
+
You don't need to self-host it, it is available for anyone at
|
|
35
|
+
[whatdidyoudo.rompe.org](https://whatdidyoudo.rompe.org).
|
|
36
|
+
|
|
37
|
+
## Setup
|
|
38
|
+
|
|
39
|
+
Fun fact: of course you don't really need *uv* for this. I'm just using this project to
|
|
40
|
+
get used to it as I think it has a lot of potential.
|
|
41
|
+
|
|
42
|
+
### Install [uv](https://github.com/astral-sh/uv) if needed
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
pip install uv
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Install dependencies using *uv*
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
uv pip install -r pyproject.toml
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
If you want to develop:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
uv pip install -r pyproject.toml --extra dev
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Run tests
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
pytest
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Run the app in test mode
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
python whatdidyoudo/app.py
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Visit [http://127.0.0.1:5000/](http://127.0.0.1:5000/) in your browser to see "hello world".
|
|
73
|
+
|
|
74
|
+
### Build a package and upload it to Pypi
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
uvx hatchling build
|
|
78
|
+
uvx twine upload dist/*
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
This project is licensed under the MIT License. See the `pyproject.toml` for details.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
whatdidyoudo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
whatdidyoudo/app.py,sha256=u6pvalY0SYe5LBlXA0U81akN09MgmK5gPi_HacuWNy4,3302
|
|
3
|
+
whatdidyoudo/static/style.css,sha256=AtT4pHBBrDSMxzForapiP-F-l648lQSISxgg6n93-98,1958
|
|
4
|
+
whatdidyoudo/templates/form.html,sha256=VLWdk1jdrpvKCtekSF088UXk9vXWLwJ4w0pkscpyy30,1904
|
|
5
|
+
whatdidyoudo-0.1.0.dist-info/METADATA,sha256=5PMd0RYYUOB7i_TldzKHPSIjLlIeEDJZbsQqIQPT3Ew,2152
|
|
6
|
+
whatdidyoudo-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
whatdidyoudo-0.1.0.dist-info/RECORD,,
|