diff --git a/api/api_specification.md b/api/api_specification.md index b26b976cd71054746922392a2aaadfd1a1f3a220..3be120b485e5913db752aaae3b1a32a7255b435c 100644 --- a/api/api_specification.md +++ b/api/api_specification.md @@ -1,4 +1,4 @@ -# Specification of the Web API for the Video-AG Website (v0.80). +# Specification of the Web API for the Video-AG Website (v0.81). ## Introduction @@ -1725,13 +1725,13 @@ Note that views only appear some time after they viewer has watched the video (u ### Types -| Type | Notes | -|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| int | | -| float | | -| boolean | | -| string | | -| datetime | A string which must be in format `YYYY-MM-ddTHH:mm:ss` (Year, Month, day, Hour, minute, second) (Note that this is a local date time according to [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)) | +| Type | Notes | +|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| int | | +| float | | +| boolean | | +| string | | +| datetime | A string which must be in format `YYYY-MM-ddTHH:mm:ss.SSSZ` (Year, Month, day, Hour, minute, second, millisecond, 'T' and 'Z' are literals) (Note that this is a *UTC* date time according to [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)) | Additionally, the following objects may appear as the type of some field: @@ -3188,6 +3188,10 @@ Possible `error_code`: ## Changelog +### v0.81 + +* Added milliseconds to `datetime` type and clarified that it is in UTC. + ### v0.80 * Updated `field_description` diff --git a/api/api_specification_template.md b/api/api_specification_template.md index 2098757462a0a1311a039a4ac16a51e9938a292b..5b45245f9d0126639743c5c80df0e53f10eb37fa 100644 --- a/api/api_specification_template.md +++ b/api/api_specification_template.md @@ -1,4 +1,4 @@ -# Specification of the Web API for the Video-AG Website (v0.80). +# Specification of the Web API for the Video-AG Website (v0.81). ## Introduction @@ -87,13 +87,13 @@ object. The object type is specified in the field description and the configurat ### Types -| Type | Notes | -|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| int | | -| float | | -| boolean | | -| string | | -| datetime | A string which must be in format `YYYY-MM-ddTHH:mm:ss` (Year, Month, day, Hour, minute, second) (Note that this is a local date time according to [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)) | +| Type | Notes | +|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| int | | +| float | | +| boolean | | +| string | | +| datetime | A string which must be in format `YYYY-MM-ddTHH:mm:ss.SSSZ` (Year, Month, day, Hour, minute, second, millisecond, 'T' and 'Z' are literals) (Note that this is a *UTC* date time according to [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)) | Additionally, the following objects may appear as the type of some field: @@ -139,6 +139,10 @@ Possible `error_code`: ## Changelog +### v0.81 + +* Added milliseconds to `datetime` type and clarified that it is in UTC. + ### v0.80 * Updated `field_description` diff --git a/api/src/api/authentication.py b/api/src/api/authentication.py index d4fdb12a28f0edab3f3357328eb3b3393d305613..01eef6d23da579b306c749c74c6aea5790f76072 100644 --- a/api/src/api/authentication.py +++ b/api/src/api/authentication.py @@ -235,7 +235,7 @@ def authenticate_fsmpi(username: str, password: str) -> {}: session.add(user_db) session.flush() # Get autoincrement id - user_db.last_login = datetime.now() + user_db.last_login = get_standard_datetime_now() # Update data (unlikely, but might have changed. Also fills 'missing' data after the migration) user_db.full_name = full_name user_db.display_name = display_name diff --git a/api/src/api/routes/job.py b/api/src/api/routes/job.py index a8060bfd942db0a663a8cb0efaa93387f544af96..12886e55da3e00402e8449f1b67e5fcb14e9d6c3 100644 --- a/api/src/api/routes/job.py +++ b/api/src/api/routes/job.py @@ -142,8 +142,8 @@ def api_route_get_job_status(): job.id: { # TODO This is not really using the serialization API "state": job.state.value, - "run_start_time": None if job.run_start_time is None else job.run_start_time.strftime(DEFAULT_DATETIME_FORMAT), - "run_end_time": None if job.run_end_time is None else job.run_end_time.strftime(DEFAULT_DATETIME_FORMAT), + "run_start_time": None if job.run_start_time is None else format_standard_datetime(job.run_start_time), + "run_end_time": None if job.run_end_time is None else format_standard_datetime(job.run_end_time), } for job in limited_jobs } diff --git a/api/src/api/routes/stats.py b/api/src/api/routes/stats.py index b529e69c4ee9e92cb2588ef3957b3c539ffe6879..96063d819c43e9aad9f37a86a5bc9efaa19ee67a 100755 --- a/api/src/api/routes/stats.py +++ b/api/src/api/routes/stats.py @@ -52,13 +52,13 @@ def api_route_watch_log_publish_medium(course_handle: str, publish_medium_id: in raise ApiClientException(ERROR_BAD_REQUEST("User Agent too long")) try: - timestamp = datetime.strptime(timestamp_str, DEFAULT_DATETIME_FORMAT) + timestamp = parse_standard_datetime(timestamp_str) except ValueError: req_json.get("timestamp").raise_error("Invalid timestamp format") return # Allow some variance, but not too much - if abs((timestamp - datetime.now()).total_seconds()) > 30: + if abs((timestamp - get_standard_datetime_now()).total_seconds()) > 30: raise ApiClientException(ERROR_BAD_REQUEST("Invalid timestamp")) def _trans(session: SessionDb): diff --git a/api/tests/routes/feedback.py b/api/tests/routes/feedback.py index 75068c0720dc56decf1d719961178d5c06137e38..a01460c0c13e949e7372d8654fe279a219032f63 100644 --- a/api/tests/routes/feedback.py +++ b/api/tests/routes/feedback.py @@ -1,5 +1,3 @@ -import unittest - from videoag_common.miscellaneous.constants import * from api_test import ApiTest @@ -7,12 +5,12 @@ from api_test import ApiTest # The python json parser does not unescape the escaped strings, so we need to put the extra \ here. _TEST_DATA_FEEDBACK_1 = { "id": 1, - "time_created": "2024-03-18T18:53:02", + "time_created": "2024-03-18T18:53:02.000Z", "text": "Hier FuNkToNiErT NICHTS!" } _TEST_DATA_FEEDBACK_2 = { "id": 2, - "time_created": "2024-03-10T20:11:08", + "time_created": "2024-03-10T20:11:08.000Z", "email": "no-one@example.com", "text": "Ein bisschen Feedback" } diff --git a/api/tests/routes/object_modifications.py b/api/tests/routes/object_modifications.py index c40a3250ceb077f6e1a9a51524250cd575ad528a..c0da04fc4cf23871faeb2d97517713131fa89e7a 100644 --- a/api/tests/routes/object_modifications.py +++ b/api/tests/routes/object_modifications.py @@ -718,7 +718,7 @@ class ObjectModificationsTest(ApiTest): "/object_management/lecture/29/configuration", { "updates": { - "time": "2000-01-01T01:01:01" + "time": "2000-01-01T01:01:01.000Z" }, "expected_current_values": {} }, @@ -728,7 +728,7 @@ class ObjectModificationsTest(ApiTest): "GET", "/object_management/lecture/29/configuration", use_moderator_login=True - )[1], "time", "2000-01-01T01:01:01") + )[1], "time", "2000-01-01T01:01:01.000Z") self.do_json_request( "PATCH", "/object_management/announcement/3/configuration", @@ -1363,7 +1363,7 @@ class ObjectModificationsTest(ApiTest): "title": "Hallo", "speaker": "Someone", "location": "Somewhere", - "time": "2000-01-01T00:00:00", + "time": "2000-01-01T00:00:00.000Z", "duration": 90, "description": "Something", "livestream_planned": True, @@ -1384,7 +1384,7 @@ class ObjectModificationsTest(ApiTest): "title": "Hallo", "speaker": "Someone", "location": "Somewhere", - "time": "2000-01-01T00:00:00", + "time": "2000-01-01T00:00:00.000Z", "duration": 90, "description": "Something", "livestream_planned": True, @@ -1405,7 +1405,7 @@ class ObjectModificationsTest(ApiTest): "title": "Hallo", "speaker": "Someone", "location": "Somewhere", - "time": "2000-01-01T00:00:00", + "time": "2000-01-01T00:00:00.000Z", "duration": 90, "description": "Something", "livestream_planned": True, @@ -1427,7 +1427,7 @@ class ObjectModificationsTest(ApiTest): "title": "Hallo", "speaker": "Someone", "location": "Somewhere", - "time": "2000-01-01T00:00:00", + "time": "2000-01-01T00:00:00.000Z", "duration": 90, "description": "Something", "livestream_planned": True, @@ -1449,7 +1449,7 @@ class ObjectModificationsTest(ApiTest): "title": "Hallo", "speaker": "Someone", "location": "Somewhere", - "time": "2000-01-01T00:00:00", + "time": "2000-01-01T00:00:00.000Z", "duration": 90, "description": "Something", "livestream_planned": True, @@ -2021,7 +2021,7 @@ class ObjectModificationsTest(ApiTest): "parent_id": 3, "values": { "title": "New Year", - "time": "2024-01-01T00:00:00", + "time": "2024-01-01T00:00:00.000Z", "duration": 42 } }, diff --git a/api/tests/routes/stats.py b/api/tests/routes/stats.py index 459ff972fe990ceb007e64f88885195343742489..60df40e6483103d29a30818c37f6fc1cfa7b9a43 100644 --- a/api/tests/routes/stats.py +++ b/api/tests/routes/stats.py @@ -1,8 +1,8 @@ -from datetime import datetime, timedelta +from datetime import timedelta from api_test import ApiTest -from videoag_common.miscellaneous import HTTP_200_OK, DEFAULT_DATETIME_FORMAT, HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND, \ - HTTP_401_UNAUTHORIZED +from videoag_common.miscellaneous import HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND, \ + HTTP_401_UNAUTHORIZED, format_standard_datetime, get_standard_datetime_now class StatsTest(ApiTest): @@ -13,7 +13,7 @@ class StatsTest(ApiTest): "/course/07ws-diskrete/watched/publish_medium/1495", { "watch_id": "1VdWFVE1eCDKy7ClpjRDXqtgSwJkzjHRTK3NbBg7hyLCZXGrLed2A9LmSF74vq0f", - "timestamp": datetime.now().strftime(DEFAULT_DATETIME_FORMAT), + "timestamp": format_standard_datetime(get_standard_datetime_now()), "watch_start_time_sec": -1, # invalid "watch_duration_sec": 30, "watch_speed_percent": 150, @@ -25,7 +25,7 @@ class StatsTest(ApiTest): "/course/07ws-diskrete/watched/publish_medium/1495", { "watch_id": "1VdWFVE1eCDKy7ClpjRDXqtgSwJkzjHRTK3NbBg7hyLCZXGrLed2A9LmSF74vq0f", - "timestamp": datetime.now().strftime(DEFAULT_DATETIME_FORMAT), + "timestamp": format_standard_datetime(get_standard_datetime_now()), "watch_start_time_sec": 50, "watch_duration_sec": -1, # invalid "watch_speed_percent": 150, @@ -37,7 +37,7 @@ class StatsTest(ApiTest): "/course/07ws-diskrete/watched/publish_medium/1495", { "watch_id": "1VdWFVE1eCDKy7ClpjRDXqtgSwJkzjHRTK3NbBg7hyLCZXGrLed2A9LmSF74vq0f", - "timestamp": datetime.now().strftime(DEFAULT_DATETIME_FORMAT), + "timestamp": format_standard_datetime(get_standard_datetime_now()), "watch_start_time_sec": 50, "watch_duration_sec": 30, "watch_speed_percent": 0, # invalid @@ -49,7 +49,7 @@ class StatsTest(ApiTest): "/course/wrong-handle/watched/publish_medium/1495", # wrong handle { "watch_id": "1VdWFVE1eCDKy7ClpjRDXqtgSwJkzjHRTK3NbBg7hyLCZXGrLed2A9LmSF74vq0f", - "timestamp": datetime.now().strftime(DEFAULT_DATETIME_FORMAT), + "timestamp": format_standard_datetime(get_standard_datetime_now()), "watch_start_time_sec": 50, "watch_duration_sec": 30, "watch_speed_percent": 150, @@ -61,7 +61,7 @@ class StatsTest(ApiTest): "/course/07ws-diskrete/watched/publish_medium/1495", { "watch_id": "1VdWFVE1eCDKy7ClpjRDXqtgSwJkzjHRTK3NbBg7hyLCZXGrLed2A9LmSF74vq0f", - "timestamp": (datetime.now() + timedelta(minutes=1)).strftime(DEFAULT_DATETIME_FORMAT), + "timestamp": format_standard_datetime(get_standard_datetime_now() + timedelta(minutes=1)), "watch_start_time_sec": 50, "watch_duration_sec": 30, "watch_speed_percent": 150, @@ -73,7 +73,7 @@ class StatsTest(ApiTest): "/course/07ws-diskrete/watched/publish_medium/1495", { "watch_id": "1VdWFVE1eCDKy7ClpjRDXqtgSwJkzjHRTK3NbBg7hyLCZXGrLed2A9LmSF74vq0f", - "timestamp": (datetime.now() - timedelta(minutes=1)).strftime(DEFAULT_DATETIME_FORMAT), + "timestamp": format_standard_datetime(get_standard_datetime_now() - timedelta(minutes=1)), "watch_start_time_sec": 50, "watch_duration_sec": 30, "watch_speed_percent": 150, @@ -97,7 +97,7 @@ class StatsTest(ApiTest): "/course/07ws-diskrete/watched/publish_medium/1495", { "watch_id": "1VdWFVE1eCDKy7ClpjRDXqtgSwJkzjHRTK3NbBg7hyLCZXGrLed2A9LmSF74vq0f", - "timestamp": datetime.now().strftime(DEFAULT_DATETIME_FORMAT), + "timestamp": format_standard_datetime(get_standard_datetime_now()), "watch_start_time_sec": 50, "watch_duration_sec": 30, "watch_speed_percent": 150, @@ -113,7 +113,7 @@ class StatsTest(ApiTest): "/course/07ws-diskrete/watched/publish_medium/1495", { "watch_id": "1VdWFVE1eCDKy7ClpjRDXqtgSwJkzjHRTK3NbBg7hyLCZXGrLed2A9LmSF74vq0f", - "timestamp": datetime.now().strftime(DEFAULT_DATETIME_FORMAT), + "timestamp": format_standard_datetime(get_standard_datetime_now()), "watch_start_time_sec": 50, "watch_duration_sec": 30, "watch_speed_percent": 150, @@ -126,7 +126,7 @@ class StatsTest(ApiTest): "/course/07ws-buk/watched/publish_medium/1368", # publish medium not visible { "watch_id": "4nl8S29xePTNocTkhVgR9rMcHKQxsFxnvzU2hwNqinmXDjtolA9C7sDHsXWTmIAU", - "timestamp": datetime.now().strftime(DEFAULT_DATETIME_FORMAT), + "timestamp": format_standard_datetime(get_standard_datetime_now()), "watch_start_time_sec": 50, "watch_duration_sec": 30, "watch_speed_percent": 150, @@ -138,7 +138,7 @@ class StatsTest(ApiTest): "/course/07ws-buk/watched/publish_medium/1368", # publish medium not visible { "watch_id": "4nl8S29xePTNocTkhVgR9rMcHKQxsFxnvzU2hwNqinmXDjtolA9C7sDHsXWTmIAU", - "timestamp": datetime.now().strftime(DEFAULT_DATETIME_FORMAT), + "timestamp": format_standard_datetime(get_standard_datetime_now()), "watch_start_time_sec": 50, "watch_duration_sec": 30, "watch_speed_percent": 150, @@ -152,7 +152,7 @@ class StatsTest(ApiTest): "/course/11ws-infin/watched/publish_medium/1497", # has authentication view perms { "watch_id": "upsTW8OkUfFGBesyzC6JJn1j8lVUDwbZPL7R9NiWmqDyBZMzW70up13Bnm0DDzrJ", - "timestamp": datetime.now().strftime(DEFAULT_DATETIME_FORMAT), + "timestamp": format_standard_datetime(get_standard_datetime_now()), "watch_start_time_sec": 50, "watch_duration_sec": 30, "watch_speed_percent": 150, @@ -164,7 +164,7 @@ class StatsTest(ApiTest): "/course/11ws-infin/watched/publish_medium/1497", # has authentication view perms { "watch_id": "upsTW8OkUfFGBesyzC6JJn1j8lVUDwbZPL7R9NiWmqDyBZMzW70up13Bnm0DDzrJ", - "timestamp": datetime.now().strftime(DEFAULT_DATETIME_FORMAT), + "timestamp": format_standard_datetime(get_standard_datetime_now()), "watch_start_time_sec": 50, "watch_duration_sec": 30, "watch_speed_percent": 150, diff --git a/common_py/src/videoag_common/api_object/fields/basic_fields.py b/common_py/src/videoag_common/api_object/fields/basic_fields.py index aac8cf316694deacdac01bfcba84c5234951dd68..675d33d00f752fbbfe34878b1213ad4d2a277013 100644 --- a/common_py/src/videoag_common/api_object/fields/basic_fields.py +++ b/common_py/src/videoag_common/api_object/fields/basic_fields.py @@ -1,6 +1,5 @@ import enum import re -from datetime import datetime from typing import Any, TYPE_CHECKING from videoag_common.miscellaneous import * @@ -183,12 +182,12 @@ class ApiDatetimeField[_O: "ApiObject"](ApiSimpleColumnField[_O]): } def _db_value_to_json(self, db_value) -> JsonTypes: - return db_value.strftime(DEFAULT_DATETIME_FORMAT) + return format_standard_datetime(db_value) def _json_value_to_db(self, session: SessionDb, object_db: _O, json_value: CJsonValue): time_string = json_value.as_string(100) try: - return datetime.strptime(time_string, DEFAULT_DATETIME_FORMAT) + return parse_standard_datetime(time_string) except ValueError: json_value.raise_error("Invalid datetime format") diff --git a/common_py/src/videoag_common/miscellaneous/__init__.py b/common_py/src/videoag_common/miscellaneous/__init__.py index 298e13768d4cfa7b9c0c0a9214c2bf0d7af27957..15b7f81fc15d9e201d722f3109d5322e1b38431b 100644 --- a/common_py/src/videoag_common/miscellaneous/__init__.py +++ b/common_py/src/videoag_common/miscellaneous/__init__.py @@ -3,7 +3,9 @@ from .util import ( check_json_type, ID_STRING_REGEX_NO_LENGTH, ID_STRING_PATTERN_NO_LENGTH, - DEFAULT_DATETIME_FORMAT, + format_standard_datetime, + parse_standard_datetime, + get_standard_datetime_now, ArgAttributeObject, _raise, truncate_string, diff --git a/common_py/src/videoag_common/miscellaneous/mail.py b/common_py/src/videoag_common/miscellaneous/mail.py index 0f704202f9b2f97cf291415f8ffaa94098dae02c..753e6cb5463ccb4742765f49f8776500dcf29ccc 100644 --- a/common_py/src/videoag_common/miscellaneous/mail.py +++ b/common_py/src/videoag_common/miscellaneous/mail.py @@ -8,6 +8,7 @@ from datetime import datetime from email.mime.text import MIMEText from .scheduler import SchedulerCoordinator +from .util import get_standard_datetime_now class MailSender: @@ -126,7 +127,7 @@ class ErrorMailNotifier: self._send_mail(bulk_msg) def _append_to_bulk_queue(self, msg: str): - self._bulk_queue.append((datetime.now(), msg)) + self._bulk_queue.append((get_standard_datetime_now(), msg)) def notify(self, message: str, exception: Exception or None = None): if exception is not None: diff --git a/common_py/src/videoag_common/miscellaneous/util.py b/common_py/src/videoag_common/miscellaneous/util.py index f82244ea4f4d4254b765138e0e8a08247c566240..4ab8d61e667a21e7c663f5f40a6ba3e48fb128a2 100644 --- a/common_py/src/videoag_common/miscellaneous/util.py +++ b/common_py/src/videoag_common/miscellaneous/util.py @@ -1,4 +1,7 @@ import json +import math +import datetime +from datetime import datetime as Datetime from pathlib import Path from types import ModuleType from typing import Callable, TypeVar, Iterable @@ -28,7 +31,26 @@ ID_STRING_REGEX_NO_LENGTH = "(?=.*[a-z_-])[a-z0-9_-]*" ID_STRING_PATTERN_NO_LENGTH = re.compile(ID_STRING_REGEX_NO_LENGTH) -DEFAULT_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S" +def pad_string(val: str, padding: str, length: int): + return (padding * math.ceil( (length - len(val)) / len(padding) )) + val + + +# Python doesn't have a formatter for milliseconds +# Don't just remove last three numbers with microseconds. See here: https://stackoverflow.com/a/35643540 +def format_standard_datetime(dt: Datetime): + return dt.astimezone(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%S.") + pad_string(str(dt.microsecond // 1000), "0", 3) + "Z" + + +def parse_standard_datetime(val: str): + if not val.endswith("Z"): + raise ValueError("datetime must have Z (UTC) designator") + val = val[:-1] + return Datetime.strptime(val + "000", "%Y-%m-%dT%H:%M:%S.%f").replace(tzinfo=datetime.UTC) + + +# ALWAYS use this method. Everything needs to be in UTC +def get_standard_datetime_now() -> Datetime: + return Datetime.now(datetime.UTC) class ArgAttributeObject: diff --git a/common_py/src/videoag_common/objects/site.py b/common_py/src/videoag_common/objects/site.py index 75e580f2e7a35154c07b4c2fc6d919f2ee0cef61..0d0e420036ee57a4c39fcc34de533a642000b85e 100644 --- a/common_py/src/videoag_common/objects/site.py +++ b/common_py/src/videoag_common/objects/site.py @@ -2,6 +2,7 @@ from datetime import datetime from enum import Enum from typing import Any +from videoag_common.miscellaneous import * from videoag_common.database import * from videoag_common.api_object import * from .course import Course, Lecture @@ -66,7 +67,7 @@ class Announcement(DeletableApiObject, VisibilityApiObject, Base): def has_access(self, context: dict[AccessContextKey, Any]): cond = super().has_access(context) if not AC_IS_MOD.get(context): - current_time = datetime.now() + current_time = get_standard_datetime_now() cond &= self.hybrid_is_none(self.publish_time) | (self.publish_time <= current_time) cond &= self.hybrid_is_none(self.expiration_time) | (self.expiration_time > current_time) return cond diff --git a/common_py/src/videoag_common/test/object_data.py b/common_py/src/videoag_common/test/object_data.py index 9dd232e9875a40bb95c3ede1c6526ca79a120ce1..d3d6bb6996d9bcb6a8db4841d5d838a90587dd6e 100644 --- a/common_py/src/videoag_common/test/object_data.py +++ b/common_py/src/videoag_common/test/object_data.py @@ -447,7 +447,7 @@ TEST_DATA_LECTURE_1_NO_CHAP_MEDIA = \ "title": "Einführung zur Berechenbarkeit", "speaker": "", "location": "", - "time": "2007-10-19T12:00:00", + "time": "2007-10-19T12:00:00.000Z", "duration": 0, "description": "", "no_recording": False, @@ -480,7 +480,7 @@ TEST_DATA_LECTURE_2_NO_CHAP_MEDIA = \ "title": "Einführung zur Berechenbarkeit", "speaker": "", "location": "", - "time": "2007-10-23T08:30:00", + "time": "2007-10-23T08:30:00.000Z", "duration": 0, "description": "", "no_recording": False, @@ -513,7 +513,7 @@ TEST_DATA_LECTURE_3_NO_CHAP_MEDIA = \ "title": "Einführung zur Berechenbarkeit", "speaker": "", "location": "", - "time": "2007-10-26T12:00:00", + "time": "2007-10-26T12:00:00.000Z", "duration": 0, "description": "", "no_recording": False, @@ -543,7 +543,7 @@ TEST_DATA_LECTURE_25_NO_CHAP_MEDIA = \ "title": "Graphentheorie: Grundbegriffe, Datenstrukturen, Algorithmus für Breitensuche", "speaker": "", "location": "", - "time": "2007-12-11T13:30:00", + "time": "2007-12-11T13:30:00.000Z", "duration": 0, "description": "", "no_recording": False, @@ -555,7 +555,7 @@ TEST_DATA_LECTURE_25_NO_CHAP_MEDIA_MOD = TEST_DATA_LECTURE_25_NO_CHAP_MEDIA | \ { "visible": True, "internal_comment": "", - "publish_time": "2007-12-12T19:12:04", + "publish_time": "2007-12-12T19:12:04.000Z", } TEST_DATA_LECTURE_25 = TEST_DATA_LECTURE_25_NO_CHAP_MEDIA | { "chapters": [TEST_DATA_CHAPTER_1, TEST_DATA_CHAPTER_2], @@ -573,7 +573,7 @@ _TEST_DATA_LECTURE_26_NO_CHAP_MEDIA = \ "title": "Hamiltonkreis, Eulertour, Eulerweg", "speaker": "", "location": "", - "time": "2007-12-18T13:30:00", + "time": "2007-12-18T13:30:00.000Z", "duration": 0, "description": "", "no_recording": False, @@ -599,7 +599,7 @@ TEST_DATA_LECTURE_29_NO_CHAP_MEDIA = \ "title": "Modulare Arithmetik: Gruppe, Ring, Körper, abelsche Gruppe, Untergruppe, Einheitengruppe. Restklassenringe, Primzahl.", "speaker": "", "location": "", - "time": "2008-01-17T08:15:00", + "time": "2008-01-17T08:15:00.000Z", "duration": 0, "description": "", "no_recording": False, @@ -629,7 +629,7 @@ TEST_DATA_LECTURE_185_NO_CHAP_MEDIA = \ "title": "Organisatorisches, Motivation, Künstliche Pflanzen, Alphabete, Wörter, Sprachen", "speaker": "", "location": "", - "time": "2009-04-16T10:00:00", + "time": "2009-04-16T10:00:00.000Z", "duration": 90, "description": "Sorry für den schlechten Ton", "no_recording": False, @@ -661,7 +661,7 @@ TEST_DATA_LECTURE_186_NO_CHAP_MEDIA = \ "title": "Alphabete, Wörter, Sprachen, Reguläre Ausdrücke", "speaker": "", "location": "", - "time": "2009-04-21T08:15:00", + "time": "2009-04-21T08:15:00.000Z", "duration": 45, "description": "", "no_recording": False, @@ -675,7 +675,7 @@ TEST_DATA_LECTURE_186_NO_CHAP_MEDIA_MOD = TEST_DATA_LECTURE_186_NO_CHAP_MEDIA | { "visible": True, "internal_comment": "", - "publish_time": "2009-05-18T03:13:20", + "publish_time": "2009-05-18T03:13:20.000Z", } TEST_DATA_LECTURE_186 = TEST_DATA_LECTURE_186_NO_CHAP_MEDIA | { "chapters": [], @@ -703,7 +703,7 @@ TEST_DATA_LECTURE_1186_NO_CHAP_MEDIA = \ "title": "Einführung, I. Grundlagen", "speaker": "", "location": "Aula", - "time": "2011-10-10T18:30:00", + "time": "2011-10-10T18:30:00.000Z", "duration": 90, "description": "", "no_recording": False, @@ -715,7 +715,7 @@ TEST_DATA_LECTURE_1186_NO_CHAP_MEDIA_MOD = TEST_DATA_LECTURE_1186_NO_CHAP_MEDIA { "visible": True, "internal_comment": "", - "publish_time": "2011-10-17T14:33:45", + "publish_time": "2011-10-17T14:33:45.000Z", } TEST_DATA_LECTURE_1186 = TEST_DATA_LECTURE_1186_NO_CHAP_MEDIA | { "chapters": [], @@ -733,7 +733,7 @@ TEST_DATA_LECTURE_1187_NO_CHAP_MEDIA = \ "title": "", "speaker": "", "location": "Aula", - "time": "2011-10-17T18:30:00", + "time": "2011-10-17T18:30:00.000Z", "duration": 90, "description": "noch kein Titel", "no_recording": False, @@ -763,7 +763,7 @@ TEST_DATA_LECTURE_1188_NO_CHAP_MEDIA = \ "title": "", "speaker": "", "location": "Aula", - "time": "2050-10-24T18:30:00", + "time": "2050-10-24T18:30:00.000Z", "duration": 90, "description": "noch kein Titel", "no_recording": False, @@ -915,8 +915,8 @@ TEST_DATA_ANNOUNCEMENT_1 = \ } TEST_DATA_ANNOUNCEMENT_1_MOD = TEST_DATA_ANNOUNCEMENT_1 | \ { - "publish_time": "2024-01-26T00:00:00", - "expiration_time": "2050-03-25T00:00:00", + "publish_time": "2024-01-26T00:00:00.000Z", + "expiration_time": "2050-03-25T00:00:00.000Z", "visible": True, } @@ -929,7 +929,7 @@ TEST_DATA_ANNOUNCEMENT_2 = \ } TEST_DATA_ANNOUNCEMENT_2_MOD = TEST_DATA_ANNOUNCEMENT_2 | \ { - "publish_time": "2024-03-01T00:00:00", + "publish_time": "2024-03-01T00:00:00.000Z", "expiration_time": None, "visible": True, } @@ -957,7 +957,7 @@ _TEST_DATA_ANNOUNCEMENT_4 = \ } TEST_DATA_ANNOUNCEMENT_4_MOD = _TEST_DATA_ANNOUNCEMENT_4 | \ { - "publish_time": "2050-03-22T00:00:00", + "publish_time": "2050-03-22T00:00:00.000Z", "expiration_time": None, "visible": True, } @@ -971,8 +971,8 @@ _TEST_DATA_ANNOUNCEMENT_5 = \ } TEST_DATA_ANNOUNCEMENT_5_MOD = _TEST_DATA_ANNOUNCEMENT_5 | \ { - "publish_time": "2024-01-26T00:00:00", - "expiration_time": "2024-02-01T00:00:00", + "publish_time": "2024-01-26T00:00:00.000Z", + "expiration_time": "2024-02-01T00:00:00.000Z", "visible": True, } diff --git a/job_controller/jobs/media_process_scheduler/job.py b/job_controller/jobs/media_process_scheduler/job.py index e7952bca675cdf345cdf8141c2109da7458704d5..2ba51840cedae2fb47d8cb66caba2c7aa13aee40 100644 --- a/job_controller/jobs/media_process_scheduler/job.py +++ b/job_controller/jobs/media_process_scheduler/job.py @@ -1,7 +1,6 @@ import logging import random import string -from datetime import datetime from pathlib import Path from videoag_common.database import * @@ -189,7 +188,7 @@ class ProcessScheduler: self._lecture.media_duration_sec = media_duration_sec if self._lecture.publish_time is None and len(self._lecture.publish_media) > 0: - self._lecture.publish_time = datetime.now() + self._lecture.publish_time = get_standard_datetime_now() def _try_create_metadata_for_file(self, file: MediumFile) -> bool: logger.info(f"Probing medium file {file.file_path} ({file.process_target_id}, {file.id}) for metadata") diff --git a/job_controller/jobs/source_file_sorter/job.py b/job_controller/jobs/source_file_sorter/job.py index bb6ac1a4ad3aa5a5049b4dcbc2fcc22132fe0f5e..0cdc7e047c903f94c562c48130ebaa66f34f9fe8 100644 --- a/job_controller/jobs/source_file_sorter/job.py +++ b/job_controller/jobs/source_file_sorter/job.py @@ -7,7 +7,7 @@ from pathlib import Path import videoag_common from videoag_common.database import * -from videoag_common.miscellaneous import CJsonObject +from videoag_common.miscellaneous import * from videoag_common.objects import * from videoag_common.media_process import * from videoag_common.objects.medium import SorterFileStatus @@ -66,7 +66,7 @@ def _check_file( file_path=db_path, file_modification_time=file_mod_time, status=status, - update_time=datetime.now() + update_time=get_standard_datetime_now() ) session.add(file) logger.info(f"Adding new file {db_path} to database") @@ -88,7 +88,7 @@ def _check_file( file.status = status file.force_immediate_sort = False file.sorter_error_message = error_message - file.update_time = datetime.now() + file.update_time = get_standard_datetime_now() if sort_now: logger.info(f"Queuing {db_path} to be sorted") @@ -150,7 +150,7 @@ def _sort_file(session: SessionDb, own_job_id: int, db_path: str): try: logger.info(f"Sorting file {sorter_file}") - sorter_file.update_time = datetime.now() + sorter_file.update_time = get_standard_datetime_now() assert sorter_file.file_path.startswith(_SORTER_DIR_NAME) file = _DATA_DIR.joinpath(sorter_file.file_path) diff --git a/job_controller/jobs/view_stats_aggregator/job.py b/job_controller/jobs/view_stats_aggregator/job.py index 1a4af09581724bc06a2f1e62760783625c92e048..fa1293629e55fb6b79d1fb1b57bc6e292ef13416 100644 --- a/job_controller/jobs/view_stats_aggregator/job.py +++ b/job_controller/jobs/view_stats_aggregator/job.py @@ -379,7 +379,7 @@ def execute(database, own_job_id, input_data: CJsonObject): .group_by(PublishMediumWatchLogEntry.watch_id) .having( sql.func.max(PublishMediumWatchLogEntry.timestamp) - < (Datetime.now() - timedelta(minutes=_MINUTES_UNTIL_PROCESSING)) + < (get_standard_datetime_now() - timedelta(minutes=_MINUTES_UNTIL_PROCESSING)) ) .limit(_BATCH_SIZE_LIMIT) ) diff --git a/job_controller/src/job_controller/executor_api/local_docker_executor.py b/job_controller/src/job_controller/executor_api/local_docker_executor.py index 396b585e053c42ac9e102cd56337ab44a1a98f4d..024f02f822185b6cc159fcbcfc062e4110392bec 100644 --- a/job_controller/src/job_controller/executor_api/local_docker_executor.py +++ b/job_controller/src/job_controller/executor_api/local_docker_executor.py @@ -10,7 +10,7 @@ from pathlib import Path from time import sleep import job_controller -from videoag_common.miscellaneous import JsonTypes +from videoag_common.miscellaneous import * from .job_executor_api import JobExecutorApi, JobExecutionInfo from ..job.config import JobMetaData @@ -40,7 +40,7 @@ class DockerJob(JobExecutionInfo): if self._failed: return self._failed = True - self._finish_time = datetime.now() + self._finish_time = get_standard_datetime_now() def spawn(self): if self._build_process is not None: @@ -112,7 +112,7 @@ class DockerJob(JobExecutionInfo): # Might have missing quoting, etc. but can be useful for debugging. print("UNSAFE: " + ' '.join(str(a) for a in self._run_args)) self._run_process = subprocess.Popen(self._run_args) - self._start_time = datetime.now() + self._start_time = get_standard_datetime_now() def _check_status_from_scheduler(self): self._check_and_update_status() @@ -160,7 +160,7 @@ class DockerJob(JobExecutionInfo): # Set last for consistent variables in other threads if self._finish_time is None: - self._finish_time = datetime.now() + self._finish_time = get_standard_datetime_now() def is_success(self) -> bool or None: if self._finish_time is None: diff --git a/job_controller/src/job_controller/job_controller.py b/job_controller/src/job_controller/job_controller.py index d5f8f7ae5faad27a65e218c4063b4c814e834b00..0d79daecdcc803c1da1045656d4a5f4f765e24bc 100644 --- a/job_controller/src/job_controller/job_controller.py +++ b/job_controller/src/job_controller/job_controller.py @@ -7,6 +7,7 @@ from datetime import datetime # noinspection PyPep8Naming from sched import scheduler as Scheduler +from videoag_common.miscellaneous import * from videoag_common.database import * from videoag_common.objects import * @@ -204,11 +205,11 @@ class JobController: print(f"Handling {'failed' if failed else 'finished'} job {job.id}") if job_info is None: - job.run_end_time = datetime.now() + job.run_end_time = get_standard_datetime_now() else: job.run_end_time = job_info.get_finish_time() if job.run_end_time is None: # In case of failure the API may return None - job.run_end_time = datetime.now() + job.run_end_time = get_standard_datetime_now() if failed: job.state = JobState.FAILED