From 99dc6425e7ffa3dbcceeafe22070acf5085f41e4 Mon Sep 17 00:00:00 2001 From: Paul Prescod Date: Wed, 23 Feb 2022 15:04:00 -0800 Subject: [PATCH] Add 'now' variable to Snowfakery (#485) * Add 'now' variable to Snowfakery --- docs/index.md | 85 +++++++++++++++++++++++++++- snowfakery/data_generator_runtime.py | 3 +- tests/test_datetime.yml | 10 ++++ tests/test_now.yml | 5 ++ tests/test_template_funcs.py | 31 +++++++++- 5 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 tests/test_datetime.yml create mode 100644 tests/test_now.yml diff --git a/docs/index.md b/docs/index.md index a0458268..cdd08bd3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -983,7 +983,40 @@ Contact(id=3, FirstName=Gene, LastName=Wall, DepartmentCode=42Q3XX3N) #### `today` -The `today` variable returns a date representing the current date. This date does not change chanage during the execution of a single recipe. +The `today` variable returns a date +representing the current date. This date +will not chanage during the execution of +a single recipe. See the `date` function +to learn more about this type of object. + +#### `now` + +The `now` variable returns a [datetime](#datetime) +representing the current moment. It outputs +microsecond precision and is in the UTC timezone. + +Using this recipe, you can see three different +ways of outputting the timestamp: + +```yaml +# tests/test_now.yml +- object: Times + fields: + current_datetime: ${{now}} + current_datetime_as_number: ${{now.timestamp()}} + current_datetime_without_microseconds: ${{int(now.timestamp())}} +``` + +This would generate field values similar to these: + +```yaml +current_datetime=2022-02-23 15:39:49.513086+00:00, current_datetime_as_number=1645630789.513975, current_datetime_without_microseconds=1645630789 +``` + +Experimentally, this variable seems to return a unique value +every time it is called, but it might depend on +your operating system and hardware setup +(e.g. a very fast CPU with a very slow system clock). #### `fake:` and `fake.` @@ -1013,12 +1046,60 @@ the_date: ${{date("2018-10-30")}} another_date: ${{date(year=2018, month=11, day=30)}} ``` +These objects have the following attributes: + +- date.year: the year +- date.month: between 1 and 12 inclusive. +- date.day: between 1 and the number of days in the given month of the given year. + +And these methods: + +- date.weekday(): the day of the week as an integer, where Monday is 0 and Sunday is 6. For example, date(2002, 12, 4).weekday() == 2, a Wednesday. +- date.isoformat() - string representing the date in ISO 8601 format, YYYY-MM-DD +- strftime(format) - create a string representing the time under the control of an explicit [format string.](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) + +#### `datetime` + +The `datetime` function can generate +a new datetime object from year/month/day parts: + +```yaml +# tests/test_datetime.yml +- object: Datetimes + fields: + from_date: ${{datetime(year=2000, month=1, day=1)}} + from_datetime: ${{datetime(year=2000, month=1, day=1, hour=1, minute=1, second=1)}} + right_now: ${{now}} + hour: ${{now.hour}} + minute: ${{now.minute}} + second: ${{now.second}} + right_now_with_timezone: ${{now.astimezone()}} + to_date: ${{now.date()}} +``` + +These objects have the following attributes: + +- datetime.year: the year +- datetime.month: between 1 and 12 inclusive. +- datetime.day: between 1 and the number of days in the given month of the given year. +- datetime.hour: the hour +- datetime.minute: the minute +- datetime.second: the second + +And these methods: + +- date.weekday(): the day of the week as an integer, where Monday is 0 and Sunday is 6. For example, date(2002, 12, 4).weekday() == 2, a Wednesday. +- date.isoformat() - string representing the date in ISO 8601 format, YYYY-MM-DD +- strftime(format) - create a string representing the time under the control of an explicit [format string.](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) +- astimezone() - create a timezone-aware datetime for your current timezone +- date() - convert a datetime into a date. + #### `relativedelta` The [`relativedelta` function](https://dateutil.readthedocs.io/en/stable/relativedelta.html) from `dateutil` is available for use in calculations. For example: ```yaml -${{ date(Date_Established__c) + relativedelta(months=child_index) }} +${{ date(Date_Established__c) + relativedelta(months=) }} ``` Some plugins are also interested in a `template` variable that has an `id` attribute that represents a unique identifier for the current template. Look at diff --git a/snowfakery/data_generator_runtime.py b/snowfakery/data_generator_runtime.py index 777117cd..5ca82df7 100644 --- a/snowfakery/data_generator_runtime.py +++ b/snowfakery/data_generator_runtime.py @@ -1,5 +1,5 @@ from collections import defaultdict, ChainMap -from datetime import date +from datetime import date, datetime, timezone from contextlib import contextmanager from typing import Optional, Dict, Sequence, Mapping, NamedTuple, Set @@ -586,6 +586,7 @@ def simple_field_vars(self): "child_index": obj._child_index if obj else None, "this": obj, "today": interpreter.globals.today, + "now": datetime.now(timezone.utc), "fake": self.runtime_context.faker_template_library, "template": self.runtime_context.current_template, **interpreter.options, diff --git a/tests/test_datetime.yml b/tests/test_datetime.yml new file mode 100644 index 00000000..88561533 --- /dev/null +++ b/tests/test_datetime.yml @@ -0,0 +1,10 @@ +- object: Datetimes + fields: + from_date: ${{datetime(year=2000, month=1, day=1)}} + from_datetime: ${{datetime(year=2000, month=1, day=1, hour=1, minute=1, second=1)}} + right_now: ${{now}} + hour: ${{now.hour}} + minute: ${{now.minute}} + second: ${{now.second}} + right_now_with_timezone: ${{now.astimezone()}} + to_date: ${{now.date()}} diff --git a/tests/test_now.yml b/tests/test_now.yml new file mode 100644 index 00000000..5c07c2e2 --- /dev/null +++ b/tests/test_now.yml @@ -0,0 +1,5 @@ +- object: Times + fields: + current_datetime: ${{now}} + current_datetime_as_number: ${{now.timestamp()}} + current_datetime_without_microseconds: ${{int(now.timestamp())}} diff --git a/tests/test_template_funcs.py b/tests/test_template_funcs.py index b5f9d309..395ae660 100644 --- a/tests/test_template_funcs.py +++ b/tests/test_template_funcs.py @@ -1,8 +1,8 @@ from io import StringIO from unittest import mock import pydantic -import datetime +from datetime import datetime, date from snowfakery.data_generator import generate from snowfakery.data_gen_exceptions import DataGenError @@ -244,6 +244,33 @@ def test_date_from_datetime(self, write_row): generate(StringIO(yaml), {}, None) assert write_row.mock_calls[0][1][1]["a"] == "2012-01-01" + def test_now_variable(self, generated_rows): + yaml = """ + - object : A + fields: + a: ${{now}} + b: ${{now}} + """ + generate(StringIO(yaml), {}, None) + assert datetime.fromisoformat(generated_rows.table_values("A", 0, "a")) + assert datetime.fromisoformat(generated_rows.table_values("A", 0, "b")) + assert generated_rows.table_values("A", 0, "a") != generated_rows.table_values( + "A", 0, "b" + ) + + @mock.patch("snowfakery.data_generator_runtime.datetime") + def test_now_calls_datetime_now(self, datetime): + now = datetime.now = mock.Mock() + yaml = """ + - object : A + fields: + a: ${{now}} + b: ${{now}} + c: ${{now}} + """ + generate(StringIO(yaml)) + assert len(now.mock_calls) == 3 + @mock.patch(write_row_path) def test_old_syntax(self, write_row): yaml = """ @@ -417,7 +444,7 @@ def test_random_choice_nonstring_keys(self, generated_rows): class ResultModel(pydantic.BaseModel): id: int bool: bool - date: datetime.date + date: date assert ResultModel(**result)