Repository: PrefectHQ/prefect

Issue:
Timezone parameter to deployment cli for cron schedule
### First check

- [X] I added a descriptive title to this issue.
- [X] I used the GitHub search to find a similar request and didn't find it.
- [X] I searched the Prefect documentation for this feature.

### Prefect Version

2.x

### Describe the current behavior

Currently when we add a deployment through cli with a cron schedule the default timezone "UTC" is used for the schedule. 
When deploying with a Python script we can use `CronSchedule()` and specify a timezone there.


### Describe the proposed behavior

There should be a additional parameter to specify the timezone eg. `--crontz` which only can be set if `--cron` was specified. This is fully optional. If not specified UTC is used as a fallback.

Another solution would be to specifiy a default timezone for the whole orion instance.

### Example Use

`prefect deployment build --cron "0 4 * * *"....` -> results in "0 4 * * *" in UTC timezone (current behavior).

`prefect deployment build --cron "0 4 * * *" --crontz "Europe/Berlin"....` -> results in "0 4 * * *" in Europe/Berlin timezone.


### Additional context

_No response_


Discussion:
@anticorrelator do you think we should add it? seems very useful

@sti0 thx for flagging, for now you can do that in a separate CLI command after build:

```
prefect deployment set-schedule flow_name/deploy_name --cron '0 9 * * *' --timezone 'America/New_York'
```
I think if all we need is to pass an optional timezone string into the schedule object I think this should be pretty straightforward!

