From 9ab9d011dec7c7a7714afa66bcf59de7ac42d9a9 Mon Sep 17 00:00:00 2001 From: sblack-usu Date: Mon, 5 Jan 2026 13:20:59 -0700 Subject: [PATCH 01/10] add basic support for tool resource metadata schemas --- hsmodels/schemas/__init__.py | 6 ++- hsmodels/schemas/rdf/resource.py | 15 +++++++ hsmodels/schemas/resource.py | 10 +++++ setup.py | 4 +- tests/data/json/webapp.json | 33 +++++++++++++++ tests/data/metadata/webapp_meta.xml | 62 +++++++++++++++++++++++++++++ tests/test_defaults.py | 6 ++- tests/test_metadata.py | 1 + tests/test_metadata_json.py | 3 ++ tests/test_schemas.py | 3 ++ 10 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 tests/data/json/webapp.json create mode 100644 tests/data/metadata/webapp_meta.xml diff --git a/hsmodels/schemas/__init__.py b/hsmodels/schemas/__init__.py index 29b647a..f1e89a3 100644 --- a/hsmodels/schemas/__init__.py +++ b/hsmodels/schemas/__init__.py @@ -31,13 +31,14 @@ TimeSeriesMetadataInRDF, CSVFileMetadataInRDF, ) -from hsmodels.schemas.rdf.resource import CollectionMetadataInRDF, ResourceMap, ResourceMetadataInRDF -from hsmodels.schemas.resource import CollectionMetadata, ResourceMetadata +from hsmodels.schemas.rdf.resource import CollectionMetadataInRDF, ResourceMap, ResourceMetadataInRDF, WebAppMetadataInRDF +from hsmodels.schemas.resource import CollectionMetadata, ResourceMetadata, WebAppMetadata rdf_schemas = { ORE.ResourceMap: ResourceMap, HSTERMS.CompositeResource: ResourceMetadataInRDF, HSTERMS.CollectionResource: CollectionMetadataInRDF, + HSTERMS.ToolResource: WebAppMetadataInRDF, HSTERMS.GeographicRasterAggregation: GeographicRasterMetadataInRDF, HSTERMS.GeographicFeatureAggregation: GeographicFeatureMetadataInRDF, HSTERMS.MultidimensionalAggregation: MultidimensionalMetadataInRDF, @@ -53,6 +54,7 @@ user_schemas = { ResourceMetadataInRDF: ResourceMetadata, CollectionMetadataInRDF: CollectionMetadata, + WebAppMetadataInRDF: WebAppMetadata, GeographicRasterMetadataInRDF: GeographicRasterMetadata, GeographicFeatureMetadataInRDF: GeographicFeatureMetadata, MultidimensionalMetadataInRDF: MultidimensionalMetadata, diff --git a/hsmodels/schemas/rdf/resource.py b/hsmodels/schemas/rdf/resource.py index b0917b9..ae40066 100644 --- a/hsmodels/schemas/rdf/resource.py +++ b/hsmodels/schemas/rdf/resource.py @@ -130,3 +130,18 @@ class CollectionMetadataInRDF(BaseResource): @field_serializer('dc_type', 'rdf_type') def serialize_url(self, _type: URIRef, _info): return AnyUrl(_type) + + +class WebAppMetadataInRDF(BaseResource): + dc_type: AnyUrl = Field( + json_schema_extra={"rdf_predicate": DC.type}, default=HSTERMS.ToolResource, frozen=True + ) + rdf_type: AnyUrl = Field( + json_schema_extra={"rdf_predicate": RDF.type}, frozen=True, default=HSTERMS.ToolResource + ) + _label_literal = Literal["Web App Resource"] + label: _label_literal = Field(default="Web App Resource", frozen=True, alias='label') + + @field_serializer('dc_type', 'rdf_type') + def serialize_url(self, _type: URIRef, _info): + return AnyUrl(_type) diff --git a/hsmodels/schemas/resource.py b/hsmodels/schemas/resource.py index 9538b56..435f86c 100644 --- a/hsmodels/schemas/resource.py +++ b/hsmodels/schemas/resource.py @@ -196,3 +196,13 @@ class CollectionMetadata(BaseResourceMetadata): description="An object containing a URL that points to the HydroShare resource type selected from the hsterms namespace", json_schema_extra={"readOnly": True}, ) + + +class WebAppMetadata(BaseResourceMetadata): + type: Literal['ToolResource'] = Field( + frozen=True, + default="ToolResource", + title="Resource Type", + description="An object containing a URL that points to the HydroShare resource type selected from the hsterms namespace", + json_schema_extra={"readOnly": True}, + ) diff --git a/setup.py b/setup.py index 46585f1..14cdf94 100644 --- a/setup.py +++ b/setup.py @@ -4,12 +4,12 @@ setup( name='hsmodels', - version='1.0.4', + version='1.1.0', packages=find_packages(include=['hsmodels', 'hsmodels.*', 'hsmodels.schemas.*', 'hsmodels.schemas.rdf.*'], exclude=("tests",)), install_requires=[ 'rdflib<6.0.0', - 'pydantic==2.7.*', + 'pydantic==2.8.*', 'email-validator' ], url='https://github.com/hydroshare/hsmodels', diff --git a/tests/data/json/webapp.json b/tests/data/json/webapp.json new file mode 100644 index 0000000..8cd5639 --- /dev/null +++ b/tests/data/json/webapp.json @@ -0,0 +1,33 @@ +{ + "title": "GeoTrust", + "abstract": "This app is used to execute the scuint package for the MODFLOW-NWT model. During testing of the work, this app is linked to a deployed EC2 machine on AWS. Full instruction is provided at https://github.com/uva-hydroinformatics/Sciunit_HydroShare_Implementation for how a user could deploy this on AWS to reproduce this work.", + "language": "eng", + "subjects": [ + "MODFLOW-NWT-scuint" + ], + "creators": [ + { + "name": "Bakinam Essawy", + "phone": "8034634471", + "organization": "University of Virginia", + "email": "btaessawy@gmail.com", + "creator_order": 1, + "hydroshare_user_id": 878, + "identifiers": {} + } + ], + "contributors": [], + "relations": [ + { + "type": "The content of this resource is part of", + "value": "Essawy, B. (2018). ModflowNwtCollection, HydroShare, http://www.hydroshare.org/resource/bf598099ed384540aaa9284b7343a717" + } + ], + "additional_metadata": {}, + "rights": { + "statement": "This resource is shared under the Creative Commons Attribution CC BY.", + "url": "http://creativecommons.org/licenses/by/4.0/" + }, + "awards": [], + "citation": "Essawy, B. (2018). GeoTrust, HydroShare, http://www.hydroshare.org/resource/126701df868e4da9872d9b533db34ae6" +} \ No newline at end of file diff --git a/tests/data/metadata/webapp_meta.xml b/tests/data/metadata/webapp_meta.xml new file mode 100644 index 0000000..52054f1 --- /dev/null +++ b/tests/data/metadata/webapp_meta.xml @@ -0,0 +1,62 @@ + + + + + + 2018-06-12T17:35:56.675086+00:00 + + + + + + + + + + + Web App Resource + + + MODFLOW-NWT-scuint + eng + + + 2017-06-12T13:58:14.473992+00:00 + + + GeoTrust + + + This app is used to execute the scuint package for the MODFLOW-NWT model. During testing of the work, this app is linked to a deployed EC2 machine on AWS. Full instruction is provided at https://github.com/uva-hydroinformatics/Sciunit_HydroShare_Implementation for how a user could deploy this on AWS to reproduce this work. + + + + + + This resource is shared under the Creative Commons Attribution CC BY. + + + + + btaessawy@gmail.com + Bakinam Essawy + 1 + 878 + 8034634471 + University of Virginia + + + + + Essawy, B. (2018). ModflowNwtCollection, HydroShare, http://www.hydroshare.org/resource/bf598099ed384540aaa9284b7343a717 + + + Essawy, B. (2018). GeoTrust, HydroShare, http://www.hydroshare.org/resource/126701df868e4da9872d9b533db34ae6 + + diff --git a/tests/test_defaults.py b/tests/test_defaults.py index 8bed477..e995119 100644 --- a/tests/test_defaults.py +++ b/tests/test_defaults.py @@ -24,12 +24,13 @@ SingleFileMetadataInRDF, TimeSeriesMetadataInRDF, ) -from hsmodels.schemas.rdf.resource import CollectionMetadataInRDF, ResourceMetadataInRDF -from hsmodels.schemas.resource import CollectionMetadata, ResourceMetadata +from hsmodels.schemas.rdf.resource import CollectionMetadataInRDF, ResourceMetadataInRDF, WebAppMetadataInRDF +from hsmodels.schemas.resource import CollectionMetadata, ResourceMetadata, WebAppMetadata schema_list_count = [ (ResourceMetadata, 5), (CollectionMetadata, 5), + (WebAppMetadata, 5), (GeographicRasterMetadata, 1), (GeographicFeatureMetadata, 2), (MultidimensionalMetadata, 2), @@ -41,6 +42,7 @@ (ModelInstanceMetadata, 1), (ResourceMetadataInRDF, 8), (CollectionMetadataInRDF, 8), + (WebAppMetadataInRDF, 8), (GeographicRasterMetadataInRDF, 3), (GeographicFeatureMetadataInRDF, 4), (MultidimensionalMetadataInRDF, 4), diff --git a/tests/test_metadata.py b/tests/test_metadata.py index dc84c0f..bddd5fe 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -55,6 +55,7 @@ def compare_metadatas(new_graph, original_metadata_file): 'modelprogram_meta.xml', 'modelinstance_meta.xml', 'collection_meta.xml', + 'webapp_meta.xml', 'csvfile_meta.xml', ] diff --git a/tests/test_metadata_json.py b/tests/test_metadata_json.py index 620475e..fdaee76 100644 --- a/tests/test_metadata_json.py +++ b/tests/test_metadata_json.py @@ -55,6 +55,7 @@ def test_resource_additional_metadata_dictionary(res_md): (ModelProgramMetadataIn, 'modelprogram.json'), (ModelInstanceMetadataIn, 'modelinstance.json'), (ResourceMetadataIn, 'collection.json'), + (ResourceMetadataIn, 'webapp.json'), (CSVFileMetadataIn, 'csvfile.json'), ] @@ -69,6 +70,8 @@ def test_metadata_json_serialization(metadata_json_input): from_schema = sorting(json.loads(md.model_dump_json())) from_file = sorting(json.loads(json_file_str)) for i in range(1, len(from_file)): + if i >= len(from_schema): + assert False, f"Missing field {from_file[i]} in from_schema" assert from_file[i] == from_schema[i] diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 29799e0..8552824 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -2,6 +2,7 @@ from hsmodels.schemas import ( CollectionMetadata, + WebAppMetadata, FileSetMetadata, GeographicFeatureMetadata, GeographicRasterMetadata, @@ -26,6 +27,7 @@ read_only_fields = [ (ResourceMetadata, ['type', 'identifier', 'created', 'modified', 'review_started', 'published', 'url']), (CollectionMetadata, ['type', 'identifier', 'created', 'modified', 'review_started', 'published', 'url']), + (WebAppMetadata, ['type', 'identifier', 'created', 'modified', 'review_started', 'published', 'url']), (GeographicRasterMetadata, ['type', 'url']), (ModelProgramMetadata, ['type', 'url']), (ModelInstanceMetadata, ['type', 'url']), @@ -59,6 +61,7 @@ def test_readonly(read_only_field): additional_metadata_fields = [ (ResourceMetadata, ['additional_metadata']), (CollectionMetadata, ['additional_metadata']), + (WebAppMetadata, ['additional_metadata']), (GeographicRasterMetadata, ['additional_metadata']), (GeographicFeatureMetadata, ['additional_metadata']), (MultidimensionalMetadata, ['additional_metadata']), From cd8d894d24349af0d26a79c9b94f8617a04ebe6c Mon Sep 17 00:00:00 2001 From: sblack-usu Date: Mon, 5 Jan 2026 15:08:00 -0700 Subject: [PATCH 02/10] update validation to allow equal to on spatial coverage limits --- hsmodels/schemas/fields.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/hsmodels/schemas/fields.py b/hsmodels/schemas/fields.py index 29c2085..168c08b 100644 --- a/hsmodels/schemas/fields.py +++ b/hsmodels/schemas/fields.py @@ -689,26 +689,26 @@ class BoxCoverage(base_models.BaseCoverage): description="A string containing a name for the place associated with the geographic coverage", ) northlimit: float = Field( - gt=-90, - lt=90, + gte=-90, + lt3=90, title="North limit", description="A floating point value containing the constant coordinate for the northernmost face or edge of the bounding box", ) eastlimit: float = Field( - gt=-180, - lt=180, + gte=-180, + lte=180, title="East limit", description="A floating point value containing the constant coordinate for the easternmost face or edge of the bounding box", ) southlimit: float = Field( - gt=-90, - lt=90, + gte=-90, + lte=90, title="South limit", description="A floating point value containing the constant coordinate for the southernmost face or edge of the bounding box", ) westlimit: float = Field( - gt=-180, - lt=180, + gte=-180, + lte=180, title="West limit", description="A floating point value containing the constant coordinate for the westernmost face or edge of the bounding box", ) @@ -824,10 +824,10 @@ class PointCoverage(base_models.BaseCoverage): description="A string containing a name for the place associated with the geographic coverage", ) east: float = Field( - gt=-180, lt=180, title="East", description="The coordinate of the point location measured in the east direction" + gte=-180, lte=180, title="East", description="The coordinate of the point location measured in the east direction" ) north: float = Field( - gt=-90, lt=90, title="North", description="The coordinate of the point location measured in the north direction" + gte=-90, lte=90, title="North", description="The coordinate of the point location measured in the north direction" ) units: str = Field( title="Units", description="The units applying to the unlabelled numeric values of north and east" From 126fb5ce72a49589b0e8955cf9526b91d637c197 Mon Sep 17 00:00:00 2001 From: sblack-usu Date: Mon, 5 Jan 2026 16:01:06 -0700 Subject: [PATCH 03/10] fix review_started term and remove max_length constraint --- hsmodels/schemas/enums.py | 2 +- hsmodels/schemas/fields.py | 5 ++--- hsmodels/schemas/rdf/fields.py | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/hsmodels/schemas/enums.py b/hsmodels/schemas/enums.py index d18f678..3ffbecb 100644 --- a/hsmodels/schemas/enums.py +++ b/hsmodels/schemas/enums.py @@ -88,7 +88,7 @@ class DateType(TermEnum): created = str(DCTERMS.created) valid = str(DCTERMS.valid) available = str(DCTERMS.available) - review_started = str(HSTERMS.reviewStarted) + review_started = str(HSTERMS.review_started) published = str(HSTERMS.published) diff --git a/hsmodels/schemas/fields.py b/hsmodels/schemas/fields.py index 168c08b..419300f 100644 --- a/hsmodels/schemas/fields.py +++ b/hsmodels/schemas/fields.py @@ -22,7 +22,6 @@ class Relation(BaseMetadata): type: RelationType = Field(title="Relation type", description="The type of relationship with the related resource") value: str = Field( - max_length=500, title="Value", description="String expressing the Full text citation, URL link for, or description of the related resource", ) @@ -38,7 +37,7 @@ class CellInformation(BaseMetadata): model_config = ConfigDict(title='Raster Cell Metadata') # TODO: Is there such a thing as "name" for CellInformation? - name: str = Field(default=None, max_length=500, title="Name", description="Name of the cell information",) + name: str = Field(default=None, title="Name", description="Name of the cell information",) rows: int = Field(default=None, title="Rows", description="The integer number of rows in the raster dataset",) columns: int = Field( @@ -274,7 +273,7 @@ class BandInformation(BaseMetadata): model_config = ConfigDict(title='Raster Band Metadata') - name: str = Field(max_length=500, title="Name", description="A string containing the name of the raster band", + name: str = Field(title="Name", description="A string containing the name of the raster band", ) variable_name: Optional[str] = Field( default=None, diff --git a/hsmodels/schemas/rdf/fields.py b/hsmodels/schemas/rdf/fields.py index a7692ca..0993535 100644 --- a/hsmodels/schemas/rdf/fields.py +++ b/hsmodels/schemas/rdf/fields.py @@ -127,7 +127,7 @@ class DateInRDF(RDFBaseModel): class RightsInRDF(RDFBaseModel): statement: str = Field(json_schema_extra={"rdf_predicate": HSTERMS.rightsStatement}) - url: AnyUrl = Field(json_schema_extra={"rdf_predicate": HSTERMS.URL}) + url: AnyUrl = Field(default=None, json_schema_extra={"rdf_predicate": HSTERMS.URL}) class CreatorInRDF(RDFBaseModel): @@ -153,7 +153,7 @@ class ContributorInRDF(RDFBaseModel): phone: str = Field(default=None, json_schema_extra={"rdf_predicate": HSTERMS.phone}) address: str = Field(default=None, json_schema_extra={"rdf_predicate": HSTERMS.address}) organization: str = Field(default=None, json_schema_extra={"rdf_predicate": HSTERMS.organization}) - email: EmailStr = Field(default=None, json_schema_extra={"rdf_predicate": HSTERMS.email}) + email: str = Field(default=None, json_schema_extra={"rdf_predicate": HSTERMS.email}) homepage: HttpUrl = Field(default=None, json_schema_extra={"rdf_predicate": HSTERMS.homepage}) hydroshare_user_id: int = Field(default=None, json_schema_extra={"rdf_predicate": HSTERMS.hydroshare_user_id}) ORCID: AnyUrl = Field(default=None, json_schema_extra={"rdf_predicate": HSTERMS.ORCID}) From 892e69c627e1b9a557e4fbb1629da10670e96a50 Mon Sep 17 00:00:00 2001 From: sblack-usu Date: Mon, 5 Jan 2026 16:22:17 -0700 Subject: [PATCH 04/10] support both reviewstarted and review_started --- hsmodels/schemas/enums.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hsmodels/schemas/enums.py b/hsmodels/schemas/enums.py index 3ffbecb..d0e9c4e 100644 --- a/hsmodels/schemas/enums.py +++ b/hsmodels/schemas/enums.py @@ -89,6 +89,7 @@ class DateType(TermEnum): valid = str(DCTERMS.valid) available = str(DCTERMS.available) review_started = str(HSTERMS.review_started) + reviewStarted = str(HSTERMS.reviewStarted) published = str(HSTERMS.published) From 5c78df3615adeb99aaa098906785eac9908229ba Mon Sep 17 00:00:00 2001 From: sblack-usu Date: Mon, 5 Jan 2026 16:35:38 -0700 Subject: [PATCH 05/10] remove validation that fails for published resource example --- hsmodels/schemas/fields.py | 9 ++------- hsmodels/schemas/rdf/resource.py | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/hsmodels/schemas/fields.py b/hsmodels/schemas/fields.py index 419300f..eaf4eec 100644 --- a/hsmodels/schemas/fields.py +++ b/hsmodels/schemas/fields.py @@ -71,6 +71,7 @@ class Rights(BaseMetadata): url: AnyUrl = Field( title="URL", description="An object containing the URL pointing to a description of the license or rights statement", + default=None ) @classmethod @@ -204,7 +205,7 @@ class Contributor(BaseMetadata): title="Organization", description="A string containing the name of the organization with which the contributor is affiliated", ) - email: Optional[EmailStr] = Field( + email: Optional[str] = Field( default=None, title="Email", description="A string containing an email address for the contributor" ) homepage: Optional[HttpUrl] = Field( @@ -721,12 +722,6 @@ class BoxCoverage(base_models.BaseCoverage): description="A string containing the name of the projection used with any parameters required, such as ellipsoid parameters, datum, standard parallels and meridians, zone, etc.", ) - @model_validator(mode='after') - def compare_north_south(self): - if self.northlimit < self.southlimit: - raise ValueError(f"North latitude [{self.northlimit}] must be greater than or equal to South latitude [{self.southlimit}]") - return self - class BoxSpatialReference(base_models.BaseCoverage): """ diff --git a/hsmodels/schemas/rdf/resource.py b/hsmodels/schemas/rdf/resource.py index ae40066..b9517c6 100644 --- a/hsmodels/schemas/rdf/resource.py +++ b/hsmodels/schemas/rdf/resource.py @@ -98,7 +98,7 @@ class BaseResource(BaseModel): _dates_constraint = field_validator('dates')(dates_constraint) _coverages_constraint = field_validator('coverages')(coverages_constraint) _coverages_spatial_constraint = field_validator('coverages')(coverages_spatial_constraint) - _sort_creators = field_validator("creators")(sort_creators) + # _sort_creators = field_validator("creators")(sort_creators) class ResourceMetadataInRDF(BaseResource): From acb4038dcabbfbe3ac26cc3b7d5c96e815676e26 Mon Sep 17 00:00:00 2001 From: sblack-usu Date: Mon, 5 Jan 2026 16:38:10 -0700 Subject: [PATCH 06/10] increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 14cdf94..103ecdf 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name='hsmodels', - version='1.1.0', + version='1.1.1', packages=find_packages(include=['hsmodels', 'hsmodels.*', 'hsmodels.schemas.*', 'hsmodels.schemas.rdf.*'], exclude=("tests",)), install_requires=[ From 42639869b5ea1824e10258131281847128c13c9b Mon Sep 17 00:00:00 2001 From: sblack-usu Date: Mon, 5 Jan 2026 17:10:25 -0700 Subject: [PATCH 07/10] make citation optional --- hsmodels/schemas/rdf/resource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hsmodels/schemas/rdf/resource.py b/hsmodels/schemas/rdf/resource.py index b9517c6..7c615f7 100644 --- a/hsmodels/schemas/rdf/resource.py +++ b/hsmodels/schemas/rdf/resource.py @@ -85,7 +85,7 @@ class BaseResource(BaseModel): awards: List[AwardInfoInRDF] = Field(json_schema_extra={"rdf_predicate": HSTERMS.awardInfo}, default=[]) coverages: List[CoverageInRDF] = Field(json_schema_extra={"rdf_predicate": DC.coverage}, default=[]) publisher: PublisherInRDF = Field(json_schema_extra={"rdf_predicate": DC.publisher}, default=None) - citation: str = Field(json_schema_extra={"rdf_predicate": DCTERMS.bibliographicCitation}) + citation: str = Field(default= None, json_schema_extra={"rdf_predicate": DCTERMS.bibliographicCitation}) _parse_rdf_subject = model_validator(mode='before')(rdf_parse_rdf_subject) _parse_coverages = model_validator(mode='before')(parse_coverages) From 5dafa417fa42584854838a4a06f5ad7c06bb5142 Mon Sep 17 00:00:00 2001 From: sblack-usu Date: Mon, 5 Jan 2026 18:06:52 -0700 Subject: [PATCH 08/10] put a condition around splits --- hsmodels/utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/hsmodels/utils.py b/hsmodels/utils.py index 6e49e2c..acc1855 100644 --- a/hsmodels/utils.py +++ b/hsmodels/utils.py @@ -3,9 +3,11 @@ def to_coverage_dict(value): value_dict = {} - for key_value in value.split("; "): - k, v = key_value.split("=") - value_dict[k] = v + if ";" in value: + for key_value in value.split("; "): + if "=" in key_value: + k, v = key_value.split("=") + value_dict[k] = v return value_dict From 5413f7cda73f4fe19b65227c8975ce2f5908a42d Mon Sep 17 00:00:00 2001 From: sblack-usu Date: Tue, 6 Jan 2026 12:01:29 -0700 Subject: [PATCH 09/10] tighten up the validators and get tests running again --- hsmodels/schemas/enums.py | 3 +-- hsmodels/schemas/fields.py | 9 +++++++++ hsmodels/schemas/rdf/resource.py | 2 +- hsmodels/schemas/rdf/validators.py | 4 ++-- hsmodels/utils.py | 9 ++++----- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/hsmodels/schemas/enums.py b/hsmodels/schemas/enums.py index d0e9c4e..d18f678 100644 --- a/hsmodels/schemas/enums.py +++ b/hsmodels/schemas/enums.py @@ -88,8 +88,7 @@ class DateType(TermEnum): created = str(DCTERMS.created) valid = str(DCTERMS.valid) available = str(DCTERMS.available) - review_started = str(HSTERMS.review_started) - reviewStarted = str(HSTERMS.reviewStarted) + review_started = str(HSTERMS.reviewStarted) published = str(HSTERMS.published) diff --git a/hsmodels/schemas/fields.py b/hsmodels/schemas/fields.py index eaf4eec..3c8012a 100644 --- a/hsmodels/schemas/fields.py +++ b/hsmodels/schemas/fields.py @@ -722,6 +722,15 @@ class BoxCoverage(base_models.BaseCoverage): description="A string containing the name of the projection used with any parameters required, such as ellipsoid parameters, datum, standard parallels and meridians, zone, etc.", ) + @model_validator(mode='after') + def compare_north_south(self): + if self.northlimit < self.southlimit: + if self.southlimit == 90 and self.northlimit == -90: + # special case for global coverage + return self + raise ValueError(f"North latitude [{self.northlimit}] must be greater than or equal to South latitude [{self.southlimit}]") + return self + class BoxSpatialReference(base_models.BaseCoverage): """ diff --git a/hsmodels/schemas/rdf/resource.py b/hsmodels/schemas/rdf/resource.py index 7c615f7..b66d022 100644 --- a/hsmodels/schemas/rdf/resource.py +++ b/hsmodels/schemas/rdf/resource.py @@ -98,7 +98,7 @@ class BaseResource(BaseModel): _dates_constraint = field_validator('dates')(dates_constraint) _coverages_constraint = field_validator('coverages')(coverages_constraint) _coverages_spatial_constraint = field_validator('coverages')(coverages_spatial_constraint) - # _sort_creators = field_validator("creators")(sort_creators) + _sort_creators = field_validator("creators")(sort_creators) class ResourceMetadataInRDF(BaseResource): diff --git a/hsmodels/schemas/rdf/validators.py b/hsmodels/schemas/rdf/validators.py index 30a0797..db1c1bc 100644 --- a/hsmodels/schemas/rdf/validators.py +++ b/hsmodels/schemas/rdf/validators.py @@ -79,8 +79,8 @@ def sort_creators(cls, creators): # assign creator_order to creators that don't have it creator_order_numbers = [c.creator_order for c in creators if c.creator_order is not None] if creator_order_numbers: - if len(creator_order_numbers) != len(set(creator_order_numbers)): - raise ValueError("creator_order values must be unique") + #if len(creator_order_numbers) != len(set(creator_order_numbers)): + # raise ValueError("creator_order values must be unique") max_order_number = max(creator_order_numbers) else: max_order_number = 0 diff --git a/hsmodels/utils.py b/hsmodels/utils.py index acc1855..b845873 100644 --- a/hsmodels/utils.py +++ b/hsmodels/utils.py @@ -3,11 +3,10 @@ def to_coverage_dict(value): value_dict = {} - if ";" in value: - for key_value in value.split("; "): - if "=" in key_value: - k, v = key_value.split("=") - value_dict[k] = v + for key_value in value.split(";"): + if "=" in key_value: + k, v = key_value.split("=") + value_dict[k.strip()] = v.strip() return value_dict From 043a594787dde31f108362b9a80182b5ad4478c7 Mon Sep 17 00:00:00 2001 From: sblack-usu Date: Wed, 7 Jan 2026 10:13:03 -0700 Subject: [PATCH 10/10] fix typo and restore EmailStr annotation --- hsmodels/schemas/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hsmodels/schemas/fields.py b/hsmodels/schemas/fields.py index 3c8012a..c67f5e6 100644 --- a/hsmodels/schemas/fields.py +++ b/hsmodels/schemas/fields.py @@ -205,7 +205,7 @@ class Contributor(BaseMetadata): title="Organization", description="A string containing the name of the organization with which the contributor is affiliated", ) - email: Optional[str] = Field( + email: Optional[EmailStr] = Field( default=None, title="Email", description="A string containing an email address for the contributor" ) homepage: Optional[HttpUrl] = Field( @@ -690,7 +690,7 @@ class BoxCoverage(base_models.BaseCoverage): ) northlimit: float = Field( gte=-90, - lt3=90, + lte=90, title="North limit", description="A floating point value containing the constant coordinate for the northernmost face or edge of the bounding box", )