From 6607487f988aacca521f55866eccf6140e2d90af Mon Sep 17 00:00:00 2001 From: "Aaron (\"AJ\") Steers" Date: Mon, 24 Oct 2022 09:03:28 -0700 Subject: [PATCH] feat: add JSON Schema `Property` helpers for `allowed_values` (`enum`) and `examples` (#1098) * chore: initial refactor for readability * feat: add Meltano rendering logic in private helper module * feat: add `secret=True` support in JSON Schema type helpers * change: update examples to use 'secret=True' for protected settings * chore: flake8 fix * add unit tests for type helpers * fix missing secret flag on unit test * chore: get tests passing * chore: add test for description * chore: remove commented code * chore: remove files related to #1094 * chore: revert --about updates * use constants for annotation keys * chore: bump validator to Draft7 * chore: add testing for is_secret_type * chore: add tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * chore: more tests * docs: add info to FAQ * chore: add test for integer type * feat: add `allowed_values` and `examples` to Property class * chore: add tests and samples * chore: fix missing typing import * docs: updated usage examples in typing module ref Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- samples/sample_tap_google_analytics/ga_tap.py | 4 ++- samples/sample_tap_sqlite/__init__.py | 1 + singer_sdk/typing.py | 28 +++++++++++++++---- tests/core/test_jsonschema_helpers.py | 16 +++++++++++ 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/samples/sample_tap_google_analytics/ga_tap.py b/samples/sample_tap_google_analytics/ga_tap.py index 76fd952b2..2c4d3ee50 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 5f92b85d0..715839190 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 d39072ac3..ba523a2d1 100644 --- a/singer_sdk/typing.py +++ b/singer_sdk/typing.py @@ -5,13 +5,16 @@ .. 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 @@ Property("name", StringType), ) ), + Property("tags", ArrayType(StringType)), Property( "groups", ArrayType( @@ -42,7 +46,7 @@ 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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( 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 @@ def to_dict(self) -> dict: 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} diff --git a/tests/core/test_jsonschema_helpers.py b/tests/core/test_jsonschema_helpers.py index 9b731af16..a23ba5813 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(