diff --git a/hsmodels/schemas/aggregations.py b/hsmodels/schemas/aggregations.py index cbfe9d0..26fda73 100644 --- a/hsmodels/schemas/aggregations.py +++ b/hsmodels/schemas/aggregations.py @@ -1,5 +1,5 @@ from datetime import date -from typing import Dict, List, Union +from typing import Dict, List, Optional, Union from pydantic import AnyUrl, ConfigDict, Field, GetJsonSchemaHandler, model_validator, field_validator from pydantic.json_schema import JsonSchemaValue @@ -61,12 +61,12 @@ class BaseAggregationMetadataIn(BaseMetadata): title="Additional metadata", description="A dictionary of additional metadata elements expressed as key-value pairs", ) - spatial_coverage: Union[PointCoverage, BoxCoverage] = Field( + spatial_coverage: Optional[Union[PointCoverage, BoxCoverage]] = Field( default=None, title="Spatial coverage", description="An object containing the geospatial coverage for the aggregation expressed as either a bounding box or point", ) - period_coverage: PeriodCoverage = Field( + period_coverage: Optional[PeriodCoverage] = Field( default=None, title="Temporal coverage", description="An object containing the temporal coverage for a aggregation expressed as a date range", @@ -116,6 +116,11 @@ class GeographicRasterMetadataIn(BaseAggregationMetadataIn): title="Band information", description="An object containing information about the bands contained in the raster dataset", ) + spatial_coverage: Union[PointCoverage, BoxCoverage] = Field( + default=None, + title="Spatial coverage", + description="An object containing the geospatial coverage for the aggregation expressed as either a bounding box or point", + ) spatial_reference: Union[BoxSpatialReference, PointSpatialReference] = Field( default=None, title="Spatial reference", @@ -142,7 +147,7 @@ class GeographicRasterMetadata(GeographicRasterMetadataIn): json_schema_extra={"readOnly": True}, ) - rights: Rights = Field( + rights: Optional[Rights] = Field( default=None, title="Rights statement", description="An object containing information about the rights held in and over the aggregation and the license under which a aggregation is shared", @@ -171,6 +176,11 @@ class GeographicFeatureMetadataIn(BaseAggregationMetadataIn): title="Geometry information", description="An object containing information about the geometry of the features in the dataset", ) + spatial_coverage: Union[PointCoverage, BoxCoverage] = Field( + default=None, + title="Spatial coverage", + description="An object containing the geospatial coverage for the aggregation expressed as either a bounding box or point", + ) spatial_reference: Union[BoxSpatialReference, PointSpatialReference] = Field( default=None, title="Spatial reference", @@ -194,7 +204,7 @@ class GeographicFeatureMetadata(GeographicFeatureMetadataIn): json_schema_extra={"readOnly": True}, ) - rights: Rights = Field( + rights: Optional[Rights] = Field( default=None, title="Rights statement", description="An object containing information about the rights held in and over the aggregation and the license under which a aggregation is shared", @@ -218,12 +228,21 @@ class MultidimensionalMetadataIn(BaseAggregationMetadataIn): title="Variables", description="A list containing information about the variables for which data are stored in the dataset", ) + spatial_coverage: Union[PointCoverage, BoxCoverage] = Field( + default=None, + title="Spatial coverage", + description="An object containing the geospatial coverage for the aggregation expressed as either a bounding box or point", + ) spatial_reference: Union[MultidimensionalBoxSpatialReference, MultidimensionalPointSpatialReference] = Field( default=None, title="Spatial reference", description="An object containing spatial reference information for the dataset", ) - + period_coverage: PeriodCoverage = Field( + default=None, + title="Temporal coverage", + description="An object containing the temporal coverage for a aggregation expressed as a date range", + ) _parse_spatial_reference = field_validator("spatial_reference", mode='before')( parse_multidimensional_spatial_reference ) @@ -243,7 +262,7 @@ class MultidimensionalMetadata(MultidimensionalMetadataIn): json_schema_extra={"readOnly": True}, ) - rights: Rights = Field( + rights: Optional[Rights] = Field( default=None, title="Rights statement", description="An object containing information about the rights held in and over the aggregation and the license under which a aggregation is shared", @@ -278,7 +297,7 @@ class ReferencedTimeSeriesMetadata(ReferencedTimeSeriesMetadataIn): json_schema_extra={"readOnly": True}, ) - rights: Rights = Field( + rights: Optional[Rights] = Field( default=None, title="Rights statement", description="An object containing information about the rights held in and over the aggregation and the license under which a aggregation is shared", @@ -313,7 +332,7 @@ class FileSetMetadata(FileSetMetadataIn): json_schema_extra={"readOnly": True}, ) - rights: Rights = Field( + rights: Optional[Rights] = Field( default=None, title="Rights statement", description="An object containing information about the rights held in and over the aggregation and the license under which a aggregation is shared", @@ -347,7 +366,7 @@ class SingleFileMetadata(SingleFileMetadataIn): json_schema_extra={"readOnly": True}, ) - rights: Rights = Field( + rights: Optional[Rights] = Field( default=None, title="Rights statement", description="An object containing information about the rights held in and over the aggregation and the license under which a aggregation is shared", @@ -375,7 +394,17 @@ class TimeSeriesMetadataIn(BaseAggregationMetadataIn): description="A list of time series results contained within the time series aggregation", ) - abstract: str = Field(default=None, title="Abstract", description="A string containing a summary of a aggregation") + abstract: Optional[str] = Field(default=None, title="Abstract", description="A string containing a summary of a aggregation") + spatial_coverage: Union[PointCoverage, BoxCoverage] = Field( + default=None, + title="Spatial coverage", + description="An object containing the geospatial coverage for the aggregation expressed as either a bounding box or point", + ) + period_coverage: PeriodCoverage = Field( + default=None, + title="Temporal coverage", + description="An object containing the temporal coverage for a aggregation expressed as a date range", + ) _parse_abstract = model_validator(mode='before')(parse_abstract) @@ -394,7 +423,7 @@ class TimeSeriesMetadata(TimeSeriesMetadataIn): json_schema_extra={"readOnly": True}, ) - rights: Rights = Field( + rights: Optional[Rights] = Field( default=None, title="Rights statement", description="An object containing information about the rights held in and over the aggregation and the license under which a aggregation is shared", @@ -410,7 +439,7 @@ class ModelProgramMetadataIn(BaseAggregationMetadataIn): model_config = ConfigDict(title="Model Program Aggregation Metadata") - version: str = Field( + version: Optional[str] = Field( default=None, title="Version", description="The software version or build number of the model", max_length=255 ) @@ -428,17 +457,17 @@ class ModelProgramMetadataIn(BaseAggregationMetadataIn): description="Compatible operating systems to setup and run the model", ) - release_date: date = Field( + release_date: Optional[date] = Field( default=None, title="Release Date", description="The date that this version of the model was released" ) - website: AnyUrl = Field( + website: Optional[AnyUrl] = Field( default=None, title='Website', description='A URL to a website describing the model that is maintained by the model developers', ) - code_repository: AnyUrl = Field( + code_repository: Optional[AnyUrl] = Field( default=None, title='Software Repository', description='A URL to the source code repository for the model code (e.g., git, mercurial, svn, etc.)', @@ -448,7 +477,7 @@ class ModelProgramMetadataIn(BaseAggregationMetadataIn): default=[], title='File Types', description='File types used by the model program' ) - program_schema_json: AnyUrl = Field( + program_schema_json: Optional[AnyUrl] = Field( default=None, title='Model program schema', description='A url to the JSON metadata schema for the model program', @@ -471,7 +500,7 @@ class ModelProgramMetadata(ModelProgramMetadataIn): json_schema_extra={"readOnly": True}, ) - rights: Rights = Field( + rights: Optional[Rights] = Field( default=None, title="Rights statement", description="An object containing information about the rights held in and over the aggregation and the license under which a aggregation is shared", @@ -492,19 +521,19 @@ class ModelInstanceMetadataIn(BaseAggregationMetadataIn): description="Indicates whether model output files are included in the aggregation", ) - executed_by: AnyUrl = Field( + executed_by: Optional[AnyUrl] = Field( default=None, title="Executed By", description="A URL to the Model Program that can be used to execute this model instance", ) - program_schema_json: AnyUrl = Field( + program_schema_json: Optional[AnyUrl] = Field( default=None, title="JSON Metadata schema URL", description="A URL to the JSON metadata schema for the related model program", ) - program_schema_json_values: AnyUrl = Field( + program_schema_json_values: Optional[AnyUrl] = Field( default=None, title="JSON metadata schema values URL", description="A URL to a JSON file containing the metadata values conforming to the JSON metadata schema for the related model program", @@ -525,7 +554,7 @@ class ModelInstanceMetadata(ModelInstanceMetadataIn): json_schema_extra={"readOnly": True}, ) - rights: Rights = Field( + rights: Optional[Rights] = Field( default=None, title="Rights statement", description="An object containing information about the rights held in and over the aggregation and the license under which a aggregation is shared", diff --git a/hsmodels/schemas/base_models.py b/hsmodels/schemas/base_models.py index 58bff3d..4018dde 100644 --- a/hsmodels/schemas/base_models.py +++ b/hsmodels/schemas/base_models.py @@ -1,6 +1,7 @@ from datetime import datetime -from typing import Any, Dict, Union +from typing import Any, Literal, Union +import typing_extensions from pydantic import BaseModel, ConfigDict @@ -8,14 +9,17 @@ class BaseMetadata(BaseModel): def model_dump( self, *, - include: Union['AbstractSetIntStr', 'MappingIntStrAny'] = None, - exclude: Union['AbstractSetIntStr', 'MappingIntStrAny'] = None, + mode: Union[typing_extensions.Literal['json', 'python'], str] = 'python', + include: 'IncEx' = None, + exclude: 'IncEx' = None, + context: Union[dict[str, Any], None] = None, by_alias: bool = False, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = True, round_trip: bool = False, - warnings: bool = False, + warnings: Union[bool, Literal['none', 'warn', 'error']] = False, + serialize_as_any: bool = False, to_rdf: bool = False, ) -> dict[str, Any]: """ @@ -28,14 +32,17 @@ def model_dump( Override the default of exclude_none to True """ d = super().model_dump( + mode=mode, include=include, exclude=exclude, + context=context, by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, round_trip=round_trip, warnings=warnings, + serialize_as_any=serialize_as_any, ) if to_rdf and "additional_metadata" in d: additional_metadata = d["additional_metadata"] @@ -47,14 +54,16 @@ def model_dump_json( self, *, indent: Union[int, None] = None, - include: Union['AbstractSetIntStr', 'MappingIntStrAny'] = None, - exclude: Union['AbstractSetIntStr', 'MappingIntStrAny'] = None, + include: 'IncEx' = None, + exclude: 'IncEx' = None, + context: Union[dict[str, Any], None] = None, by_alias: bool = False, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = True, round_trip: bool = False, - warnings: bool = False, + warnings: Union[bool, Literal['none', 'warn', 'error']] = False, + serialize_as_any: bool = False, ) -> str: """ Generate a JSON representation of the model, `include` and `exclude` arguments as per `dict()`. @@ -67,12 +76,14 @@ def model_dump_json( indent=indent, include=include, exclude=exclude, + context=context, by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, round_trip=round_trip, warnings=warnings, + serialize_as_any=serialize_as_any, ) model_config = ConfigDict(validate_assignment=True) diff --git a/hsmodels/schemas/fields.py b/hsmodels/schemas/fields.py index b7731a3..cd1230e 100644 --- a/hsmodels/schemas/fields.py +++ b/hsmodels/schemas/fields.py @@ -130,22 +130,22 @@ class Creator(BaseMetadata): name: str = Field( default=None, max_length=100, title="Name", description="A string containing the name of the creator" ) - phone: str = Field( + phone: Optional[str] = Field( default=None, max_length=25, title="Phone", description="A string containing a phone number for the creator" ) - address: str = Field( + address: Optional[str] = Field( default=None, max_length=250, title="Address", description="A string containing an address for the creator" ) - organization: str = Field( + organization: Optional[str] = Field( default=None, max_length=200, title="Organization", description="A string containing the name of the organization with which the creator is affiliated", ) - email: EmailStr = Field( + email: Optional[EmailStr] = Field( default=None, title="Email", description="A string containing an email address for the creator" ) - homepage: HttpUrl = Field( + homepage: Optional[HttpUrl] = Field( default=None, title="Homepage", description="An object containing the URL for website associated with the creator", @@ -156,7 +156,7 @@ class Creator(BaseMetadata): description="An integer to order creators", frozen=True, ) - hydroshare_user_id: int = Field( + hydroshare_user_id: Optional[int] = Field( default=None, title="Hydroshare user id", description="An integer containing the Hydroshare user ID", @@ -191,26 +191,26 @@ class Contributor(BaseMetadata): model_config = ConfigDict(title='Contributor Metadata') name: str = Field(default=None, title="Name", description="A string containing the name of the contributor") - phone: str = Field( + phone: Optional[str] = Field( default=None, title="Phone", description="A string containing a phone number for the contributor" ) - address: str = Field( + address: Optional[str] = Field( default=None, title="Address", description="A string containing an address for the contributor" ) - organization: str = Field( + organization: Optional[str] = Field( default=None, title="Organization", description="A string containing the name of the organization with which the contributor is affiliated", ) - email: EmailStr = Field( + email: Optional[EmailStr] = Field( default=None, title="Email", description="A string containing an email address for the contributor" ) - homepage: HttpUrl = Field( + homepage: Optional[HttpUrl] = Field( default=None, title="Homepage", description="An object containing the URL for website associated with the contributor", ) - hydroshare_user_id: int = Field( + hydroshare_user_id: Optional[int] = Field( default=None, title="Hyroshare user id", description="An integer containing the Hydroshare user ID", @@ -251,13 +251,13 @@ class AwardInfo(BaseMetadata): funding_agency_name: str = Field( title="Agency name", description="A string containing the name of the funding agency or organization", ) - title: str = Field( + title: Optional[str] = Field( default=None, title="Award title", description="A string containing the title of the project or award", ) - number: str = Field( + number: Optional[str] = Field( default=None, title="Award number", description="A string containing the award number or other identifier", ) - funding_agency_url: AnyUrl = Field( + funding_agency_url: Optional[AnyUrl] = Field( default=None, title="Agency URL", description="An object containing a URL pointing to a website describing the funding award", @@ -273,37 +273,37 @@ class BandInformation(BaseMetadata): name: str = Field(max_length=500, title="Name", description="A string containing the name of the raster band", ) - variable_name: str = Field( + variable_name: Optional[str] = Field( default=None, max_length=100, title="Variable name", description="A string containing the name of the variable represented by the raster band", ) - variable_unit: str = Field( + variable_unit: Optional[str] = Field( default=None, max_length=50, title="Variable unit", description="A string containing the units for the raster band variable", ) - no_data_value: str = Field( + no_data_value: Optional[str] = Field( default=None, title="Nodata value", description="A string containing the numeric nodata value for the raster band", ) - maximum_value: str = Field( + maximum_value: Optional[str] = Field( default=None, title="Maximum value", description="A string containing the maximum numeric value for the raster band", ) - comment: str = Field( + comment: Optional[str] = Field( default=None, title="Comment", description="A string containing a comment about the raster band", ) - method: str = Field( + method: Optional[str] = Field( default=None, title="Method", description="A string containing a description of the method used to create the raster band data", ) - minimum_value: str = Field( + minimum_value: Optional[str] = Field( default=None, title="Minimum value", description="A string containing the minimum numerica value for the raster dataset", @@ -326,16 +326,16 @@ class FieldInformation(BaseMetadata): ) # TODO: What is the "field_type_code"? It's not displayed on the resource landing page, but it's encoded in the # aggregation metadata as an integer value. - field_type_code: str = Field( + field_type_code: Optional[str] = Field( default=None, max_length=50, title="Field type code", description="A string value containing a code that indicates the field type", ) - field_width: int = Field( + field_width: Optional[int] = Field( default=None, title="Field width", description="An integer value containing the width of the attribute field", ) - field_precision: int = Field( + field_precision: Optional[int] = Field( default=None, title="Field precision", description="An integer value containing the precision of the attribute field", @@ -383,18 +383,18 @@ class Variable(BaseMetadata): title="Shape", description="A string containing the shape of the variable expressed as a list of dimensions", ) - descriptive_name: str = Field( + descriptive_name: Optional[str] = Field( default=None, max_length=1000, title="Descriptive name", description="A string containing a descriptive name for the variable", ) - method: str = Field( + method: Optional[str] = Field( default=None, title="Method", description="A string containing a description of the method used to create the values for the variable", ) - missing_value: str = Field( + missing_value: Optional[str] = Field( default=None, max_length=1000, title="Missing value", @@ -442,13 +442,13 @@ class TimeSeriesVariable(BaseMetadata): # It is an integer in the HydroShare database, so will have to be updated there as well if changed no_data_value: int = Field(title="NoData value", description="The NoData value for the variable", ) - variable_definition: str = Field( + variable_definition: Optional[str] = Field( default=None, max_length=255, title="Variable definition", description="A string containing a detailed description of the variable", ) - speciation: str = Field( + speciation: Optional[str] = Field( default=None, max_length=255, title="Speciation", @@ -468,32 +468,32 @@ class TimeSeriesSite(BaseMetadata): title="Site code", description="A string containing a short but meaningful code identifying the site", ) - site_name: str = Field( + site_name: Optional[str] = Field( default=None, max_length=255, title="Site name", description="A string containing the name of the site", ) - elevation_m: float = Field( + elevation_m: Optional[float] = Field( default=None, title="Elevation", description="A floating point number expressing the elevation of the site in meters", ) - elevation_datum: str = Field( + elevation_datum: Optional[str] = Field( default=None, max_length=50, title="Elevation datum", description="A string expressing the elevation datum used from the ODM2 Elevation Datum controlled vocabulary", ) - site_type: str = Field( + site_type: Optional[str] = Field( default=None, max_length=100, title="Site type", description="A string containing the type of site from the ODM2 Sampling Feature Type controlled vocabulary ", ) - latitude: float = Field( + latitude: Optional[float] = Field( default=None, title="Latitude", description="A floating point value expressing the latitude coordinate of the site", ) - longitude: float = Field( + longitude: Optional[float] = Field( default=None, title="Longitude", description="A floating point value expressing the longitude coordinate of the site", @@ -520,11 +520,11 @@ class TimeSeriesMethod(BaseMetadata): title="Method type", description="A string containing the method type from the ODM2 Method Type controlled vocabulary", ) - method_description: str = Field( + method_description: Optional[str] = Field( default=None, title="Method description", description="A string containing a detailed description of the method", ) - method_link: AnyUrl = Field( + method_link: Optional[AnyUrl] = Field( default=None, title="Method link", description="An object containing a URL that points to a website having a detailed description of the method", @@ -544,13 +544,13 @@ class ProcessingLevel(BaseMetadata): title="Processing level code", description="A string containing a short but meaningful code identifying the processing level", ) - definition: str = Field( + definition: Optional[str] = Field( default=None, max_length=200, title="Definition", description="A string containing a description of the processing level", ) - explanation: str = Field( + explanation: Optional[str] = Field( default=None, title="Explanation", description="A string containing a more extensive explanation of the meaning of the processing level", @@ -612,7 +612,7 @@ class TimeSeriesResult(BaseMetadata): title="Units", description="An object containing the units in which the values of the time series are expressed", ) - status: str = Field( + status: Optional[str] = Field( default=None, max_length=255, title="Status", @@ -646,7 +646,7 @@ class TimeSeriesResult(BaseMetadata): description="An object containing metadata about the site at which the time series result was created", ) variable: TimeSeriesVariable = Field( - title="Variablef", + title="Variable", description="An object containing metadata about the observed variable associated with the time series result values", ) method: TimeSeriesMethod = Field( @@ -657,7 +657,7 @@ class TimeSeriesResult(BaseMetadata): title="Processing level", description="An object containing metadata about the processing level or level of quality control to which the time series result values have been subjected", ) - utc_offset: float = Field( + utc_offset: Optional[float] = Field( default=None, title="UTC Offset", description="A floating point value that represents the time offset from UTC time in hours associated with the time series result value timestamps", diff --git a/hsmodels/schemas/resource.py b/hsmodels/schemas/resource.py index 852684f..9538b56 100644 --- a/hsmodels/schemas/resource.py +++ b/hsmodels/schemas/resource.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Dict, List, Union, Literal +from typing import Dict, List, Union, Literal, Optional from pydantic import AnyUrl, ConfigDict, Field, GetJsonSchemaHandler, field_validator, model_validator from pydantic.json_schema import JsonSchemaValue @@ -78,12 +78,12 @@ class ResourceMetadataIn(BaseMetadata): title="Funding agency information", description="A list of objects containing information about the funding agencies and awards associated with a resource", ) - spatial_coverage: Union[PointCoverage, BoxCoverage] = Field( + spatial_coverage: Optional[Union[PointCoverage, BoxCoverage]] = Field( default=None, title="Spatial coverage", description="An object containing information about the spatial topic of a resource, the spatial applicability of a resource, or jurisdiction under with a resource is relevant", ) - period_coverage: PeriodCoverage = Field( + period_coverage: Optional[PeriodCoverage] = Field( default=None, title="Temporal coverage", description="An object containing information about the temporal topic or applicability of a resource", diff --git a/requirements.txt b/requirements.txt index cdf0952..b01dd2e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ rdflib<6.0.0 -pydantic==2.* +pydantic==2.7.* email-validator jsonschema2md black diff --git a/setup.py b/setup.py index e588049..42b28de 100644 --- a/setup.py +++ b/setup.py @@ -4,12 +4,12 @@ setup( name='hsmodels', - version='1.0.0', + version='1.0.1', packages=find_packages(include=['hsmodels', 'hsmodels.*', 'hsmodels.schemas.*', 'hsmodels.schemas.rdf.*'], exclude=("tests",)), install_requires=[ 'rdflib<6.0.0', - 'pydantic==2.*', + 'pydantic==2.7.*', 'email-validator' ], url='https://github.com/hydroshare/hsmodels', diff --git a/tests/test_metadata.py b/tests/test_metadata.py index a35b47a..9ae85ed 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -86,6 +86,9 @@ def test_resource_metadata(res_md): assert "key2" in res_md.additional_metadata assert res_md.additional_metadata["key2"] == "value2" + # test additional_metadata can be optional + res_md.additional_metadata = {} + assert len(res_md.creators) == 3 for cr in res_md.creators: assert cr.creator_order in (1, 2, 3) @@ -119,12 +122,18 @@ def test_resource_metadata(res_md): assert str(contributor.identifiers[UserIdentifierType.ORCID]) == "https://orcid.org/0000-0002-1998-3479" assert contributor.name == "David Tarboton" + # test contributor can be optional + res_md.contributors = [] + assert len(res_md.relations) == 3 assert any(x for x in res_md.relations if x.value == "https://sadf.com" and x.type == RelationType.isPartOf) assert any( x for x in res_md.relations if x.value == "https://www.google.com/" and x.type == RelationType.isCreatedBy ) + # test relation can be optional + res_md.relations = [] + assert res_md.rights.statement == "my statement" assert str(res_md.rights.url) == "http://studio.bakajo.com/" @@ -139,10 +148,14 @@ def test_resource_metadata(res_md): assert award.number == "n" assert award.funding_agency_name == "agency1" assert str(award.funding_agency_url) == "https://google.com/" + # test awards can be optional + res_md.awards = [] assert res_md.period_coverage == PeriodCoverage( start=datetime.fromisoformat("2020-07-10T00:00:00"), end=datetime.fromisoformat("2020-07-29T00:00:00") ) + # test period_coverage can be optional + res_md.period_coverage = None assert res_md.spatial_coverage == BoxCoverage( name="asdfsadf", @@ -165,6 +178,8 @@ def test_resource_metadata(res_md): ) res_md.spatial_coverage = pc + # test spatial_coverage can be optional + res_md.spatial_coverage = None assert res_md.publisher assert ( diff --git a/tests/test_metadata_json.py b/tests/test_metadata_json.py index 7409b43..31d88a3 100644 --- a/tests/test_metadata_json.py +++ b/tests/test_metadata_json.py @@ -66,3 +66,127 @@ def test_metadata_json_serialization(metadata_json_input): from_file = sorting(json.loads(json_file_str)) for i in range(1, len(from_file)): assert from_file[i] == from_schema[i] + + +def test_optional_fields_resource(): + with open("data/json/resource.json", 'r') as f: + md = ResourceMetadataIn(**json.loads(f.read())) + # test period_coverage is optional + md.period_coverage = None + # test spatial_coverage is optional + md.spatial_coverage = None + # test contributor is optional + md.contributors = [] + # test relation is optional + md.relations = [] + # test additional_metadata is optional + md.additional_metadata = {} + # test awards is optional + md.awards = [] + # test subjects is optional + md.subjects = [] + + +def test_optional_fields_raster_aggr(): + with open("data/json/geographicraster.json", 'r') as f: + md = GeographicRasterMetadataIn(**json.loads(f.read())) + # test period_coverage is optional + md.period_coverage = None + # test some of the fields of the band_information are optional + md.band_information.variable_name = None + md.band_information.variable_unit = None + md.band_information.maximum_value = None + md.band_information.minimum_value = None + md.band_information.no_data_value = None + md.band_information.comment = None + md.band_information.method = None + + +def test_optional_fields_geo_feature_aggr(): + with open("data/json/geographicfeature.json", 'r') as f: + md = GeographicFeatureMetadataIn(**json.loads(f.read())) + # test period_coverage is optional + md.period_coverage = None + + # test some of the fields of the field_information are optional + md.field_information[0].field_type_code = None + md.field_information[0].field_width = None + md.field_information[0].field_precision = None + + +def test_optional_fields_multidimensional_aggr(): + with open("data/json/multidimensional.json", 'r') as f: + md = MultidimensionalMetadataIn(**json.loads(f.read())) + # test variable optional attributes + md.variables[0].method = None + md.variables[0].descriptive_name = None + md.variables[0].missing_value = None + + +def test_optional_fields_fileset_aggr(): + with open("data/json/fileset.json", 'r') as f: + md = FileSetMetadataIn(**json.loads(f.read())) + # test spatial_coverage is optional + md.spatial_coverage = None + # test period_coverage is optional + md.period_coverage = None + + +def test_optional_fields_singlefile_aggr(): + with open("data/json/singlefile.json", 'r') as f: + md = SingleFileMetadataIn(**json.loads(f.read())) + # test spatial_coverage is optional + md.spatial_coverage = None + # test period_coverage is optional + md.period_coverage = None + + +def test_optional_fields_timeseries_aggr(): + with open("data/json/timeseries.json", 'r') as f: + md = TimeSeriesMetadataIn(**json.loads(f.read())) + + # test some of the fields of the variable are optional + md.time_series_results[0].variable.variable_definition = None + md.time_series_results[0].variable.speciation = None + + # test some of the fields of the site are optional + md.time_series_results[0].site.site_name = None + md.time_series_results[0].site.elevation_m = None + md.time_series_results[0].site.elevation_datum = None + md.time_series_results[0].site.site_type = None + md.time_series_results[0].site.latitude = None + md.time_series_results[0].site.longitude = None + + # test some of the fields of the method are optional + md.time_series_results[0].method.method_description = None + md.time_series_results[0].method.method_link = None + + # test some of the fields of the processing_level are optional + md.time_series_results[0].processing_level.definition = None + md.time_series_results[0].processing_level.explanation = None + + +def test_optional_fields_modelprogram_aggr(): + with open("data/json/modelprogram.json", 'r') as f: + md = ModelProgramMetadataIn(**json.loads(f.read())) + # test version is optional + md.version = None + # test release_date is optional + md.release_date = None + # test website is optional + md.website = None + # test code_repository is optional + md.code_repository = None + # test program_schema_json is optional + md.program_schema_json = None + + +def test_optional_fields_modelinstance_aggr(): + with open("data/json/modelinstance.json", 'r') as f: + md = ModelInstanceMetadataIn(**json.loads(f.read())) + # test executed_by is optional + md.executed_by = None + # test program_schema_json is optional + md.program_schema_json = None + # test program_schema_json_values is optional + md.program_schema_json_values = None diff --git a/tests/test_validation.py b/tests/test_validation.py index 6e361b1..d5fc4f7 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -327,7 +327,7 @@ def test_aggregation_metadata_from_form(): "units": "Decimal degrees", "projection": "WGS 84 EPSG:4326", }, - # "period_coverage": None, + "period_coverage": None, "rights": { "statement": "This resource is shared under the Creative Commons Attribution CC BY.", "url": "http://creativecommons.org/licenses/by/4.0/", @@ -335,12 +335,12 @@ def test_aggregation_metadata_from_form(): "type": "GeoRaster", "band_information": { "name": "Band_1", - # "variable_name": None, - # "variable_unit": None, + "variable_name": None, + "variable_unit": None, "no_data_value": "-3.40282346639e+38", "maximum_value": "2880.00708008", - # "comment": None, - # "method": None, + "comment": None, + "method": None, "minimum_value": "2274.95898438", }, "spatial_reference": { @@ -384,13 +384,6 @@ def test_subjects_aggregation(agg_md): def test_default_exclude_none(res_md): - # TODO: we can't do the following assignment unless we change the schema for spatial_coverage - # to Optional[Union[PointCoverage, BoxCoverage]] - # res_md.spatial_coverage = None - assert "spatial_coverage" in res_md.model_dump() - model_data = res_md.model_dump() - - # remove spatial_coverage from input data - model_data.pop("spatial_coverage") - res_md = type(res_md)(**model_data) + res_md.spatial_coverage = None + assert "spatial_coverage" not in res_md.model_dump() assert "spatial_coverage" in res_md.model_dump(exclude_none=False)