Skip to content

Commit 5e8e418

Browse files
committed
backend: Use planet.osm.org/users_deleted/users_deleted.txt to avoid hammering API
Signed-off-by: Taylor Smock <[email protected]>
1 parent f46366b commit 5e8e418

File tree

4 files changed

+73
-13
lines changed

4 files changed

+73
-13
lines changed

backend/api/users/resources.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from distutils.util import strtobool
22
from typing import Optional
3+
from collections.abc import Generator
34

45
from flask import stream_with_context, Response
56
from flask_restful import Resource, current_app, request
@@ -146,22 +147,36 @@ def get(self):
146147
@token_auth.login_required
147148
def delete(self):
148149
if UserService.is_user_an_admin(token_auth.current_user()):
149-
150-
def delete_users():
151-
for user in User.get_all_users_not_paginated():
152-
# We specifically want to remove users that have deleted their OSM accounts.
153-
if OSMService.is_osm_user_gone(user.id):
154-
data = UserService.delete_user_by_id(
155-
user.id, token_auth.current_user()
156-
).to_primitive()
157-
yield f"\u001e{data}\n"
158-
159150
return Response(
160-
stream_with_context(delete_users()),
151+
stream_with_context(UsersAllAPI._delete_users()),
161152
headers={"Content-Type": "application/json-seq"},
162153
)
163154
raise Unauthorized()
164155

156+
@staticmethod
157+
def _delete_users() -> Generator[str, None, None]:
158+
# Updated daily
159+
deleted_users = OSMService.get_deleted_users()
160+
if deleted_users:
161+
last_deleted_user = 0
162+
for user in User.get_all_users_not_paginated(User.id):
163+
while last_deleted_user < user.id:
164+
last_deleted_user = next(deleted_users)
165+
if last_deleted_user == user.id:
166+
data = UserService.delete_user_by_id(
167+
user.id, token_auth.current_user()
168+
).to_primitive()
169+
yield f"\u001e{data}\n"
170+
return
171+
# Fall back to hitting the API (if the OSM API is not the default)
172+
for user in User.get_all_users_not_paginated():
173+
# We specifically want to remove users that have deleted their OSM accounts.
174+
if OSMService.is_osm_user_gone(user.id):
175+
data = UserService.delete_user_by_id(
176+
user.id, token_auth.current_user()
177+
).to_primitive()
178+
yield f"\u001e{data}\n"
179+
165180

166181
class UsersQueriesUsernameAPI(Resource):
167182
@token_auth.login_required

backend/models/postgis/user.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,12 @@ def get_all_users(query: UserSearchQuery) -> UserSearchDTO:
182182
return dto
183183

184184
@staticmethod
185-
def get_all_users_not_paginated():
185+
def get_all_users_not_paginated(*order):
186186
"""Get all users in DB"""
187-
return db.session.query(User.id).all()
187+
query = db.session.query(User.id)
188+
if not order:
189+
return query.all()
190+
return query.order_by(*order).all()
188191

189192
@staticmethod
190193
def filter_users(user_filter: str, project_id: int, page: int) -> UserFilterDTO:

backend/services/users/osm_service.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import re
2+
from collections.abc import Generator
3+
from typing import Optional
4+
15
import requests
26
from flask import current_app
37

@@ -32,6 +36,32 @@ def is_osm_user_gone(user_id: int) -> bool:
3236

3337
return False
3438

39+
@staticmethod
40+
def get_deleted_users() -> Optional[Generator[int, None, None]]:
41+
"""
42+
Get the list of deleted users from OpenStreetMap.
43+
This only returns users from the https://www.openstreetmap.org instance.
44+
:return: The deleted users
45+
"""
46+
if current_app.config["OSM_SERVER_URL"] == "https://www.openstreetmap.org":
47+
48+
def get_planet_osm_deleted_users() -> Generator[int, None, None]:
49+
response = requests.get(
50+
"https://planet.openstreetmap.org/users_deleted/users_deleted.txt",
51+
stream=True,
52+
)
53+
username = re.compile(r"^\s*(\d+)\s*$")
54+
try:
55+
for line in response.iter_lines(decode_unicode=True):
56+
match = username.fullmatch(line)
57+
if match:
58+
yield int(match.group(1))
59+
finally:
60+
response.close()
61+
62+
return get_planet_osm_deleted_users()
63+
return None
64+
3565
@staticmethod
3666
def get_osm_details_for_user(user_id: int) -> UserOSMDTO:
3767
"""

tests/backend/integration/services/users/test_osm_service.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,15 @@ def test_get_osm_details_for_user_returns_user_details_if_valid_user_id(self):
1717
def test_is_user_deleted(self):
1818
self.assertTrue(OSMService.is_osm_user_gone(535043))
1919
self.assertFalse(OSMService.is_osm_user_gone(2078753))
20+
21+
def test_get_deleted_users(self):
22+
# These are the first 10 deleted users on 2024-04-16. This should ensure that the test finishes quickly.
23+
# Otherwise, it can take 6s+ (dependent upon network speed)
24+
deleted_users = [4, 142, 593, 601, 1769, 2161, 2238, 2782, 2868]
25+
generator = OSMService.get_deleted_users()
26+
for deleted_user in generator:
27+
if deleted_user in deleted_users:
28+
deleted_users.remove(deleted_user)
29+
if len(deleted_users) == 0:
30+
break
31+
self.assertEquals(0, len(deleted_users))

0 commit comments

Comments
 (0)