|
1 | | -from datetime import datetime |
| 1 | +from datetime import timezone |
2 | 2 | from enum import Enum, auto |
3 | 3 | from typing import Any, Dict, List, Optional, Tuple, Union |
4 | 4 | from warnings import warn |
5 | 5 |
|
6 | | -from pydantic import BaseModel, ConfigDict, Field |
| 6 | +from pydantic import ( |
| 7 | + AfterValidator, |
| 8 | + AwareDatetime, |
| 9 | + BaseModel, |
| 10 | + ConfigDict, |
| 11 | + Field, |
| 12 | + model_validator, |
| 13 | +) |
| 14 | +from typing_extensions import Annotated, Self |
7 | 15 |
|
8 | 16 | from stac_pydantic.utils import AutoValueEnum |
9 | 17 |
|
|
15 | 23 |
|
16 | 24 | SEMVER_REGEX = r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" |
17 | 25 |
|
18 | | -# https://tools.ietf.org/html/rfc3339#section-5.6 |
19 | | -# Unused, but leaving it here since it's used by dependencies |
20 | | -DATETIME_RFC339 = "%Y-%m-%dT%H:%M:%SZ" |
| 26 | +# Allows for some additional flexibility in the input datetime format. As long as |
| 27 | +# the input value has timezone information, it will be converted to UTC timezone. |
| 28 | +UtcDatetime = Annotated[ |
| 29 | + # Input value must be in a format which has timezone information |
| 30 | + AwareDatetime, |
| 31 | + # Convert the input value to UTC timezone |
| 32 | + AfterValidator(lambda d: d.astimezone(timezone.utc)), |
| 33 | +] |
21 | 34 |
|
22 | 35 |
|
23 | 36 | class MimeTypes(str, Enum): |
@@ -106,41 +119,76 @@ class Provider(StacBaseModel): |
106 | 119 | https://github.com/radiantearth/stac-spec/blob/v1.0.0/collection-spec/collection-spec.md#provider-object |
107 | 120 | """ |
108 | 121 |
|
109 | | - name: str = Field(..., alias="name", min_length=1) |
| 122 | + name: str = Field(..., min_length=1) |
110 | 123 | description: Optional[str] = None |
111 | 124 | roles: Optional[List[str]] = None |
112 | 125 | url: Optional[str] = None |
113 | 126 |
|
114 | 127 |
|
115 | 128 | class StacCommonMetadata(StacBaseModel): |
116 | 129 | """ |
117 | | - https://github.com/radiantearth/stac-spec/blob/v1.0.0/item-spec/common-metadata.md#date-and-time-range |
| 130 | + https://github.com/radiantearth/stac-spec/blob/v1.0.0/item-spec/common-metadata.md |
118 | 131 | """ |
119 | 132 |
|
120 | | - title: Optional[str] = Field(None, alias="title") |
121 | | - description: Optional[str] = Field(None, alias="description") |
122 | | - start_datetime: Optional[datetime] = Field(None, alias="start_datetime") |
123 | | - end_datetime: Optional[datetime] = Field(None, alias="end_datetime") |
124 | | - created: Optional[datetime] = Field(None, alias="created") |
125 | | - updated: Optional[datetime] = Field(None, alias="updated") |
126 | | - platform: Optional[str] = Field(None, alias="platform") |
127 | | - instruments: Optional[List[str]] = Field(None, alias="instruments") |
128 | | - constellation: Optional[str] = Field(None, alias="constellation") |
129 | | - mission: Optional[str] = Field(None, alias="mission") |
130 | | - providers: Optional[List[Provider]] = Field(None, alias="providers") |
131 | | - gsd: Optional[float] = Field(None, alias="gsd", gt=0) |
| 133 | + # Basic |
| 134 | + title: Optional[str] = None |
| 135 | + description: Optional[str] = None |
| 136 | + # Date and Time |
| 137 | + datetime: Optional[UtcDatetime] = None |
| 138 | + created: Optional[UtcDatetime] = None |
| 139 | + updated: Optional[UtcDatetime] = None |
| 140 | + # Date and Time Range |
| 141 | + start_datetime: Optional[UtcDatetime] = None |
| 142 | + end_datetime: Optional[UtcDatetime] = None |
| 143 | + # Provider |
| 144 | + providers: Optional[List[Provider]] = None |
| 145 | + # Instrument |
| 146 | + platform: Optional[str] = None |
| 147 | + instruments: Optional[List[str]] = None |
| 148 | + constellation: Optional[str] = None |
| 149 | + mission: Optional[str] = None |
| 150 | + gsd: Optional[float] = Field(None, gt=0) |
| 151 | + |
| 152 | + @model_validator(mode="after") |
| 153 | + def validate_datetime_or_start_end(self) -> Self: |
| 154 | + # When datetime is null, start_datetime and end_datetime must be specified |
| 155 | + if not self.datetime and (not self.start_datetime or not self.end_datetime): |
| 156 | + raise ValueError( |
| 157 | + "start_datetime and end_datetime must be specified when datetime is null" |
| 158 | + ) |
| 159 | + |
| 160 | + return self |
| 161 | + |
| 162 | + @model_validator(mode="after") |
| 163 | + def validate_start_end(self) -> Self: |
| 164 | + # Using one of start_datetime or end_datetime requires the use of the other |
| 165 | + if (self.start_datetime and not self.end_datetime) or ( |
| 166 | + not self.start_datetime and self.end_datetime |
| 167 | + ): |
| 168 | + raise ValueError( |
| 169 | + "use of start_datetime or end_datetime requires the use of the other" |
| 170 | + ) |
| 171 | + return self |
132 | 172 |
|
133 | 173 |
|
134 | 174 | class Asset(StacCommonMetadata): |
135 | 175 | """ |
136 | 176 | https://github.com/radiantearth/stac-spec/blob/v1.0.0/item-spec/item-spec.md#asset-object |
137 | 177 | """ |
138 | 178 |
|
139 | | - href: str = Field(..., alias="href", min_length=1) |
| 179 | + href: str = Field(..., min_length=1) |
140 | 180 | type: Optional[str] = None |
141 | 181 | title: Optional[str] = None |
142 | 182 | description: Optional[str] = None |
143 | 183 | roles: Optional[List[str]] = None |
| 184 | + |
144 | 185 | model_config = ConfigDict( |
145 | 186 | populate_by_name=True, use_enum_values=True, extra="allow" |
146 | 187 | ) |
| 188 | + |
| 189 | + @model_validator(mode="after") |
| 190 | + def validate_datetime_or_start_end(self) -> Self: |
| 191 | + # Overriding the parent method to avoid requiring datetime or start/end_datetime |
| 192 | + # Additional fields MAY be added on the Asset object, but are not required. |
| 193 | + # https://github.com/radiantearth/stac-spec/blob/v1.0.0/item-spec/item-spec.md#additional-fields-for-assets |
| 194 | + return self |
0 commit comments