diff --git a/terracotta/config.py b/terracotta/config.py index ec23bf2c..fedd7080 100644 --- a/terracotta/config.py +++ b/terracotta/config.py @@ -116,8 +116,9 @@ class TerracottaSettings(NamedTuple): } -def _is_writable(path: str) -> bool: - return os.access(os.path.dirname(path) or os.getcwd(), os.W_OK) +def _is_writable(path: str) -> None: + if not os.access(os.path.dirname(path) or os.getcwd(), os.W_OK): + raise ValidationError(f"Path {path} is not writable.") class SettingSchema(Schema): diff --git a/terracotta/drivers/geotiff_raster_store.py b/terracotta/drivers/geotiff_raster_store.py index 3e5ac6cc..1031438f 100644 --- a/terracotta/drivers/geotiff_raster_store.py +++ b/terracotta/drivers/geotiff_raster_store.py @@ -58,7 +58,7 @@ def submit_to_executor(task: Callable[..., Any]) -> Future: future = _executor.submit(task) except BrokenProcessPool: # re-create executor and try again - logger.warn("Re-creating broken process pool") + logger.warning("Re-creating broken process pool") _executor = create_executor() future = _executor.submit(task) diff --git a/terracotta/expressions.py b/terracotta/expressions.py index 5d93b5a5..085398dc 100644 --- a/terracotta/expressions.py +++ b/terracotta/expressions.py @@ -125,8 +125,11 @@ def visit_Call(self, node: ast.Call) -> Any: ) return func(*map(self.visit, node.args)) - def visit_Num(self, node: ast.Num) -> Any: - return node.n + def visit_Constant(self, node: ast.Constant) -> Any: + if isinstance(node.value, (int, float)): + return node.value + else: + self.generic_visit(node) def visit_UnaryOp(self, node: ast.UnaryOp) -> Any: op_type = type(node.op) diff --git a/terracotta/server/colormap.py b/terracotta/server/colormap.py index fc7858d8..2c91992f 100644 --- a/terracotta/server/colormap.py +++ b/terracotta/server/colormap.py @@ -14,10 +14,8 @@ class ColormapEntrySchema(Schema): - value = fields.Number(required=True) - rgba = fields.List( - fields.Number(), required=True, validate=validate.Length(equal=4) - ) + value = fields.Float(required=True) + rgba = fields.List(fields.Float(), required=True, validate=validate.Length(equal=4)) class ColormapSchema(Schema): @@ -29,19 +27,29 @@ class Meta: unknown = EXCLUDE stretch_range = fields.List( - fields.Number(), + fields.Float(), validate=validate.Length(equal=2), required=True, - description="Minimum and maximum value of colormap as JSON array " - "(same as for /singleband and /rgb)", + metadata={ + "description": ( + "Minimum and maximum value of colormap as JSON array " + "(same as for /singleband and /rgb)", + ), + }, ) colormap = fields.String( - description="Name of color map to use (for a preview see " - "https://terracotta-python.readthedocs.io/en/latest/reference/colormaps.html)", - missing=None, + metadata={ + "description": ( + "Name of color map to use (for a preview see " + "https://terracotta-python.readthedocs.io/en/latest/reference/colormaps.html)", + ), + }, + load_default=None, validate=validate.OneOf(AVAILABLE_CMAPS), ) - num_values = fields.Int(description="Number of values to return", missing=255) + num_values = fields.Int( + metadata={"description": "Number of values to return"}, load_default=255 + ) @pre_load def process_ranges(self, data: Mapping[str, Any], **kwargs: Any) -> Dict[str, Any]: diff --git a/terracotta/server/compute.py b/terracotta/server/compute.py index e0dbe9cd..938ea7dd 100644 --- a/terracotta/server/compute.py +++ b/terracotta/server/compute.py @@ -15,15 +15,17 @@ class ComputeQuerySchema(Schema): keys = fields.String( - required=True, description="Keys identifying dataset, in order" + required=True, metadata={"description": "Keys identifying dataset, in order"} ) - tile_z = fields.Int(required=True, description="Requested zoom level") - tile_y = fields.Int(required=True, description="y coordinate") - tile_x = fields.Int(required=True, description="x coordinate") + tile_z = fields.Int(required=True, metadata={"description": "Requested zoom level"}) + tile_y = fields.Int(required=True, metadata={"description": "y coordinate"}) + tile_x = fields.Int(required=True, metadata={"description": "x coordinate"}) def _operator_field(i: int) -> fields.String: - return fields.String(description=f"Last key of variable v{i} in given expression.") + return fields.String( + metadata={"description": f"Last key of variable v{i} in given expression."} + ) class ComputeOptionSchema(Schema): @@ -31,30 +33,36 @@ class Meta: unknown = EXCLUDE expression = fields.String( - description="Mathematical expression to execute.", - example="(v1 - v2) / (v1 + v2)", + metadata={ + "description": "Mathematical expression to execute.", + "example": "(v1 - v2) / (v1 + v2)", + }, required=True, ) stretch_range = fields.List( - fields.Number(allow_none=True), + fields.Float(allow_none=True), validate=validate.Length(equal=2), - example="[0,1]", - description="Stretch range to use as JSON array.", + metadata={ + "description": "Stretch range to use as JSON array.", + "example": "[0,1]", + }, required=True, ) colormap = fields.String( - description="Colormap to apply to image (see /colormap).", + metadata={"description": "Colormap to apply to image (see /colormap)."}, validate=validate.OneOf(("explicit", *AVAILABLE_CMAPS)), - missing=None, + load_default=None, ) tile_size = fields.List( fields.Integer(), validate=validate.Length(equal=2), - example="[256,256]", - description="Pixel dimensions of the returned PNG image as JSON list.", + metadata={ + "description": "Pixel dimensions of the returned PNG image as JSON list.", + "example": "[256,256]", + }, ) v1 = _operator_field(1) @@ -111,7 +119,7 @@ def get_compute(tile_z: int, tile_y: int, tile_x: int, keys: str = "") -> Respon class ComputePreviewSchema(Schema): keys = fields.String( - required=True, description="Keys identifying dataset, in order" + required=True, metadata={"description": "Keys identifying dataset, in order"} ) diff --git a/terracotta/server/datasets.py b/terracotta/server/datasets.py index 4d8dffbf..8ea02759 100644 --- a/terracotta/server/datasets.py +++ b/terracotta/server/datasets.py @@ -16,17 +16,25 @@ class Meta: unknown = INCLUDE # placeholder values to document keys - key1 = fields.String(example="value1", description="Value of key1", dump_only=True) - key2 = fields.String(example="value2", description="Value of key2", dump_only=True) + key1 = fields.String( + metadata={"description": "Value of key1", "example": "value1"}, + dump_only=True, + ) + key2 = fields.String( + metadata={"description": "Value of key2", "example": "value2"}, + dump_only=True, + ) # real options limit = fields.Integer( - description="Maximum number of returned datasets per page", - missing=100, + metadata={"description": "Maximum number of returned datasets per page"}, + load_default=100, validate=validate.Range(min=0), ) page = fields.Integer( - missing=0, description="Current dataset page", validate=validate.Range(min=0) + metadata={"description": "Current dataset page"}, + load_default=0, + validate=validate.Range(min=0), ) @post_load @@ -41,19 +49,21 @@ def list_items( class DatasetSchema(Schema): - class Meta: - ordered = True - - page = fields.Integer(description="Current page", required=True) + page = fields.Integer( + metadata={"description": "Current page"}, + required=True, + ) limit = fields.Integer( - description="Maximum number of returned items", required=True + metadata={"description": "Maximum number of returned items"}, + required=True, ) datasets = fields.List( fields.Dict( - values=fields.String(example="value1"), keys=fields.String(example="key1") + values=fields.String(metadata={"example": "value1"}), + keys=fields.String(metadata={"example": "key1"}), ), required=True, - description="Array containing all available key combinations", + metadata={"description": "Array containing all available key combinations"}, ) diff --git a/terracotta/server/keys.py b/terracotta/server/keys.py index 3c7d292d..34da856c 100644 --- a/terracotta/server/keys.py +++ b/terracotta/server/keys.py @@ -10,11 +10,8 @@ class KeyItemSchema(Schema): - class Meta: - ordered = True - - key = fields.String(description="Key name", required=True) - description = fields.String(description="Key description") + key = fields.String(metadata={"description": "Key name"}, required=True) + description = fields.String(metadata={"description": "Key description"}) class KeySchema(Schema): diff --git a/terracotta/server/metadata.py b/terracotta/server/metadata.py index 573926fd..8d3b2d85 100644 --- a/terracotta/server/metadata.py +++ b/terracotta/server/metadata.py @@ -14,50 +14,51 @@ class MetadataSchema(Schema): - class Meta: - ordered = True - keys = fields.Dict( keys=fields.String(), values=fields.String(), - description="Keys identifying dataset", + metadata={"description": "Keys identifying dataset"}, required=True, ) bounds = fields.List( - fields.Number(), + fields.Float(), validate=validate.Length(equal=4), required=True, - description="Physical bounds of dataset in WGS84 projection", + metadata={"description": "Physical bounds of dataset in WGS84 projection"}, ) convex_hull = fields.Dict( - required=True, description="GeoJSON representation of the dataset's convex hull" + required=True, + metadata={"description": "GeoJSON representation of the dataset's convex hull"}, ) - valid_percentage = fields.Number( - description="Percentage of valid data in the dataset" + valid_percentage = fields.Float( + metadata={"description": "Percentage of valid data in the dataset"} ) range = fields.List( - fields.Number(), + fields.Float(), validate=validate.Length(equal=2), required=True, - description="Minimum and maximum data value", + metadata={"description": "Minimum and maximum data value"}, + ) + mean = fields.Float(metadata={"description": "Data mean"}, required=True) + stdev = fields.Float( + metadata={"description": "Data standard deviation"}, required=True ) - mean = fields.Number(description="Data mean", required=True) - stdev = fields.Number(description="Data standard deviation", required=True) percentiles = fields.List( - fields.Number(), + fields.Float(), validate=validate.Length(equal=99), required=True, - description="1st, 2nd, 3rd, ..., 99th data percentile", + metadata={"description": "1st, 2nd, 3rd, ..., 99th data percentile"}, ) metadata = fields.Raw( - description="Any additional (manually added) metadata", required=True + metadata={"description": "Any additional (manually added) metadata"}, + required=True, ) class MetadataColumnsSchema(Schema): columns = fields.List( fields.String(), - description="List of columns to return", + metadata={"description": "List of columns to return"}, required=False, ) @@ -82,11 +83,11 @@ class MultipleMetadataDatasetsSchema(Schema): keys = fields.List( fields.List( fields.String(), - description="Keys identifying dataset", + metadata={"description": "Keys identifying dataset"}, required=True, ), required=True, - description="Array containing all available key combinations", + metadata={"description": "Array containing all available key combinations"}, ) diff --git a/terracotta/server/rgb.py b/terracotta/server/rgb.py index 606d86ab..767c5ce4 100644 --- a/terracotta/server/rgb.py +++ b/terracotta/server/rgb.py @@ -20,69 +20,82 @@ class RGBQuerySchema(Schema): keys = fields.String( - required=True, description="Keys identifying dataset, in order" + required=True, + metadata={"description": "Keys identifying dataset, in order"}, ) - tile_z = fields.Int(required=True, description="Requested zoom level") - tile_y = fields.Int(required=True, description="y coordinate") - tile_x = fields.Int(required=True, description="x coordinate") + tile_z = fields.Int(required=True, metadata={"description": "Requested zoom level"}) + tile_y = fields.Int(required=True, metadata={"description": "y coordinate"}) + tile_x = fields.Int(required=True, metadata={"description": "x coordinate"}) class RGBOptionSchema(Schema): class Meta: unknown = EXCLUDE - r = fields.String(required=True, description="Key value for red band") - g = fields.String(required=True, description="Key value for green band") - b = fields.String(required=True, description="Key value for blue band") + r = fields.String(required=True, metadata={"description": "Key value for red band"}) + g = fields.String( + required=True, metadata={"description": "Key value for green band"} + ) + b = fields.String( + required=True, metadata={"description": "Key value for blue band"} + ) r_range = fields.List( StringOrNumber(allow_none=True, validate=validate_stretch_range), validate=validate.Length(equal=2), - example="[0,1]", - missing=None, - description=( - "Stretch range [min, max] to use for the red band as JSON array. " - "Min and max may be numbers to use as absolute range, or strings " - "of the format `p` with an integer between 0 and 100 " - "to use percentiles of the image instead. " - "Null values indicate global minimum / maximum." - ), + load_default=None, + metadata={ + "example": "[0,1]", + "description": ( + "Stretch range [min, max] to use for the red band as JSON array. " + "Min and max may be numbers to use as absolute range, or strings " + "of the format `p` with an integer between 0 and 100 " + "to use percentiles of the image instead. " + "Null values indicate global minimum / maximum." + ), + }, ) g_range = fields.List( StringOrNumber(allow_none=True, validate=validate_stretch_range), validate=validate.Length(equal=2), - example="[0,1]", - missing=None, - description=( - "Stretch range [min, max] to use for the green band as JSON array. " - "Min and max may be numbers to use as absolute range, or strings " - "of the format `p` with an integer between 0 and 100 " - "to use percentiles of the image instead. " - "Null values indicate global minimum / maximum." - ), + load_default=None, + metadata={ + "example": "[0,1]", + "description": ( + "Stretch range [min, max] to use for the green band as JSON array. " + "Min and max may be numbers to use as absolute range, or strings " + "of the format `p` with an integer between 0 and 100 " + "to use percentiles of the image instead. " + "Null values indicate global minimum / maximum." + ), + }, ) b_range = fields.List( StringOrNumber(allow_none=True, validate=validate_stretch_range), validate=validate.Length(equal=2), - example="[0,1]", - missing=None, - description=( - "Stretch range [min, max] to use for the blue band as JSON array. " - "Min and max may be numbers to use as absolute range, or strings " - "of the format `p` with an integer between 0 and 100 " - "to use percentiles of the image instead. " - "Null values indicate global minimum / maximum." - ), + load_default=None, + metadata={ + "example": "[0,1]", + "description": ( + "Stretch range [min, max] to use for the blue band as JSON array. " + "Min and max may be numbers to use as absolute range, or strings " + "of the format `p` with an integer between 0 and 100 " + "to use percentiles of the image instead. " + "Null values indicate global minimum / maximum." + ), + }, ) color_transform = fields.String( validate=partial(validate_color_transform, test_array_bands=3), - missing=None, - description="Color transform DSL string from color-operations.", + load_default=None, + metadata={"description": "Color transform DSL string from color-operations."}, ) tile_size = fields.List( fields.Integer(), validate=validate.Length(equal=2), - example="[256,256]", - description="Pixel dimensions of the returned PNG image as JSON list.", + metadata={ + "example": "[256,256]", + "description": "Pixel dimensions of the returned PNG image as JSON list.", + }, ) @pre_load @@ -132,7 +145,7 @@ def get_rgb(tile_z: int, tile_y: int, tile_x: int, keys: str = "") -> Response: class RGBPreviewQuerySchema(Schema): keys = fields.String( - required=True, description="Keys identifying dataset, in order" + required=True, metadata={"description": "Keys identifying dataset, in order"} ) diff --git a/terracotta/server/singleband.py b/terracotta/server/singleband.py index f3b060b4..b6fc6858 100644 --- a/terracotta/server/singleband.py +++ b/terracotta/server/singleband.py @@ -29,11 +29,11 @@ class SinglebandQuerySchema(Schema): keys = fields.String( - required=True, description="Keys identifying dataset, in order" + required=True, metadata={"description": "Keys identifying dataset, in order"} ) - tile_z = fields.Int(required=True, description="Requested zoom level") - tile_y = fields.Int(required=True, description="y coordinate") - tile_x = fields.Int(required=True, description="x coordinate") + tile_z = fields.Int(required=True, metadata={"description": "Requested zoom level"}) + tile_y = fields.Int(required=True, metadata={"description": "y coordinate"}) + tile_x = fields.Int(required=True, metadata={"description": "x coordinate"}) class SinglebandOptionSchema(Schema): @@ -43,46 +43,59 @@ class Meta: stretch_range = fields.List( StringOrNumber(allow_none=True, validate=validate_stretch_range), validate=validate.Length(equal=2), - example="[0,1]", - description=( - "Stretch range [min, max] to use as JSON array. " - "Min and max may be numbers to use as absolute range, or strings " - "of the format `p` with an integer between 0 and 100 " - "to use percentiles of the image instead. " - "Null values indicate global minimum / maximum." - ), - missing=None, + metadata={ + "example": "[0,1]", + "description": ( + "Stretch range [min, max] to use as JSON array. " + "Min and max may be numbers to use as absolute range, or strings " + "of the format `p` with an integer between 0 and 100 " + "to use percentiles of the image instead. " + "Null values indicate global minimum / maximum." + ), + }, + load_default=None, ) colormap = fields.String( - description="Colormap to apply to image (see /colormap)", + metadata={"description": "Colormap to apply to image (see /colormap)"}, validate=validate.OneOf(("explicit", *AVAILABLE_CMAPS)), - missing=None, + load_default=None, ) explicit_color_map = fields.Dict( - keys=fields.Number(), - values=fields.List(fields.Number, validate=validate.Length(min=3, max=4)), - example='{"0": [255, 255, 255]}', - description="Explicit value-color mapping to use, encoded as JSON object. " - "Must be given together with `colormap=explicit`. Color values can be " - "specified either as RGB or RGBA tuple (in the range of [0, 255]), or as " - "hex strings.", + # TODO: that might be wrong? + keys=fields.Float(), + values=fields.List(fields.Float, validate=validate.Length(min=3, max=4)), + metadata={ + "example": '{"0": [255, 255, 255]}', + "description": ( + "Explicit value-color mapping to use, encoded as JSON object. " + "Must be given together with `colormap=explicit`. Color values can be " + "specified either as RGB or RGBA tuple (in the range of [0, 255]), or as " + "hex strings." + ), + }, ) color_transform = fields.String( validate=partial(validate_color_transform, test_array_bands=1), - missing=None, - example="gamma 1 1.5, sigmoidal 1 15 0.5", - description="Color transform DSL string from color-operations." - "All color operations for singleband should specify band 1.", + load_default=None, + metadata={ + "example": "gamma 1 1.5, sigmoidal 1 15 0.5", + "description": ( + "Color transform DSL string from color-operations." + "All color operations for singleband should specify band 1." + ), + }, ) tile_size = fields.List( fields.Integer(), validate=validate.Length(equal=2), - example="[256,256]", - description="Pixel dimensions of the returned PNG image as JSON list.", + metadata={ + "example": "[256,256]", + "description": "Pixel dimensions of the returned PNG image as JSON list.", + }, ) @validates_schema @@ -164,7 +177,7 @@ def get_singleband(tile_z: int, tile_y: int, tile_x: int, keys: str) -> Response class SinglebandPreviewSchema(Schema): keys = fields.String( - required=True, description="Keys identifying dataset, in order" + required=True, metadata={"description": "Keys identifying dataset, in order"} )