cardinal_pythonlib.django.fields.isodatetimetz


Original code copyright (C) 2009-2022 Rudolf Cardinal (rudolf@pobox.com).

This file is part of cardinal_pythonlib.

Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.


Django field class implementing storage of a date/time, with its timezone, in a text field (in ISO-8601 format).

class cardinal_pythonlib.django.fields.isodatetimetz.IsoDateTimeExact(lhs, rhs)[source]

Lookup for a ‘=’ comparison where the LHS is a column containing an ISO-8601 field (and the RHS will be comparable to a DATETIME).

class cardinal_pythonlib.django.fields.isodatetimetz.IsoDateTimeGreaterThan(lhs, rhs)[source]

Lookup for a ‘>’ comparison where the LHS is a column containing an ISO-8601 field (and the RHS will be comparable to a DATETIME).

class cardinal_pythonlib.django.fields.isodatetimetz.IsoDateTimeGreaterThanEqual(lhs, rhs)[source]

Lookup for a ‘>=’ comparison where the LHS is a column containing an ISO-8601 field (and the RHS will be comparable to a DATETIME).

class cardinal_pythonlib.django.fields.isodatetimetz.IsoDateTimeLessThan(lhs, rhs)[source]

Lookup for a ‘<’ comparison where the LHS is a column containing an ISO-8601 field (and the RHS will be comparable to a DATETIME).

class cardinal_pythonlib.django.fields.isodatetimetz.IsoDateTimeLessThanEqual(lhs, rhs)[source]

Lookup for a ‘<=’ comparison where the LHS is a column containing an ISO-8601 field (and the RHS will be comparable to a DATETIME).

class cardinal_pythonlib.django.fields.isodatetimetz.IsoDateTimeTzField(*args, **kwargs)[source]

Django field for date/time stored in ISO format. Microsecond resolution; timezone-aware. Example sent TO the database:

2015-11-11T22:21:37.000000+05:00

YYYY-MM-DD HH:MM:SS uuuuuu TZ:TZ
1234567890123456789012345678901234567890 ... so 32 chars

Will also accept FROM the database any ISO-8601 format accepted by dateutil.parser.parse()

  • Python side: datetime.datetime

  • The database-specific bits are tricky.

    • In SQLite, the DATETIME() and STRFTIME() functions can be used to convert ISO strings to UTC, so that’s a good basis for comparison. However, the actual comparisons, as always in SQLite, are string-based.

    • DATETIME(...) ends up like this:

      2015-11-11 22:21:37
      

      STRFTIME('%Y-%m-%d %H:%M:%f', ...) looks like this, allowing millisecond precision:

      2015-11-11 22:21:37.000
      2015-11-11 22:21:37.123
      

      The Django automatic RHS converter for datetime values looks like this:

      2015-11-11 22:21:37
      2015-11-11 22:21:37.123456
      

      https://stackoverflow.com/questions/14368290/django-datetimefield-and-timezone-now

      … so a direct comparison with DATETIME works only if the microsecond component is zero (or you coerce it to zero via get_db_prep_value), and a direct comparison to the STRFTIME expression fails.

      Coercing the fractional seconds to zero via get_db_prep_value() would make sub-second comparisons meaningless in the database. So we should avoid that, and operate as close to the database resolution as possible.

      That means that we have to alter the RHS in the same way we altered the LHS, and that means we can’t use a transform… … oh, hang on, I’m just underestimating get_db_prep_value(); we .can coerce to exactly the thing yielded by SQLite’s STRFTIME, by .converting to a string!

      Next step: the data conversion specified for the RHS of a transform is governed by its output_field. So when we’re coercing to a date, we specify a DateField here, and all is happy. But for a datetime, we should NOT specify DateTimeField, because that will bypass the equality lookup from our custom field, and also bypass the RHS conversion (via get_db_prep_value() etc.).

      Specifically, for a DateTimeField, conversion to a database value is done by

      django/db/models/fields/__init__.py : DateTimeField.get_prep_value
      django/db/backends/{backend}/operations.py
          : DatabaseOperations.value_to_db_datetime
          - for sqlite3: calls str(value) [via six.texttype]
            and for a datetime, that gives
              2015-11-17 00:00:00  # if microsecond == 0
              2015-11-17 00:00:00.001000  # if microsecond != 0
      

      So for a proper conversion, we need to convert SQLite stuff to that. The MySQL backend does the same thing, but MySQL has a concept of a datetime anyway.

  • For MySQL, see also:

Declare that we’re a VARCHAR(32) on the database side.

deconstruct() Tuple[str, str, List[Any], Dict[str, Any]][source]

Takes an instance and calculates the arguments to pass to __init__ to reconstruct it.

from_db_value(value, expression, connection)[source]

Convert database value to Python value. Called when data is loaded from the database.

get_db_prep_save(value, connection, prepared=False)[source]

Convert Python value to database value for SAVING. We save with full timezone information.

get_db_prep_value(value, connection, prepared=False)[source]

Further conversion of Python value to database value for QUERYING. This follows get_prep_value(), and is for backend-specific stuff. See notes above.

get_prep_value(value)[source]

