diff --git a/docs/_templates/class.rst b/docs/_templates/class.rst index 4ebc9be04..ffd79f482 100644 --- a/docs/_templates/class.rst +++ b/docs/_templates/class.rst @@ -6,3 +6,6 @@ .. autoclass:: {{ name }} :members: :special-members: __init__, __call__ + {%- if name in ('IntegerType', 'NumberType') %} + :inherited-members: JSONTypeHelper + {%- endif %} diff --git a/docs/classes/typing/singer_sdk.typing.IntegerType.rst b/docs/classes/typing/singer_sdk.typing.IntegerType.rst index 23475e039..b2097acae 100644 --- a/docs/classes/typing/singer_sdk.typing.IntegerType.rst +++ b/docs/classes/typing/singer_sdk.typing.IntegerType.rst @@ -5,4 +5,5 @@ .. autoclass:: IntegerType :members: - :special-members: __init__, __call__ \ No newline at end of file + :special-members: __init__, __call__ + :inherited-members: JSONTypeHelper \ No newline at end of file diff --git a/docs/classes/typing/singer_sdk.typing.NumberType.rst b/docs/classes/typing/singer_sdk.typing.NumberType.rst index e71147bee..09c29edf9 100644 --- a/docs/classes/typing/singer_sdk.typing.NumberType.rst +++ b/docs/classes/typing/singer_sdk.typing.NumberType.rst @@ -5,4 +5,5 @@ .. autoclass:: NumberType :members: - :special-members: __init__, __call__ \ No newline at end of file + :special-members: __init__, __call__ + :inherited-members: JSONTypeHelper \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index b60cd2069..ada3ecdf6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -40,6 +40,7 @@ "sphinx.ext.napoleon", "sphinx.ext.autosectionlabel", "sphinx.ext.autosummary", + "sphinx.ext.extlinks", "sphinx.ext.intersphinx", "sphinx.ext.linkcode", "sphinx_copybutton", @@ -138,6 +139,16 @@ "porting.html": "guides/porting.html", } +# -- Options for extlinks ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html + +extlinks = { + "jsonschema": ( + "https://json-schema.org/understanding-json-schema/reference/%s", + "%s", + ), +} + # -- Options for intersphinx ----------------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration intersphinx_mapping = { diff --git a/singer_sdk/typing.py b/singer_sdk/typing.py index 7a412fe81..8e68a13ab 100644 --- a/singer_sdk/typing.py +++ b/singer_sdk/typing.py @@ -266,22 +266,46 @@ class StringType(JSONTypeHelper[str]): {'type': ['string']} >>> StringType(allowed_values=["a", "b"]).type_dict {'type': ['string'], 'enum': ['a', 'b']} + >>> StringType(max_length=10).type_dict + {'type': ['string'], 'maxLength': 10} """ string_format: str | None = None """String format. - See the `formats built into the JSON Schema specification`_. + See the :jsonschema:`JSON Schema reference ` for a list of + all the built-in formats. Returns: A string describing the format. - - .. _`formats built into the JSON Schema specification`: - https://json-schema.org/understanding-json-schema/reference/string.html#built-in-formats """ + def __init__( + self, + *, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | None = None, + **kwargs: t.Any, + ) -> None: + """Initialize StringType. + + Args: + min_length: Minimum length of the string. See the + :jsonschema:`JSON Schema reference ` for details. + max_length: Maximum length of the string. See the + :jsonschema:`JSON Schema reference ` for details. + pattern: A regular expression pattern that the string must match. See the + :jsonschema:`JSON Schema reference ` for details. + **kwargs: Additional keyword arguments to pass to the parent class. + """ + super().__init__(**kwargs) + self.min_length = min_length + self.max_length = max_length + self.pattern = pattern + @property - def _format(self) -> dict: + def _format(self) -> dict[str, t.Any]: return {"format": self.string_format} if self.string_format else {} @DefaultInstanceProperty @@ -291,12 +315,23 @@ def type_dict(self) -> dict: Returns: A dictionary describing the type. """ - return { + result = { "type": ["string"], **self._format, **self.extras, } + if self.max_length is not None: + result["maxLength"] = self.max_length + + if self.min_length is not None: + result["minLength"] = self.min_length + + if self.pattern is not None: + result["pattern"] = self.pattern + + return result + class DateTimeType(StringType): """DateTime type. @@ -423,7 +458,71 @@ def type_dict(self) -> dict: return {"type": ["boolean"], **self.extras} -class IntegerType(JSONTypeHelper): +class _NumericType(JSONTypeHelper[T]): + """Abstract numeric type for integers and numbers.""" + + __type_name__: str + + def __init__( + self, + *, + minimum: int | None = None, + maximum: int | None = None, + exclusive_minimum: int | None = None, + exclusive_maximum: int | None = None, + multiple_of: int | None = None, + **kwargs: t.Any, + ) -> None: + """Initialize IntegerType. + + Args: + minimum: Minimum numeric value. See the + :jsonschema:`JSON Schema reference ` for details. + maximum: Maximum numeric value. + :jsonschema:`JSON Schema reference ` for details. + exclusive_minimum: Exclusive minimum numeric value. + :jsonschema:`JSON Schema reference ` for details. + exclusive_maximum: Exclusive maximum numeric value. See the + :jsonschema:`JSON Schema reference ` for details. + multiple_of: A number that the value must be a multiple of. See the + :jsonschema:`JSON Schema reference ` for details. + **kwargs: Additional keyword arguments to pass to the parent class. + """ + super().__init__(**kwargs) + self.minimum = minimum + self.maximum = maximum + self.exclusive_minimum = exclusive_minimum + self.exclusive_maximum = exclusive_maximum + self.multiple_of = multiple_of + + @DefaultInstanceProperty + def type_dict(self) -> dict: + """Get type dictionary. + + Returns: + A dictionary describing the type. + """ + result = {"type": [self.__type_name__], **self.extras} + + if self.minimum is not None: + result["minimum"] = self.minimum + + if self.maximum is not None: + result["maximum"] = self.maximum + + if self.exclusive_minimum is not None: + result["exclusiveMinimum"] = self.exclusive_minimum + + if self.exclusive_maximum is not None: + result["exclusiveMaximum"] = self.exclusive_maximum + + if self.multiple_of is not None: + result["multipleOf"] = self.multiple_of + + return result + + +class IntegerType(_NumericType[int]): """Integer type. Examples: @@ -433,19 +532,18 @@ class IntegerType(JSONTypeHelper): {'type': ['integer']} >>> IntegerType(allowed_values=[1, 2]).type_dict {'type': ['integer'], 'enum': [1, 2]} + >>> IntegerType(minimum=0, maximum=10).type_dict + {'type': ['integer'], 'minimum': 0, 'maximum': 10} + >>> IntegerType(exclusive_minimum=0, exclusive_maximum=10).type_dict + {'type': ['integer'], 'exclusiveMinimum': 0, 'exclusiveMaximum': 10} + >>> IntegerType(multiple_of=2).type_dict + {'type': ['integer'], 'multipleOf': 2} """ - @DefaultInstanceProperty - def type_dict(self) -> dict: - """Get type dictionary. - - Returns: - A dictionary describing the type. - """ - return {"type": ["integer"], **self.extras} + __type_name__ = "integer" -class NumberType(JSONTypeHelper[float]): +class NumberType(_NumericType[float]): """Number type. Examples: @@ -455,16 +553,15 @@ class NumberType(JSONTypeHelper[float]): {'type': ['number']} >>> NumberType(allowed_values=[1.0, 2.0]).type_dict {'type': ['number'], 'enum': [1.0, 2.0]} + >>> NumberType(minimum=0, maximum=10).type_dict + {'type': ['number'], 'minimum': 0, 'maximum': 10} + >>> NumberType(exclusive_minimum=0, exclusive_maximum=10).type_dict + {'type': ['number'], 'exclusiveMinimum': 0, 'exclusiveMaximum': 10} + >>> NumberType(multiple_of=2).type_dict + {'type': ['number'], 'multipleOf': 2} """ - @DefaultInstanceProperty - def type_dict(self) -> dict: - """Get type dictionary. - - Returns: - A dictionary describing the type. - """ - return {"type": ["number"], **self.extras} + __type_name__ = "number" W = t.TypeVar("W", bound=JSONTypeHelper) diff --git a/tests/core/test_jsonschema_helpers.py b/tests/core/test_jsonschema_helpers.py index 8438a6168..8fb5e90a8 100644 --- a/tests/core/test_jsonschema_helpers.py +++ b/tests/core/test_jsonschema_helpers.py @@ -502,6 +502,50 @@ def test_inbuilt_type(json_type: JSONTypeHelper, expected_json_schema: dict): }, {is_array_type, is_string_array_type}, ), + ( + Property( + "my_prop12", + StringType(min_length=5, max_length=10, pattern="^a.*b$"), + ), + { + "my_prop12": { + "type": ["string", "null"], + "minLength": 5, + "maxLength": 10, + "pattern": "^a.*b$", + }, + }, + {is_string_type}, + ), + ( + Property( + "my_prop13", + IntegerType(minimum=5, maximum=10), + ), + { + "my_prop13": { + "type": ["integer", "null"], + "minimum": 5, + "maximum": 10, + }, + }, + {is_integer_type}, + ), + ( + Property( + "my_prop14", + IntegerType(exclusive_minimum=5, exclusive_maximum=10, multiple_of=2), + ), + { + "my_prop14": { + "type": ["integer", "null"], + "exclusiveMinimum": 5, + "exclusiveMaximum": 10, + "multipleOf": 2, + }, + }, + {is_integer_type}, + ), ], ) def test_property_creation(