Enhanced time synchronization for drf views.
added raise_exception boolean to simplify some other cases
5 files changed, +45 -19
+6-3README.md
| @@ -65,11 +65,12 @@ TimeCheck(request, ...) has the following arguments: | ||
| 65 | 65 | - `continue` allows the view to continue processing data when the client does not provide a timestamp |
| 66 | 66 | - **noupdate_code** `int` The response code for exceptions raised to indicate the view should stop processing |
| 67 | 67 | - **dt_fmt** `str` The datetime format used to normalize timestamps. Useful when the client and server use mismatched time depths (defaults to config) |
| 68 | +- **raise_exception** `bool` Raises and exception if the process should stop (defaults to config) | |
| 68 | 69 | |
| 69 | 70 | ### TimeCheck Methods |
| 70 | 71 | |
| 71 | -- `check_get` Raises a `NoUpdate` exception when the client timestamp is newer than or equal to the server timestamp | |
| 72 | -- `raise_update` Raises a `NoUpdate` exception when the client timestamp is older than or equal to the server timestamp | |
| 72 | +- `check_get` Raises a `NoUpdate` exception when the client timestamp is newer than or equal to the server timestamp. Returns a True if the client should receive data. | |
| 73 | +- `check_update` Raises a `NoUpdate` exception when the client timestamp is older than or equal to the server timestamp. Returns a True if the update should continue. | |
| 73 | 74 | |
| 74 | 75 | --- |
| 75 | 76 | |
| @@ -77,7 +78,7 @@ TimeCheck(request, ...) has the following arguments: | ||
| 77 | 78 | |
| 78 | 79 | TimeCheck is customizable through the django settings, env, and on a per usage basis. |
| 79 | 80 | |
| 80 | -### Django Settings Dictionairy Defaults | |
| 81 | +### Django Settings Dictionairy (Defaults) | |
| 81 | 82 | |
| 82 | 83 | ```python |
| 83 | 84 | TIMECHECK_CONF = { |
| @@ -87,6 +88,7 @@ TIMECHECK_CONF = { | ||
| 87 | 88 | "instance_field": "lastUpdated", |
| 88 | 89 | "missing_action": "noupdate", |
| 89 | 90 | "noupdate_code": 418, |
| 91 | + "raise_exception": True, | |
| 90 | 92 | } |
| 91 | 93 | ``` |
| 92 | 94 | |
| @@ -102,6 +104,7 @@ Set via environment variables or in `settings.TIMECHECK_CONF`: | ||
| 102 | 104 | | `TIMECHECK_NOUPDATE_CODE` | `418` | Error code raised when update is unnecessary (int) | |
| 103 | 105 | | `TIMECHECK_MISSING_ACTION` | `noupdate` | Action if timestamp is missing (`continue`, `noupdate`) | |
| 104 | 106 | | `TIMECHECK_DT_FMT` | `%Y-%m-%dT%H:%M:%S%z` | Timestamp parsing format (str) | |
| 107 | +| RAISE_EXCEPTION | True | Raises exceptions by default (bool) | | |
| 105 | 108 | |
| 106 | 109 | --- |
| 107 | 110 |
+1-0timecheck/settings.py
| @@ -24,5 +24,6 @@ conf = TimeCheckConf( | ||
| 24 | 24 | "instance_field": getval(str, "instance_field", "lastUpdated"), |
| 25 | 25 | "missing_action": getval(str, "missing_action", "noupdate"), |
| 26 | 26 | "noupdate_code": getval(int, "noupdate_code", 418), |
| 27 | + "raise_exception": getval(bool, "raise_exception", True), | |
| 27 | 28 | } |
| 28 | 29 | ) |
+4-0timecheck/tests.py
| @@ -19,8 +19,12 @@ class TimeCheckTests(TestCase): | ||
| 19 | 19 | |
| 20 | 20 | def test_get_client_is_newer(self): |
| 21 | 21 | client_time = fmt_dt(self.later) |
| 22 | + conf["raise_exception"] = True | |
| 22 | 23 | response = self.client.get("/", HTTP_LASTUPDATED=client_time) |
| 23 | 24 | self.assertEqual(response.status_code, conf["noupdate_code"]) |
| 25 | + conf["raise_exception"] = False | |
| 26 | + response = self.client.get("/", HTTP_LASTUPDATED=client_time) | |
| 27 | + self.assertEqual(response.status_code, 200) | |
| 24 | 28 | |
| 25 | 29 | def test_get_client_is_older(self): |
| 26 | 30 | client_time = fmt_dt(self.earlier) |
+32-16timecheck/timecheck.py
| @@ -30,20 +30,23 @@ class TimeCheckPrivate: | ||
| 30 | 30 | instance: models.Model | None = None, |
| 31 | 31 | server_timestamp: dt.datetime | None = None, |
| 32 | 32 | client_timestamp: dt.datetime | None = None, |
| 33 | - header_field=conf["header_field"], | |
| 34 | - body_field=conf["body_field"], | |
| 35 | - instance_field=conf["instance_field"], | |
| 36 | - missing_action: MissingAction = conf["missing_action"], | |
| 37 | - noupdate_code=conf["noupdate_code"], | |
| 38 | - dt_fmt=conf["dt_fmt"], | |
| 33 | + header_field: str | None = None, | |
| 34 | + body_field: str | None = None, | |
| 35 | + instance_field: str | None = None, | |
| 36 | + missing_action: MissingAction | None = None, | |
| 37 | + noupdate_code: int | None = None, | |
| 38 | + dt_fmt: str | None = None, | |
| 39 | + raise_exception: bool | None = None, | |
| 39 | 40 | ): |
| 40 | 41 | self.request = request |
| 41 | - self._header_field = header_field | |
| 42 | - self._body_field = body_field | |
| 43 | - self._instance_field = instance_field | |
| 44 | - self._noupdate_code = noupdate_code | |
| 45 | - self._missing_action = missing_action | |
| 46 | - self._dt_fmt = dt_fmt | |
| 42 | + self._header_field = header_field or conf["header_field"] | |
| 43 | + self._body_field = body_field or conf["body_field"] | |
| 44 | + self._instance_field = instance_field or conf["instance_field"] | |
| 45 | + self._noupdate_code = noupdate_code or conf["noupdate_code"] | |
| 46 | + self._missing_action = missing_action or conf["missing_action"] | |
| 47 | + self._dt_fmt = dt_fmt or conf["dt_fmt"] | |
| 48 | + self._raise_exception = raise_exception or conf["raise_exception"] | |
| 49 | + logger.debug(f"raise_exception={raise_exception}, {conf['raise_exception']}") | |
| 47 | 50 | self.client_timestamp = client_timestamp |
| 48 | 51 | |
| 49 | 52 | if server_timestamp: |
| @@ -79,9 +82,16 @@ class TimeCheckPrivate: | ||
| 79 | 82 | ) |
| 80 | 83 | if not self.client_timestamp: |
| 81 | 84 | if self._missing_action == "noupdate": |
| 82 | - raise NoUpdate(self.request.method, self._noupdate_code) | |
| 85 | + if self._raise_exception: | |
| 86 | + raise NoUpdate(self.request.method, self._noupdate_code) | |
| 87 | + else: | |
| 88 | + return False | |
| 83 | 89 | elif self.client_timestamp >= self.server_timestamp: |
| 84 | - raise NoUpdate(self.request.method, self._noupdate_code) | |
| 90 | + if self._raise_exception: | |
| 91 | + raise NoUpdate(self.request.method, self._noupdate_code) | |
| 92 | + else: | |
| 93 | + return False | |
| 94 | + return True | |
| 85 | 95 | |
| 86 | 96 | def check_update(self): |
| 87 | 97 | """Raises a drf exception `NoUpdate` which provides details that there is no need to update the server.""" |
| @@ -90,6 +100,12 @@ class TimeCheckPrivate: | ||
| 90 | 100 | ) |
| 91 | 101 | if not self.client_timestamp: |
| 92 | 102 | if self._missing_action == "noupdate": |
| 93 | - raise NoUpdate(self.request.method, self._noupdate_code) | |
| 103 | + if self._raise_exception: | |
| 104 | + raise NoUpdate(self.request.method, self._noupdate_code) | |
| 105 | + else: | |
| 106 | + return False | |
| 94 | 107 | elif self.client_timestamp <= self.server_timestamp: |
| 95 | - raise NoUpdate(self.request.method, self._noupdate_code) | |
| 108 | + if self._raise_exception: | |
| 109 | + raise NoUpdate(self.request.method, self._noupdate_code) | |
| 110 | + else: | |
| 111 | + return False |
+2-0timecheck/types.py
| @@ -18,3 +18,5 @@ class TimeCheckConf(TypedDict): | ||
| 18 | 18 | """What do do when the client does not provide a timestamp""" |
| 19 | 19 | dt_fmt: str |
| 20 | 20 | """Format used to normalize datetimes.""" |
| 21 | + raise_exception: bool | |
| 22 | + """Raise an exception or just store the should update value""" |