diff options
Diffstat (limited to 'jimbrella')
-rw-r--r-- | jimbrella/config.py | 1 | ||||
-rw-r--r-- | jimbrella/exceptions.py | 8 | ||||
-rw-r--r-- | jimbrella/lockfile.py | 33 | ||||
-rw-r--r-- | jimbrella/umbrellas.py | 41 | ||||
-rw-r--r-- | jimbrella/utils.py | 11 |
5 files changed, 65 insertions, 29 deletions
diff --git a/jimbrella/config.py b/jimbrella/config.py index e0963f5..81e5d09 100644 --- a/jimbrella/config.py +++ b/jimbrella/config.py @@ -23,7 +23,6 @@ JFORM_GIVEBACK_URL = config["jform"]["giveback_url"] JFORM_BOOKMARK_DIR = JIMBRELLA_DIR / Path(config["jform"]["bookmark_dir"]) DATABASE_PATH = JIMBRELLA_DIR / Path(config["db"]["db_path"]) -USERS_PATH = JIMBRELLA_DIR / Path(config["db"]["users_path"]) DUE_HOURS = config["rules"]["due_hours"] diff --git a/jimbrella/exceptions.py b/jimbrella/exceptions.py index 498c6c4..8e7734b 100644 --- a/jimbrella/exceptions.py +++ b/jimbrella/exceptions.py @@ -2,11 +2,11 @@ class UmbrellaNotFoundError(Exception): - """For when an umbrella with required serial is not found in database.""" + """For when an umbrella with required id is not found in database.""" - def __init__(self, serial): - self.serial = serial - self.message = f"No umbrella {serial} found." + def __init__(self, umbid): + self.id = umbid + self.message = f"Umbrella {id} not found." super().__init__(self.message) diff --git a/jimbrella/lockfile.py b/jimbrella/lockfile.py new file mode 100644 index 0000000..987f0e3 --- /dev/null +++ b/jimbrella/lockfile.py @@ -0,0 +1,33 @@ +import os + + +class Lockfile: + """Prevent unwanted concurrency for file I/O. + + For a file named "<file>", create or remove a lockfile named "<file>.lock". + + When a process is modifying the file, call `lock(). When the modification + is done, call `unlock()`. + """ + + def __init__(self, filepath): + self.filepath = str(filepath) + self.lockpath = self.filepath + ".lock" + + def lock(self): + """Continue attempting to lock until locked.""" + locked = False + while not locked: + try: + f = open(self.lockpath, "x") + f.close() + locked = True + except FileExistsError: + continue + + def unlock(self): + """Remove lockfile.""" + try: + os.remove(self.lockpath) + except FileNotFoundError: + return diff --git a/jimbrella/umbrellas.py b/jimbrella/umbrellas.py index e59bb54..6da7184 100644 --- a/jimbrella/umbrellas.py +++ b/jimbrella/umbrellas.py @@ -26,7 +26,7 @@ class Umbrellas: - tenant_id | string. student or faculty ID. - tenant_phone | string. phone number via which to contact tenant when the lease is due. - tenant_email | string. for future compatibility. always None for the time being. - - lent_at | an ISO 8601 date string "YYYY-MM-DDThh:mm:ss+08:00" if status is + - lent_at | an ISO 8601 date string "YYYY-MM-DDThh:mm:ss.mmm+08:00" if status is | "lent" or "overdue. is None otherwise. Schema: @@ -43,13 +43,13 @@ class Umbrellas: self.path = path def read(self) -> list: - db = sqlite3.connect(path) + db = sqlite3.connect(self.path) db.row_factory = sqlite.Row umbrellas = db.execute("SELECT * FROM Umbrellas").fetchall() db.close() return umbrellas - def update(self, umb) -> None: + def update(self, umb) -> dict: """Update Umbrella table with new data given in `umb`. Not all fields in an umbrella dict need to be present in `umb`. Only `id` is required. @@ -61,6 +61,9 @@ class Umbrellas: If `status` is not "lent" or "overdue", `tenant_*` and `lent_at` are automatically erased. `lent_at` may be either an ISO 8601 string or a datetime.datetime object. Must be UTC+8. + + Returns a dict of <field>: (<prior_value>, <updated_value>) for each updated field unless + its erasure can be inferred. For AdminLog. """ # `id` must be specified. try: @@ -68,7 +71,7 @@ class Umbrellas: except (KeyError, ValueError): raise UmbrellaValueError("id") - db = sqlite3.connect(path) + db = sqlite3.connect(self.path) db.row_factory = sqlite.Row # check if umbrella #<id> exists in database @@ -77,12 +80,18 @@ class Umbrellas: if umb_in_db is None: raise UmbrellaNotFoundError(umbid) + diff = {} + status = umb_in_db["status"] - if "status" in umb and umb["status"] in STATUSES: - status = umb["status"] - db.execute("UPDATE Umbrellas SET status = ? WHERE id = ?", status, umbid) - else: - raise UmbrellaValueError("status") + if "status" in umb: + if umb["status"] in STATUSES: + status = umb["status"] + if umb_in_db["status"] != umb["status"]: + diff["status"] = (umb_in_db["status"], umb["status"]) + + db.execute("UPDATE Umbrellas SET status = ? WHERE id = ?", status, umbid) + else: + raise UmbrellaValueError("status") if status in ("lent", "overdue"): for key in ( @@ -92,10 +101,13 @@ class Umbrellas: "tenant_email", ): if col in umb: + if umb_in_db[col] != umb[col]: + diff[col] = (umb_in_db[col], umb[col]) + db.execute( "UPDATE Umbrellas SET ? = ? WHERE id = ?", col, - umb[col] or None, + umb[col], umbid, ) @@ -114,6 +126,8 @@ class Umbrellas: # timezone must be +08:00 raise UmbrellaValueError("lent_at") + diff["lent_at"] = (umb_in_db["lent_at"], lent_at.isoformat(timespec="milliseconds")) + db.execute( "UPDATE Umbrellas SET lent_at = ? WHERE id = ?", lent_at.isoformat(timespec="milliseconds"), @@ -133,12 +147,13 @@ class Umbrellas: # now that new data are validated, commit the SQL transaction db.commit() db.close() + return diff def take_away( self, umbid, date, tenant_name, tenant_id, tenant_phone="", tenant_email="" ) -> None: """When a user has borrowed an umbrella.""" - db = sqlite3.connect(path) + db = sqlite3.connect(self.path) db.row_factory = sqlite3.Row umb = db.execute("SELECT * FROM Umbrellas WHERE id = ?", umbid) db.close() @@ -166,7 +181,7 @@ class Umbrellas: `tenant_name` and `tenant_id` are used to verify if the umbrella is returned by the same person who borrowed it. """ - db = sqlite3.connect(path) + db = sqlite3.connect(self.path) db.row_factory = sqlite3.Row umb = db.execute("SELECT * FROM Umbrellas WHERE id = ?", umbid) db.close() @@ -187,7 +202,7 @@ class Umbrellas: def mark_overdue(self, umbid) -> None: """When an umbrella is overdue, change its status to "overdue".""" - db = sqlite3.connect(path) + db = sqlite3.connect(self.path) db.row_factory = sqlite3.Row umb = db.execute("SELECT * FROM Umbrellas WHERE id = ?", umbid) diff --git a/jimbrella/utils.py b/jimbrella/utils.py index 31fafbc..fd2c86d 100644 --- a/jimbrella/utils.py +++ b/jimbrella/utils.py @@ -19,14 +19,3 @@ def human_timedelta(delta: timedelta) -> str: hours = delta.seconds // 3600 minutes = (delta.seconds - (hours * 3600)) // 60 return days + f"{hours:0>2}:{minutes:0>2}" # zero-pad to two digits - - -def group_by_key(data: list, key: str) -> dict: - """Groups a list of dicts by the value of their `key` into a dict of lists of dicts.""" - keys = set([item[key] for item in data]) - # initiate a dict with `keys` as keys and [] as values - groups = dict.fromkeys(keys, []) - for k in keys: - groups[k] = [item for item in data if item[key] == k] - - return groups |