diff --git a/gso/products/product_blocks/site.py b/gso/products/product_blocks/site.py index 1dae1b582505ec0824bb9da67d9f3eef6d6507cc..4cd0f6874f97562dcf66504dc0f499d88260e1d3 100644 --- a/gso/products/product_blocks/site.py +++ b/gso/products/product_blocks/site.py @@ -1,10 +1,10 @@ """The product block that describes a site subscription.""" -import re from typing import Optional from orchestrator.domain.base import ProductBlockModel from orchestrator.types import SubscriptionLifecycle, strEnum -from pydantic import ConstrainedStr + +from gso.schemas.types import LatitudeCoordinate, LongitudeCoordinate class SiteTier(strEnum): @@ -20,15 +20,6 @@ class SiteTier(strEnum): TIER4 = 4 -class SnmpCoordinate(ConstrainedStr): - """An {term}`SNMP` coordinate, modeled as a constrained string. - - The coordinate must match the format of `1.35`, `-123.456`, etc. - """ - - regex = re.compile(r"^-?\d{1,3}\.\d+$") - - class SiteBlockInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="SiteBlock"): """A site that's currently inactive, see {class}`SiteBlock`.""" @@ -36,8 +27,8 @@ class SiteBlockInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INIT site_city: Optional[str] = None site_country: Optional[str] = None site_country_code: Optional[str] = None - site_latitude: Optional[SnmpCoordinate] = None - site_longitude: Optional[SnmpCoordinate] = None + site_latitude: Optional[LatitudeCoordinate] = None + site_longitude: Optional[LongitudeCoordinate] = None site_internal_id: Optional[int] = None site_bgp_community_id: Optional[int] = None site_tier: Optional[SiteTier] = None @@ -51,8 +42,8 @@ class SiteBlockProvisioning(SiteBlockInactive, lifecycle=[SubscriptionLifecycle. site_city: Optional[str] = None site_country: Optional[str] = None site_country_code: Optional[str] = None - site_latitude: Optional[SnmpCoordinate] = None - site_longitude: Optional[SnmpCoordinate] = None + site_latitude: Optional[LatitudeCoordinate] = None + site_longitude: Optional[LongitudeCoordinate] = None site_internal_id: Optional[int] = None site_bgp_community_id: Optional[int] = None site_tier: Optional[SiteTier] = None @@ -72,9 +63,9 @@ class SiteBlock(SiteBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]) site_country_code: str """The code of the corresponding country. This is also used for the {term}`FQDN`, following the example given for the site name, the country code would end up in the Y position.""" - site_latitude: SnmpCoordinate + site_latitude: LatitudeCoordinate """The latitude of the site, used for {term}`SNMP` purposes.""" - site_longitude: SnmpCoordinate + site_longitude: LongitudeCoordinate """Similar to the latitude, the longitude of a site.""" site_internal_id: int """The internal ID used within GÉANT to denote a site.""" diff --git a/gso/schemas/types.py b/gso/schemas/types.py new file mode 100644 index 0000000000000000000000000000000000000000..94aced74e3c6ba4b6703b7afee01d962603cfbd4 --- /dev/null +++ b/gso/schemas/types.py @@ -0,0 +1,40 @@ +import re +from typing import Union + +from pydantic import ConstrainedStr + + +class LatitudeCoordinate(ConstrainedStr): + """A latitude coordinate, modeled as a constrained string. + + The coordinate must match the format conforming to the latitude + range of `-`90 to +90 degrees. It can be a floating-point number or an integer. + Valid examples: 40.7128, -74.0060, 90, -90, 0 + """ + + regex = re.compile(r"^-?([1-8]?\d(\.\d+)?|90(\.0+)?)$") + + @classmethod + def validate(cls, value: Union[str]) -> Union[str]: + if not cls.regex.match(value): + raise ValueError("Invalid latitude coordinate. Valid examples: '40.7128', '-74.0060', '90', '-90', '0'.") + + return value + + +class LongitudeCoordinate(ConstrainedStr): + """A longitude coordinate, modeled as a constrained string. + + The coordinate must match the format conforming to the longitude + range of `-`180 to 180 degrees. It can be a floating point number or an integer. + Valid examples: 40.7128, -74.0060, 180, -180, 0 + """ + + regex = re.compile(r"^-?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$") + + @classmethod + def validate(cls, value: Union[str]) -> Union[str]: + if not cls.regex.match(value): + raise ValueError("Invalid longitude coordinate. Valid examples: '40.7128', '-74.0060', '180', '-180'") + + return value diff --git a/gso/workflows/site/create_site.py b/gso/workflows/site/create_site.py index b2e7406aaa9ffa868ffd201fe37c7d8e06f6b917..e495049ec17ba041d4aa7248e308a8e60c3939f2 100644 --- a/gso/workflows/site/create_site.py +++ b/gso/workflows/site/create_site.py @@ -11,8 +11,8 @@ from orchestrator.workflows.utils import wrap_create_initial_input_form from pydantic import validator from gso.products.product_blocks import site as site_pb -from gso.products.product_blocks.site import SnmpCoordinate from gso.products.product_types import site +from gso.schemas.types import LatitudeCoordinate, LongitudeCoordinate from gso.workflows.utils import customer_selector @@ -26,8 +26,8 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: # noqa: C site_city: str site_country: str site_country_code: str - site_latitude: str - site_longitude: str + site_latitude: LatitudeCoordinate + site_longitude: LongitudeCoordinate site_bgp_community_id: int site_internal_id: int site_tier: site_pb.SiteTier @@ -41,26 +41,6 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: # noqa: C except LookupError: raise ValueError("Invalid or non-existent country code, it must be in ISO 3166-1 alpha-2 format.") - @validator("site_latitude", allow_reuse=True) - def latitude_must_be_valid(cls, latitude: str) -> str | NoReturn: - def _is_valid_latitude(degree: float) -> bool: - return -90 <= degree <= 90 - - if _is_valid_latitude(float(latitude)): - return latitude - - raise ValueError("Entered latitude is not a valid value, must be between -90.0° and 90.0°.") - - @validator("site_longitude", allow_reuse=True) - def longitude_must_be_valid(cls, longitude: str) -> str | NoReturn: - def _is_valid_longitude(degree: float) -> bool: - return -180 <= degree <= 180 - - if _is_valid_longitude(float(longitude)): - return longitude - - raise ValueError("Entered longitude is not a valid value, must be between -180.0° and 180.0°.") - @validator("site_ts_address", allow_reuse=True) def ts_address_must_be_valid(cls, ts_address: str) -> str | NoReturn: try: @@ -91,8 +71,8 @@ def initialize_subscription( site_city: str, site_country: str, site_country_code: str, - site_latitude: SnmpCoordinate, - site_longitude: SnmpCoordinate, + site_latitude: LatitudeCoordinate, + site_longitude: LongitudeCoordinate, site_bgp_community_id: int, site_internal_id: int, site_ts_address: str, @@ -102,8 +82,8 @@ def initialize_subscription( subscription.site.site_city = site_city subscription.site.site_country = site_country subscription.site.site_country_code = site_country_code - subscription.site.site_latitude = site_longitude - subscription.site.site_longitude = site_latitude + subscription.site.site_latitude = site_latitude + subscription.site.site_longitude = site_longitude subscription.site.site_bgp_community_id = site_bgp_community_id subscription.site.site_internal_id = site_internal_id subscription.site.site_tier = site_tier diff --git a/test/schemas/__init__.py b/test/schemas/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/test/schemas/test_types.py b/test/schemas/test_types.py new file mode 100644 index 0000000000000000000000000000000000000000..dcfb515dbd315a92b205f4faa66a615f81ea0232 --- /dev/null +++ b/test/schemas/test_types.py @@ -0,0 +1,55 @@ +import pytest + +from gso.schemas.types import LatitudeCoordinate, LongitudeCoordinate + + +@pytest.mark.parametrize( + "input_value, is_valid", + [ + ("40.7128", True), + ("-74.0060", True), + ("90", True), + ("-90", True), + ("0", True), + ("45.6", True), + ("91", False), + ("-91", False), + ("180", False), + ("-180", False), + ("abc", False), + ("90.1", False), + ], +) +def test_latitude(input_value, is_valid): + if is_valid: + assert LatitudeCoordinate.validate(input_value) == input_value + else: + with pytest.raises(ValueError) as excinfo: + LatitudeCoordinate.validate(input_value) + assert "Invalid latitude coordinate" in str(excinfo.value) + + +@pytest.mark.parametrize( + "input_value, is_valid", + [ + ("40.7128", True), + ("-74.0060", True), + ("180", True), + ("-180", True), + ("0", True), + ("90.1", True), + ("181", False), + ("-181", False), + ("200", False), + ("-200", False), + ("abc", False), + ("90a", False), + ], +) +def test_longitude(input_value, is_valid): + if is_valid: + assert LongitudeCoordinate.validate(input_value) == input_value + else: + with pytest.raises(ValueError) as excinfo: + LongitudeCoordinate.validate(input_value) + assert "Invalid longitude coordinate" in str(excinfo.value)