Repository: PrefectHQ/prefect

Issue:
DTSTART doesn't work with RRule string set in deployment build
### First check

- [X] I added a descriptive title to this issue.
- [X] I used the GitHub search to find a similar issue and didn't find it.
- [X] I searched the Prefect documentation for this issue.
- [X] I checked that this issue is related to Prefect and not one of its dependencies.

### Bug summary

Many rrules require a start date, e.g. to schedule a daily interval for `n` number of occurrences

This works:

```
prefect deployment build -n prod -q prod -a flows/hello.py:hello --rrule 'RRULE:FREQ=DAILY;COUNT=7;BYDAY=MO,TU,WE,TH,FR;BYHOUR=9'
```

But this doesn't work:

### Reproduction

```python
prefect deployment build -n prod -q prod -a flows/hello.py:hello --rrule 'DTSTART:20220909T110000\nRRULE:FREQ=DAILY;COUNT=7;BYDAY=MO,TU,WE,TH,FR'
```


### Error

```
Traceback (most recent call last):
  File "/Users/anna/repos/prefect/src/prefect/cli/_utilities.py", line 41, in wrapper
    return fn(*args, **kwargs)
  File "/Users/anna/repos/prefect/src/prefect/utilities/asyncutils.py", line 212, in wrapper
    return run_async_in_new_loop(async_fn, *args, **kwargs)
  File "/Users/anna/repos/prefect/src/prefect/utilities/asyncutils.py", line 141, in run_async_in_new_loop
    return anyio.run(partial(__fn, *args, **kwargs))
  File "/opt/miniconda3/envs/dev/lib/python3.9/site-packages/anyio/_core/_eventloop.py", line 70, in run
    return asynclib.run(func, *args, **backend_options)
  File "/opt/miniconda3/envs/dev/lib/python3.9/site-packages/anyio/_backends/_asyncio.py", line 292, in run
    return native_run(wrapper(), debug=debug)
  File "/opt/miniconda3/envs/dev/lib/python3.9/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/opt/miniconda3/envs/dev/lib/python3.9/asyncio/base_events.py", line 647, in run_until_complete
    return future.result()
  File "/opt/miniconda3/envs/dev/lib/python3.9/site-packages/anyio/_backends/_asyncio.py", line 287, in wrapper
    return await func(*args)
  File "/Users/anna/repos/prefect/src/prefect/cli/deployment.py", line 635, in build
    schedule = RRuleSchedule(rrule=rrule)
  File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for RRuleSchedule
rrule
  Invalid RRule string "DTSTART:20220909T110000\nRRULE:FREQ=DAILY;COUNT=7;BYDAY=MO,TU,WE,TH,FR": Unknown string format: 20220909T110000\NRRULE:FREQ=DAILY;COUNT=7;BYDAY=MO (type=value_error)
```


### Versions

```
Version:             2.3.2
API version:         0.8.0
Python version:      3.9.12
Git commit:          6e931ee9
Built:               Tue, Sep 6, 2022 12:36 PM
OS/Arch:             darwin/arm64
Profile:             default
Server type:         hosted
```


### Additional context

_No response_


Discussion:
For reference, this works with direct instantiation:

```python
from prefect.orion.schemas.schedules import RRuleSchedule
RRuleSchedule(rrule='DTSTART:20220909T110000\nRRULE:FREQ=DAILY;COUNT=7;BYDAY=MO,TU,WE,TH,FR')
```

It even works if I use the string from the error so I don't see this as a parsing error. I'm a bit confused.
could it be the slash trying to escape something?
if you look at the error, it capitalized N, i.e. new line \n becomes \N in:
```
Invalid RRule string "DTSTART:20220909T110000\nRRULE:FREQ=DAILY;COUNT=7;BYDAY=MO,TU,WE,TH,FR": Unknown string format: 20220909T110000\NRRULE:FREQ=DAILY;COUNT=7;BYDAY=MO (type=value_error)
```
Hm, I don't think the prints are actually parsable strings. There are a few open issues related to that.

This seems like it should be supported https://dateutil.readthedocs.io/en/stable/rrule.html?highlight=dtstart#rrulestr-examples

Does this work if you use a space instead of `\n`?

not sure if you would be opposed, Michael, but if we could instead set it as a JSON dictionary, it would not only solve this issue, but would also allow setting timezone what currently doesn't work:

![image](https://user-images.githubusercontent.com/86264395/189151490-41faf1c1-8758-422b-8fe9-bb1fc1d56d0a.png)

![image](https://user-images.githubusercontent.com/86264395/189151598-69ecb8e6-4d95-4e75-b214-6bc755ec0d61.png)


this would also help us with this PR from Dustin: https://github.com/PrefectHQ/prefect/pull/6697

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
@@ -1,6 +1,7 @@
 """
 Command line interface for working with deployments.
 """
+import json
 from datetime import timedelta
 from enum import Enum
 from pathlib import Path
@@ -528,7 +529,10 @@ async def build(
     elif interval:
         schedule = IntervalSchedule(interval=timedelta(seconds=interval))
     elif rrule:
-        schedule = RRuleSchedule(rrule=rrule)
+        try:
+            schedule = RRuleSchedule(**json.loads(rrule))
+        except json.JSONDecodeError:
+            schedule = RRuleSchedule(rrule=rrule)
 
     # 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
@@ -165,6 +165,52 @@ def test_auto_apply_flag(self, patch_import, tmp_path):
             temp_dir=tmp_path,
         )
 
+    def test_parsing_rrule_schedule_string_literal(self, patch_import, tmp_path):
+        invoke_and_assert(
+            [
+                "deployment",
+                "build",
+                "fake-path.py:fn",
+                "-n",
+                "TEST",
+                "-o",
+                str(tmp_path / "test.yaml"),
+                "--rrule",
+                "DTSTART:20220910T110000\nRRULE:FREQ=HOURLY;BYDAY=MO,TU,WE,TH,FR,SA;BYHOUR=9,10,11,12,13,14,15,16,17",
+            ],
+            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"
+        )
+
+    def test_parsing_rrule_schedule_json(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"}',
+            ],
+            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"
+        )
+
 
 class TestOutputMessages:
     def test_message_with_work_queue_name(self, patch_import, tmp_path):