Convert Python value to database value for QUERYING. We query with UTC, so this function converts datetime values to UTC.

Calls to this function are followed by calls to get_db_prep_value(), which is for backend-specific conversions.

to_python(value: str | None) Any | None[source]

Called during deserialization and during form clean() calls. Must deal with an instance of the correct type; a string; or None (if the field allows null=True). Should raise ValidationError if problems.

class cardinal_pythonlib.django.fields.isodatetimetz.IsoStringToSourceDate(*args, **kwargs)[source]

SQL expression: converts ISO-8601 field into DATE, using the date part of the local ISO time (not the UTC date).

property output_field: Field

Return the output type of this expressions.

class cardinal_pythonlib.django.fields.isodatetimetz.IsoStringToUtcDate(*args, **kwargs)[source]

SQL expression: converts ISO-8601 field into DATE, using the UTC date.

property output_field: Field

Return the output type of this expressions.

class cardinal_pythonlib.django.fields.isodatetimetz.IsoStringToUtcDateTime(*args, **kwargs)[source]

SQL expression: converts ISO-8601 field into UTC DATETIME.

property output_field: Field

Return the output type of this expressions.

cardinal_pythonlib.django.fields.isodatetimetz.iso_string_to_python_datetime(isostring: str) datetime | None[source]

Takes an ISO-8601 string and returns a datetime.

cardinal_pythonlib.django.fields.isodatetimetz.iso_string_to_sql_date_mysql(x: str) str[source]

Provides MySQL SQL to convert an ISO-8601-format string (with punctuation) to a DATE, just by taking the date fields (without any timezone conversion). The argument x is the SQL expression to be converted (such as a column name).

cardinal_pythonlib.django.fields.isodatetimetz.iso_string_to_sql_date_sqlite(x: str) str[source]

Provides SQLite SQL to convert a column to a DATE, just by taking the date fields (without any timezone conversion). The argument x is the SQL expression to be converted (such as a column name).

cardinal_pythonlib.django.fields.isodatetimetz.iso_string_to_sql_utcdate_mysql(x: str) str[source]

Provides MySQL SQL to convert an ISO-8601-format string (with punctuation) to a DATE in UTC. The argument x is the SQL expression to be converted (such as a column name).

cardinal_pythonlib.django.fields.isodatetimetz.iso_string_to_sql_utcdate_sqlite(x: str) str[source]

Provides SQLite SQL to convert a column to a DATE in UTC. The argument x is the SQL expression to be converted (such as a column name).

cardinal_pythonlib.django.fields.isodatetimetz.iso_string_to_sql_utcdatetime_mysql(x: str) str[source]

Provides MySQL SQL to convert an ISO-8601-format string (with punctuation) to a DATETIME in UTC. The argument x is the SQL expression to be converted (such as a column name).

cardinal_pythonlib.django.fields.isodatetimetz.iso_string_to_sql_utcdatetime_pythonformat_sqlite(x: str) str[source]

Provides SQLite SQL to convert a column to a DATETIME in UTC, in a string format that matches a common Python format. The argument x is the SQL expression to be converted (such as a column name).

Output like

2015-11-14 18:52:47
2015-11-14 18:52:47.247000

i.e.

  • gets rid of trailing .000 for zero milliseconds, appends trailing 000 for everything else,

  • … thus matching the output of Python’s str(x) where x is a datetime.

  • … thus matching the RHS of a Django default datetime comparison.

cardinal_pythonlib.django.fields.isodatetimetz.iso_string_to_sql_utcdatetime_sqlite(x: str) str[source]

Provides SQLite SQL to convert a column to a DATETIME in UTC. The argument x is the SQL expression to be converted (such as a column name).

Output like:

2015-11-14 18:52:47.000
2015-11-14 18:52:47.247

Internally, we don’t use DATETIME(); using STRFTIME() allows millsecond precision.

cardinal_pythonlib.django.fields.isodatetimetz.isodt_lookup_mysql(lookup, compiler, connection, operator) Tuple[str, Any][source]

For a comparison “LHS op RHS”, transforms the LHS from a column containing an ISO-8601 date/time into an SQL DATETIME, for MySQL.

cardinal_pythonlib.django.fields.isodatetimetz.isodt_lookup_sqlite(lookup, compiler, connection, operator) Tuple[str, Any][source]

For a comparison “LHS op RHS”, transforms the LHS from a column containing an ISO-8601 date/time into an SQL DATETIME, for SQLite.

cardinal_pythonlib.django.fields.isodatetimetz.python_localized_datetime_to_human_iso(value: datetime) str[source]

Converts a Python datetime that has a timezone to an ISO-8601 string with : between the hours and minutes of the timezone.

Example

>>> import datetime
>>> x = datetime.datetime.now(datetime.timezone.utc)
>>> python_localized_datetime_to_human_iso(x)
'2017-08-21T20:47:18.952971+00:00'
cardinal_pythonlib.django.fields.isodatetimetz.python_utc_datetime_to_sqlite_strftime_string(value: datetime) str[source]

Converts a Python datetime to a string literal compatible with SQLite, including the millisecond field.