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/fields.py b/hsmodels/schemas/fields.py index e4a97dc..98de23a 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( @@ -75,6 +74,7 @@ class Rights(BaseMetadata): default=None, title="URL", description="An object containing the URL pointing to a description of the license or rights statement", + default=None ) @classmethod @@ -277,7 +277,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, @@ -692,26 +692,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, + lte=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", ) @@ -728,6 +728,9 @@ class BoxCoverage(base_models.BaseCoverage): @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 @@ -827,10 +830,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" diff --git a/hsmodels/schemas/rdf/fields.py b/hsmodels/schemas/rdf/fields.py index f5d8de9..f85532e 100644 --- a/hsmodels/schemas/rdf/fields.py +++ b/hsmodels/schemas/rdf/fields.py @@ -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}) diff --git a/hsmodels/schemas/rdf/resource.py b/hsmodels/schemas/rdf/resource.py index b0917b9..b66d022 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) @@ -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/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/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/hsmodels/utils.py b/hsmodels/utils.py index 6e49e2c..b845873 100644 --- a/hsmodels/utils.py +++ b/hsmodels/utils.py @@ -3,9 +3,10 @@ def to_coverage_dict(value): value_dict = {} - for key_value in value.split("; "): - 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 diff --git a/setup.py b/setup.py index c4cc26f..103ecdf 100644 --- a/setup.py +++ b/setup.py @@ -4,12 +4,12 @@ setup( name='hsmodels', - version='1.0.5', + version='1.1.1', 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']),