Repository: meltano/sdk
Pull request #1098 (2022-10-21 23:22:54)

feat: add JSON Schema `Property` helpers for `allowed_values` (`enum`) and `examples`
Ref docs: https://meltano-sdk--1098.org.readthedocs.build/en/1098/typing.html#singer_sdk.typing.Property


<!-- readthedocs-preview meltano-sdk start -->
----
:books: Documentation preview :books:: https://meltano-sdk--1098.org.readthedocs.build/en/1098/

<!-- readthedocs-preview meltano-sdk end -->

codecov[bot]: # [Codecov](https://codecov.io/gh/meltano/sdk/pull/1098?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano) Report
> Merging [#1098](https://codecov.io/gh/meltano/sdk/pull/1098?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano) (28dd4c3) into [main](https://codecov.io/gh/meltano/sdk/commit/253851e005a92549684959f4f6150abc0617264b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano) (253851e) will **increase** coverage by `0.02%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head 28dd4c3 differs from pull request most recent head e640769. Consider uploading reports for the commit e640769 to get more accurate results

```diff
@@            Coverage Diff             @@
##             main    #1098      +/-   ##
==========================================
+ Coverage   83.41%   83.43%   +0.02%     
==========================================
  Files          40       40              
  Lines        3840     3846       +6     
  Branches      655      657       +2     
==========================================
+ Hits         3203     3209       +6     
  Misses        472      472              
  Partials      165      165              
```


| [Impacted Files](https://codecov.io/gh/meltano/sdk/pull/1098?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano) | Coverage Δ | |
|---|---|---|
| [singer\_sdk/typing.py](https://codecov.io/gh/meltano/sdk/pull/1098/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano#diff-c2luZ2VyX3Nkay90eXBpbmcucHk=) | `94.73% <100.00%> (+0.17%)` | :arrow_up: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=meltano)


Patch:
diff --git a/samples/sample_tap_google_analytics/ga_tap.py b/samples/sample_tap_google_analytics/ga_tap.py
index 76fd952b..2c4d3ee5 100644
--- a/samples/sample_tap_google_analytics/ga_tap.py
+++ b/samples/sample_tap_google_analytics/ga_tap.py
@@ -23,7 +23,9 @@ class SampleTapGoogleAnalytics(Tap):
     name: str = "sample-tap-google-analytics"
     config_jsonschema = PropertiesList(
         Property("view_id", StringType(), required=True),
-        Property("client_email", StringType(), required=True),
+        Property(
+            "client_email", StringType(), required=True, examples=["me@example.com"]
+        ),
         Property("private_key", StringType(), required=True, secret=True),
     ).to_dict()
 
diff --git a/samples/sample_tap_sqlite/__init__.py b/samples/sample_tap_sqlite/__init__.py
index 5f92b85d..71583919 100644
--- a/samples/sample_tap_sqlite/__init__.py
+++ b/samples/sample_tap_sqlite/__init__.py
@@ -60,6 +60,7 @@ class SQLiteTap(SQLTap):
             DB_PATH_CONFIG,
             th.StringType,
             description="The path to your SQLite database file(s).",
+            examples=["./path/to/my.db", "/absolute/path/to/my.db"],
         )
     ).to_dict()
 
diff --git a/singer_sdk/typing.py b/singer_sdk/typing.py
index d39072ac..ba523a2d 100644
--- a/singer_sdk/typing.py
+++ b/singer_sdk/typing.py
@@ -5,13 +5,16 @@ Usage example:
 .. code-block:: python
 
     jsonschema = PropertiesList(
+        Property("username", StringType, required=True),
+        Property("password", StringType, required=True, secret=True),
+
         Property("id", IntegerType, required=True),
-        Property("name", StringType),
-        Property("tags", ArrayType(StringType)),
-        Property("ratio", NumberType),
+        Property("foo_or_bar", StringType, allowed_values=["foo", "bar"]),
+        Property("ratio", NumberType, examples=[0.25, 0.75, 1.0]),
         Property("days_active", IntegerType),
         Property("updated_on", DateTimeType),
         Property("is_deleted", BooleanType),
+
         Property(
             "author",
             ObjectType(
@@ -19,6 +22,7 @@ Usage example:
                 Property("name", StringType),
             )
         ),
+        Property("tags", ArrayType(StringType)),
         Property(
             "groups",
             ArrayType(
@@ -42,7 +46,7 @@ Note:
 from __future__ import annotations
 
 import sys
-from typing import Generic, Mapping, TypeVar, Union, cast
+from typing import Any, Generic, Mapping, TypeVar, Union, cast
 
 import sqlalchemy
 from jsonschema import validators
@@ -356,8 +360,10 @@ class Property(JSONTypeHelper, Generic[W]):
         wrapped: W | type[W],
         required: bool = False,
         default: _JsonValue = None,
-        description: str = None,
-        secret: bool = False,
+        description: str | None = None,
+        secret: bool | None = False,
+        allowed_values: list[Any] | None = None,
+        examples: list[Any] | None = None,
     ) -> None:
         """Initialize Property object.
 
@@ -374,6 +380,10 @@ class Property(JSONTypeHelper, Generic[W]):
             default: Default value in the JSON Schema.
             description: Long-text property description.
             secret: True if this is a credential or other secret.
+            allowed_values: A list of allowed value options, if only specific values
+                are permitted. This will define the type as an 'enum'.
+            examples: Optional. A list of one or more sample values. These may be
+                displayed to the user as hints of the expected format of inputs.
         """
         self.name = name
         self.wrapped = wrapped
@@ -381,6 +391,8 @@ class Property(JSONTypeHelper, Generic[W]):
         self.default = default
         self.description = description
         self.secret = secret
+        self.allowed_values = allowed_values or None
+        self.examples = examples or None
 
     @property
     def type_dict(self) -> dict:  # type: ignore  # OK: @classproperty vs @property
@@ -423,6 +435,10 @@ class Property(JSONTypeHelper, Generic[W]):
                     JSONSCHEMA_ANNOTATION_WRITEONLY: True,
                 }
             )
+        if self.allowed_values:
+            type_dict.update({"enum": self.allowed_values})
+        if self.examples:
+            type_dict.update({"examples": self.examples})
         return {self.name: type_dict}
 
 


Test patch:
diff --git a/tests/core/test_jsonschema_helpers.py b/tests/core/test_jsonschema_helpers.py
index 9b731af1..a23ba581 100644
--- a/tests/core/test_jsonschema_helpers.py
+++ b/tests/core/test_jsonschema_helpers.py
@@ -365,6 +365,22 @@ def test_inbuilt_type(json_type: JSONTypeHelper, expected_json_schema: dict):
             },
             {is_integer_type},
         ),
+        (
+            Property(
+                "my_prop9",
+                IntegerType,
+                allowed_values=[1, 2, 3, 4, 5, 6, 7, 8, 9],
+                examples=[1, 2, 3],
+            ),
+            {
+                "my_prop9": {
+                    "type": ["integer", "null"],
+                    "enum": [1, 2, 3, 4, 5, 6, 7, 8, 9],
+                    "examples": [1, 2, 3],
+                }
+            },
+            {is_integer_type},
+        ),
     ],
 )
 def test_property_creation(