Resolving patch:
diff --git a/src/prefect/cli/deployment.py b/src/prefect/cli/deployment.py
--- a/src/prefect/cli/deployment.py
+++ b/src/prefect/cli/deployment.py
@@ -238,6 +238,9 @@ async def set_schedule(
     cron_schedule = {"cron": cron_string, "day_or": cron_day_or, "timezone": timezone}
     if rrule_string is not None:
         rrule_schedule = json.loads(rrule_string)
+        if timezone:
+            # override timezone if specified via CLI argument
+            rrule_schedule.update({"timezone": timezone})
     else:
         # fall back to empty schedule dictionary
         rrule_schedule = {"rrule": None}
@@ -675,11 +678,19 @@ async def build(
         "--interval",
         help="An integer specifying an interval (in seconds) that will be used to set an IntervalSchedule on the deployment.",
     ),
+    interval_anchor: Optional[str] = typer.Option(
+        None, "--anchor-date", help="The anchor date for an interval schedule"
+    ),
     rrule: str = typer.Option(
         None,
         "--rrule",
         help="An RRule that will be used to set an RRuleSchedule on the deployment.",
     ),
+    timezone: str = typer.Option(
+        None,
+        "--timezone",
+        help="Deployment schedule timezone string e.g. 'America/New_York'",
+    ),
     path: str = typer.Option(
         None,
         "--path",
@@ -771,16 +782,32 @@ async def build(
         # server-side definition of this deployment
         infrastructure = None
 
+    if interval_anchor and not interval:
+        exit_with_error("An anchor date can only be provided with an interval schedule")
+
     schedule = None
     if cron:
-        schedule = CronSchedule(cron=cron)
+        cron_kwargs = {"cron": cron, "timezone": timezone}
+        schedule = CronSchedule(
+            **{k: v for k, v in cron_kwargs.items() if v is not None}
+        )
     elif interval:
-        schedule = IntervalSchedule(interval=timedelta(seconds=interval))
+        interval_kwargs = {
+            "interval": timedelta(seconds=interval),
+            "anchor_date": interval_anchor,
+            "timezone": timezone,
+        }
+        schedule = IntervalSchedule(
+            **{k: v for k, v in interval_kwargs.items() if v is not None}
+        )
     elif rrule:
         try:
             schedule = RRuleSchedule(**json.loads(rrule))
+            if timezone:
+                # override timezone if specified via CLI argument
+                schedule.timezone = timezone
         except json.JSONDecodeError:
-            schedule = RRuleSchedule(rrule=rrule)
+            schedule = RRuleSchedule(rrule=rrule, timezone=timezone)
 
     # parse storage_block
     if storage_block:


Test patch:
diff --git a/tests/cli/test_deployment_cli.py b/tests/cli/test_deployment_cli.py
--- a/tests/cli/test_deployment_cli.py
+++ b/tests/cli/test_deployment_cli.py
@@ -1,6 +1,7 @@
 import json
 from datetime import timedelta
 
+import pendulum
 import pytest
 
 from prefect import flow
@@ -252,6 +253,73 @@ def test_mixing_parameter_overrides(self, patch_import, tmp_path):
             temp_dir=tmp_path,
         )
 
+    def test_passing_cron_schedules_to_build(self, patch_import, tmp_path):
+        invoke_and_assert(
+            [
+                "deployment",
+                "build",
+                "fake-path.py:fn",
+                "-n",
+                "TEST",
+                "-o",
+                str(tmp_path / "test.yaml"),
+                "--cron",
+                "0 4 * * *",
+                "--timezone",
+                "Europe/Berlin",
+            ],
+            expected_code=0,
+            temp_dir=tmp_path,
+        )
+
+        deployment = Deployment.load_from_yaml(tmp_path / "test.yaml")
+        assert deployment.schedule.cron == "0 4 * * *"
+        assert deployment.schedule.timezone == "Europe/Berlin"
+
+    def test_passing_interval_schedules_to_build(self, patch_import, tmp_path):
+        invoke_and_assert(
+            [
+                "deployment",
+                "build",
+                "fake-path.py:fn",
+                "-n",
+                "TEST",
+                "-o",
+                str(tmp_path / "test.yaml"),
+                "--interval",
+                "42",
+                "--anchor-date",
+                "2018-02-02",
+                "--timezone",
+                "America/New_York",
+            ],
+            expected_code=0,
+            temp_dir=tmp_path,
+        )
+
+        deployment = Deployment.load_from_yaml(tmp_path / "test.yaml")
+        assert deployment.schedule.interval == timedelta(seconds=42)
+        assert deployment.schedule.anchor_date == pendulum.parse("2018-02-02")
+        assert deployment.schedule.timezone == "America/New_York"
+
+    def test_passing_anchor_without_interval_exits(self, patch_import, tmp_path):
+        invoke_and_assert(
+            [
+                "deployment",
+                "build",
+                "fake-path.py:fn",
+                "-n",
+                "TEST",
+                "-o",
+                str(tmp_path / "test.yaml"),
+                "--anchor-date",
+                "2018-02-02",
+            ],
+            expected_code=1,
+            temp_dir=tmp_path,
+            expected_output_contains="An anchor date can only be provided with an interval schedule",
+        )
+
     def test_parsing_rrule_schedule_string_literal(self, patch_import, tmp_path):
         invoke_and_assert(
             [
@@ -372,6 +440,35 @@ def test_parsing_rrule_schedule_json(self, patch_import, tmp_path):
             deployment.schedule.rrule
             == "DTSTART:20220910T110000\nRRULE:FREQ=HOURLY;BYDAY=MO,TU,WE,TH,FR,SA;BYHOUR=9,10,11,12,13,14,15,16,17"
         )
+        assert deployment.schedule.timezone == "America/New_York"
+
+    def test_parsing_rrule_timezone_overrides_if_passed_explicitly(
+        self, patch_import, tmp_path
+    ):
+        invoke_and_assert(
+            [
+                "deployment",
+                "build",
+                "fake-path.py:fn",
+                "-n",
+                "TEST",
+                "-o",
+                str(tmp_path / "test.yaml"),
+                "--rrule",
+                '{"rrule": "DTSTART:20220910T110000\\nRRULE:FREQ=HOURLY;BYDAY=MO,TU,WE,TH,FR,SA;BYHOUR=9,10,11,12,13,14,15,16,17", "timezone": "America/New_York"}',
+                "--timezone",
+                "Europe/Berlin",
+            ],
+            expected_code=0,
+            temp_dir=tmp_path,
+        )
+
+        deployment = Deployment.load_from_yaml(tmp_path / "test.yaml")
+        assert (
+            deployment.schedule.rrule
+            == "DTSTART:20220910T110000\nRRULE:FREQ=HOURLY;BYDAY=MO,TU,WE,TH,FR,SA;BYHOUR=9,10,11,12,13,14,15,16,17"
+        )
+        assert deployment.schedule.timezone == "Europe/Berlin"
 
 
 class TestOutputMessages:
@@ -494,6 +591,7 @@ async def rence_griffith():
             tags=["foo", "bar"],
             parameter_openapi_schema={},
         )
+        return deployment_id
 
     def test_updating_schedules(self, flojo):
         invoke_and_assert(
@@ -555,6 +653,41 @@ def test_rrule_schedules_are_parsed_properly(self, flojo):
             expected_output_contains="Updated deployment schedule!",
         )
 
+        invoke_and_assert(
+            [
+                "deployment",
+                "inspect",
+                "rence-griffith/test-deployment",
+            ],
+            expected_output_contains=["America/New_York"],
+            expected_code=0,
+        )
+
+    def test_rrule_schedule_timezone_overrides_if_passed_explicitly(self, flojo):
+        invoke_and_assert(
+            [
+                "deployment",
+                "set-schedule",
+                "rence-griffith/test-deployment",
+                "--rrule",
+                '{"rrule": "DTSTART:20220910T110000\\nRRULE:FREQ=HOURLY;BYDAY=MO,TU,WE,TH,FR,SA;BYHOUR=9,10,11,12,13,14,15,16,17", "timezone": "America/New_York"}',
+                "--timezone",
+                "Asia/Seoul",
+            ],
+            expected_code=0,
+            expected_output_contains="Updated deployment schedule!",
+        )
+
+        invoke_and_assert(
+            [
+                "deployment",
+                "inspect",
+                "rence-griffith/test-deployment",
+            ],
+            expected_output_contains=["Asia/Seoul"],
+            expected_code=0,
+        )
+
     def test_pausing_and_resuming_schedules(self, flojo):
         invoke_and_assert(
             [