-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
eduid_user_cleaner skatteverket worker #314
Changes from 39 commits
53d6da9
1eddece
6fae0d5
4ab0f89
97cf1dc
fae6838
a9d0d23
38e02f4
6ab8095
12cff53
76cb98a
91f82cd
9dd0d10
ce5b3be
4392f41
a72be83
503bef0
486159e
e62b015
90b0ae7
949032c
ec76bd5
5cc7f84
297531a
393ed71
e878033
4638578
fac9091
dce92b8
bbc85a9
38ac3a8
e7a02ad
a1a790e
942f3a9
86aef2c
c7fa42f
ace25ce
d7d29b0
2df3f6b
f8ab129
6f18a76
3879339
d21f3b6
87fbf30
9bc8af6
6062809
3b0072a
1a75778
f270aaa
35897db
2bf2ec7
4ad0778
521432c
ac5a6cf
a94bb52
e8f0a07
4378c29
37b1a71
67397b9
44ffed6
05b4e09
b36dbeb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,34 @@ | ||
from typing import Optional | ||
from typing import Mapping, Optional | ||
|
||
import respx | ||
from httpx import Response, Request | ||
from eduid.common.models.amapi_user import UserUpdateNameRequest | ||
|
||
from eduid.common.clients.gnap_client.testing import MockedSyncAuthAPIMixin | ||
from eduid.userdb.userdb import AmDB | ||
|
||
|
||
class MockedAMAPIMixin(MockedSyncAuthAPIMixin): | ||
def start_mock_amapi(self, access_token_value: Optional[str] = None): | ||
def start_mock_amapi(self, central_user_db: Optional[AmDB] = None, access_token_value: Optional[str] = None): | ||
self.start_mock_auth_api(access_token_value=access_token_value) | ||
|
||
self.mocked_users = respx.mock(base_url="http://localhost", assert_all_called=False) | ||
self.central_user_db = central_user_db | ||
self.mocked_users = respx.mock(base_url="http://localhost/amapi", assert_all_called=False) | ||
put_users_name_route = self.mocked_users.put( | ||
url="/users/hubba-bubba/name", | ||
name="put_users_name_request", | ||
) | ||
put_users_name_route.pass_through() | ||
put_users_name_route.mock(side_effect=self._save) | ||
self.mocked_users.start() | ||
self.addCleanup(self.mocked_users.stop) # type: ignore | ||
|
||
def _save(self, request: Request) -> Response: | ||
if self.central_user_db is None: | ||
raise ValueError("save user side affect was called but self.amdb is None") | ||
mock_request = UserUpdateNameRequest.parse_raw(request.content) | ||
|
||
db_user = self.central_user_db.get_user_by_eppn("hubba-bubba") | ||
db_user.given_name = mock_request.given_name | ||
db_user.surname = mock_request.surname | ||
db_user.display_name = mock_request.display_name | ||
self.central_user_db.save(user=db_user) | ||
return Response(200, text='{"status": "true"}') |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from unittest import TestCase | ||
|
||
from eduid.userdb.testing import MongoTemporaryInstance | ||
from eduid.userdb.user_cleaner.cachedb import CacheDB | ||
from eduid.userdb.user_cleaner.cache import CacheUser | ||
|
||
|
||
class TestUserCleanerCache(TestCase): | ||
def setUp(self): | ||
self.tmp_db = MongoTemporaryInstance.get_instance() | ||
self.user_cleaner_meta_db = CacheDB(db_uri=self.tmp_db.uri, collection="skv_cache") | ||
|
||
def tearDown(self): | ||
self.user_cleaner_meta_db._drop_whole_collection() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
from typing import Any, Mapping, Optional | ||
from pydantic import BaseModel, Field | ||
from datetime import datetime | ||
from eduid.common.misc.timeutil import utc_now | ||
|
||
from eduid.userdb.db import TUserDbDocument | ||
from eduid.userdb.user import User | ||
|
||
|
||
class CacheUser(BaseModel): | ||
eppn: str | ||
created_ts: datetime = Field(default_factory=utc_now) | ||
next_run_ts: Optional[int] = None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Varför är inte There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Jo, för att de är enklare att räkna epoch när det ändå handlar om sekunder. |
||
|
||
def to_dict(self) -> TUserDbDocument: | ||
""" | ||
Convert Element to a dict in eduid format, that can be used to reconstruct the | ||
Element later. | ||
""" | ||
data = self.dict(exclude_none=True) | ||
|
||
return TUserDbDocument(data) | ||
|
||
@classmethod | ||
def from_dict(cls, data: Mapping[str, Any]) -> "CacheUser": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I stället för att göra |
||
"""Convert a dict to a Element object.""" | ||
return cls(**data) | ||
|
||
def next_run_ts_iso8601(self) -> str: | ||
""" | ||
Convert the next_run_ts to ISO8601 format. | ||
""" | ||
if self.next_run_ts is None: | ||
return "" | ||
return datetime.utcfromtimestamp(self.next_run_ts).strftime("%Y-%m-%d %H:%M:%S") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. strftime kan bytas ut mot There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixat! |
||
|
||
def from_user(self, data: User) -> "CacheUser": | ||
""" | ||
Convert a User object to a CacheUser object. | ||
""" | ||
queue_user = CacheUser( | ||
eppn=data.eppn, | ||
) | ||
return queue_user |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
from abc import ABC | ||
from time import time | ||
import logging | ||
from eduid.userdb.db import BaseDB, TUserDbDocument | ||
from eduid.userdb.user import User | ||
from eduid.userdb.user_cleaner.cache import CacheUser | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class CacheDB(BaseDB): | ||
"""Database class for the user cleaning database cache.""" | ||
|
||
def __init__(self, db_uri: str, collection: str, db_name: str = "eduid_user_cleaner"): | ||
super().__init__(db_uri, db_name, collection) | ||
indexes = {"unique-eppn": {"key": [("eppn", 1)], "unique": True}} | ||
|
||
self.setup_indexes(indexes) | ||
|
||
def save(self, queue_user: CacheUser) -> bool: | ||
"""Save a CacheUser object to the database.""" | ||
if self.exists(queue_user.eppn): | ||
logger.debug(f"User {queue_user.eppn} already exists in the queue") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "exists in the queue" -> "exists in the cache"? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixat, det fanns massa ställen där det stod Queue ist. för cache. |
||
return False | ||
self._coll.insert_one(queue_user.to_dict()) | ||
return True | ||
|
||
def exists(self, eppn: str) -> bool: | ||
"""Check if a user exists in the cache.""" | ||
return self._coll.count_documents({"eppn": eppn}) > 0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. du kan använda db_count på BaseDB med limit=1 för att slippa räkna alla dokument. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixat |
||
|
||
def get_all(self) -> list[CacheUser]: | ||
"""Get all users from the cache.""" | ||
res = self._coll.find({}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. här kan kan du använda BaseDBs _get_all_docs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixat |
||
return [CacheUser.from_dict(data=doc) for doc in res] | ||
|
||
def count(self) -> int: | ||
"""Count the number of users in the queue.""" | ||
return self._coll.count_documents({}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. db_count finns redan på BaseDB. |
||
|
||
def delete(self, eppn: str) -> None: | ||
"""delete one user from the cache.""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove_document finns redan på BaseDB |
||
self._coll.delete_one({"eppn": eppn}) | ||
|
||
def delete_all(self) -> None: | ||
"""Delete all users from the cache.""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. _drop_whole_collection finns redan på BaseDB. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. _drop_whole_collection stoppade jag i delete_all() så använder appen bara databas methoder från de egna paketet. De kändes bra iaf. |
||
self._coll.delete_many({}) | ||
|
||
def populate( | ||
self, | ||
am_users: list[User], | ||
periodicity: int, | ||
) -> None: | ||
"""Populate cache database with the user from AMDB.""" | ||
|
||
cache_size = len(am_users) | ||
|
||
periodicity_in_seconds = 22 * 60 * 60 * periodicity # strip 2 hours of each day in order to give us some slack | ||
time_constant = int(periodicity_in_seconds / cache_size) | ||
|
||
next_run_ts = int(time()) + (60 * 60) # first process window starts in 1 hour, then the population is done | ||
for am_user in am_users: | ||
next_run_ts += time_constant | ||
queue_user = CacheUser( | ||
eppn=am_user.eppn, | ||
next_run_ts=next_run_ts, | ||
) | ||
|
||
self.save(queue_user) | ||
|
||
def is_empty(self) -> bool: | ||
"""Check if the cache is empty.""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. se kommentar till exists There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixat |
||
return self.count() == 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Vet inte om mypy eller våra IDEer kommer att vara så imponerade av att vi sätter något på self här.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Vscode brydde sig inte. Men tagit bort dom eftersom dom inte används utanför metoden.