Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 0.8.7 #45

Merged
merged 56 commits into from
Nov 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
ccedd4c
mongodb logs readable
Oct 28, 2024
9d6cfaf
Pre_path now only takes single values
awarde96 Oct 28, 2024
1ce8b2d
Merge branch 'master' into develop
peshence Nov 1, 2024
25139cb
black format
Nov 5, 2024
85579b7
Merge pull request #35 from MeteoSwiss-APN/fix/improve-mongodb-logs-r…
peshence Nov 5, 2024
d77476a
chore: bump versions
peshence Nov 5, 2024
265d69c
force numpy 1.26.4
peshence Nov 5, 2024
df42924
temp polytope-mars@develop
peshence Nov 6, 2024
c7d1519
hotfix polytope feature/try_non_existing_transformation_options
peshence Nov 7, 2024
0631750
pass all transforms
peshence Nov 7, 2024
0f65678
fix: updated pyfdb
peshence Nov 8, 2024
26a6aff
bump polytope-mars and -python releases
peshence Nov 8, 2024
1b73744
chore: cleanup
peshence Nov 8, 2024
344b6a1
Merge pull request #36 from ecmwf/bump/2024-11-05
sametd Nov 8, 2024
4edae1d
hotfix: downgrade numpy
peshence Nov 8, 2024
772ab40
fix[auth]: unpack dict before checking if list
peshence Nov 8, 2024
7065b2b
added caching for ldap get_user
sametd Nov 11, 2024
13c615a
feat[datasource]: add adapted schedule crack
peshence Nov 12, 2024
914c53c
version bump
peshence Nov 13, 2024
61bff0d
make schedule work with various string requests
peshence Nov 14, 2024
2abae04
load products to dicts
peshence Nov 14, 2024
d44051e
typo
peshence Nov 14, 2024
834efc0
add check if schedule has been loaded
peshence Nov 14, 2024
aff50a4
Merge branch 'develop' into feat/schedule
peshence Nov 14, 2024
6e50d4a
parse other time formats
peshence Nov 14, 2024
2832116
fix time HH format
peshence Nov 14, 2024
ee34d9b
ensure step is string
peshence Nov 14, 2024
51f0884
zfill step
peshence Nov 14, 2024
dc0ae1b
add coercion of non-string values in request
jameshawkes Nov 14, 2024
c64d1ba
black
jameshawkes Nov 14, 2024
3e3e666
isort
jameshawkes Nov 14, 2024
92f5ee4
bump versions
jameshawkes Nov 15, 2024
dacedca
fix s3 extension types
jameshawkes Nov 15, 2024
6feeab6
black
jameshawkes Nov 15, 2024
1bdc61d
covjsonkit version
jameshawkes Nov 15, 2024
be42a1f
coercion error on plain lists
jameshawkes Nov 15, 2024
5570099
black
jameshawkes Nov 15, 2024
004c249
include mars_only products
peshence Nov 15, 2024
0d92352
Merge branch 'develop' into feat/schedule
peshence Nov 15, 2024
dcbff6d
add schedule enabled option, change schedule file path
peshence Nov 15, 2024
a805a1a
Merge pull request #39 from ecmwf/feat/schedule
jameshawkes Nov 16, 2024
694a7bf
bump versions of polytope-mars and polytope feature extraction
jameshawkes Nov 16, 2024
eb0f3d8
allow default parameters, and extra role checking per datasource
jameshawkes Nov 16, 2024
3e75da9
make schedule check optional
jameshawkes Nov 16, 2024
5df1a05
empty extra roles should be OK
jameshawkes Nov 16, 2024
4965adb
typo
jameshawkes Nov 16, 2024
db2956f
improve error reporting to the user
jameshawkes Nov 16, 2024
316de74
improve error reporting
jameshawkes Nov 16, 2024
128741e
better error messages
jameshawkes Nov 16, 2024
7642fd5
fix defaults
jameshawkes Nov 16, 2024
6b75e9a
don't fail on missing env var
jameshawkes Nov 16, 2024
4a71210
fix date string "-1" and "0" etc
jameshawkes Nov 16, 2024
bd2bc5c
give a user-readable name to datasources
jameshawkes Nov 16, 2024
6d64988
black
jameshawkes Nov 16, 2024
85515b9
bump version
jameshawkes Nov 16, 2024
5794a9b
correct version number
jameshawkes Nov 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ ARG ecbuild_version=3.8.2
ARG eccodes_version=2.33.1
ARG eckit_version=1.28.0
ARG fdb_version=5.13.2
ARG pyfdb_version=0.0.3
ARG pyfdb_version=0.1.0
RUN apt update
# COPY polytope-deployment/common/default_fdb_schema /polytope/config/fdb/default

Expand Down Expand Up @@ -173,6 +173,7 @@ RUN set -eux && \
# Install pyfdb \
RUN set -eux \
&& git clone --single-branch --branch ${pyfdb_version} https://github.com/ecmwf/pyfdb.git \
&& python -m pip install "numpy<2.0" --user\
&& python -m pip install ./pyfdb --user

#######################################################
Expand Down Expand Up @@ -200,7 +201,7 @@ RUN set -eux \
ls -R /opt

RUN set -eux \
&& git clone --single-branch --branch develop https://github.com/ecmwf/gribjump.git
&& git clone --single-branch --branch ${gribjump_version} https://github.com/ecmwf/gribjump.git
# Install pygribjump
RUN set -eux \
&& cd /gribjump \
Expand Down Expand Up @@ -229,9 +230,13 @@ FROM mars-base AS mars-base-c
RUN apt update && apt install -y liblapack3 mars-client=${mars_client_c_version} mars-client-cloud

FROM mars-base AS mars-base-cpp
ARG pyfdb_version=0.1.0
RUN apt update && apt install -y mars-client-cpp=${mars_client_cpp_version}
RUN set -eux \
&& python3 -m pip install git+https://github.com/ecmwf/pyfdb.git@master --user
&& git clone --single-branch --branch ${pyfdb_version} https://github.com/ecmwf/pyfdb.git \
&& python -m pip install "numpy<2.0" --user\
&& python -m pip install ./pyfdb --user


FROM blank-base AS blank-base-c
FROM blank-base AS blank-base-cpp
Expand Down Expand Up @@ -342,7 +347,6 @@ COPY --chown=polytope --from=gribjump-base-final /root/.local /home/polytope/.lo
# Copy python requirements
COPY --chown=polytope --from=worker-base /root/.venv /home/polytope/.local


# Install the server source
COPY --chown=polytope . /polytope/

Expand Down
8 changes: 4 additions & 4 deletions polytope_server/common/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,16 @@ def authenticate(self, auth_header) -> User:
def is_authorized(self, user, roles):
"""Checks if the user has any of the provided roles"""

# roles can be a single value; convert to a list
if not isinstance(roles, (tuple, list, set)):
roles = [roles]

# roles can be a dict of realm:[roles] mapping; find the relevant realm.
if isinstance(roles, dict):
if user.realm not in roles:
raise ForbiddenRequest("Not authorized to access this resource.")
roles = roles[user.realm]

# roles can be a single value; convert to a list
if not isinstance(roles, (tuple, list, set)):
roles = [roles]

for required_role in roles:
if required_role in user.roles:
return True
Expand Down
2 changes: 2 additions & 0 deletions polytope_server/common/authorization/ldap_authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from ldap3 import SUBTREE, Connection, Server

from ..auth import User
from ..caching import cache
from . import authorization


Expand All @@ -40,6 +41,7 @@ def __init__(self, name, realm, config):
self.username_attribute = config.get("username-attribute", None)
super().__init__(name, realm, config)

@cache(lifetime=120)
def get_roles(self, user: User) -> list:
if user.realm != self.realm():
raise ValueError(
Expand Down
251 changes: 251 additions & 0 deletions polytope_server/common/datasource/coercion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import copy
from datetime import datetime, timedelta
from typing import Any, Dict


class CoercionError(Exception):
pass


class Coercion:

allow_ranges = [
"number",
"step",
"date",
]
allow_lists = ["class", "stream", "type", "expver", "param", "number", "date", "step"]

@staticmethod
def coerce(request: Dict[str, Any]) -> Dict[str, Any]:
request = copy.deepcopy(request)
for key, value in request.items():
request[key] = Coercion.coerce_value(key, value)
return request

@staticmethod
def coerce_value(key: str, value: Any):
if key in Coercion.coercer:
coercer_func = Coercion.coercer[key]

if isinstance(value, list):
# Coerce each item in the list
coerced_values = [Coercion.coerce_value(key, v) for v in value]
return "/".join(coerced_values)
elif isinstance(value, str):

if "/to/" in value and key in Coercion.allow_ranges:
# Handle ranges with possible "/by/" suffix
start_value, rest = value.split("/to/", 1)
if not rest:
raise CoercionError(f"Invalid range format for key {key}.")

if "/by/" in rest:
end_value, suffix = rest.split("/by/", 1)
suffix = "/by/" + suffix # Add back the '/by/'
else:
end_value = rest
suffix = ""

# Coerce start_value and end_value
start_coerced = coercer_func(start_value)
end_coerced = coercer_func(end_value)

return f"{start_coerced}/to/{end_coerced}{suffix}"
elif "/" in value and key in Coercion.allow_lists:
# Handle lists
coerced_values = [coercer_func(v) for v in value.split("/")]
return "/".join(coerced_values)
else:
# Single value
return coercer_func(value)
else: # not list or string
return coercer_func(value)
else:
if isinstance(value, list):
# Join list into '/' separated string
coerced_values = [str(v) for v in value]
return "/".join(coerced_values)
else:
return value

@staticmethod
def coerce_date(value: Any) -> str:
try:
# Attempt to convert the value to an integer
int_value = int(value)
if int_value > 0:
# Positive integers are assumed to be dates in YYYYMMDD format
date_str = str(int_value)
try:
datetime.strptime(date_str, "%Y%m%d")
return date_str
except ValueError:
raise CoercionError("Invalid date format, expected YYYYMMDD or YYYY-MM-DD.")
else:
# Zero or negative integers represent relative days from today
target_date = datetime.today() + timedelta(days=int_value)
return target_date.strftime("%Y%m%d")
except (ValueError, TypeError):
# The value is not an integer or cannot be converted to an integer
pass

if isinstance(value, str):
value_stripped = value.strip()
# Try parsing as YYYYMMDD
try:
datetime.strptime(value_stripped, "%Y%m%d")
return value_stripped
except ValueError:
# Try parsing as YYYY-MM-DD
try:
date_obj = datetime.strptime(value_stripped, "%Y-%m-%d")
return date_obj.strftime("%Y%m%d")
except ValueError:
raise CoercionError("Invalid date format, expected YYYYMMDD or YYYY-MM-DD.")
else:
raise CoercionError("Invalid date format, expected YYYYMMDD or YYYY-MM-DD.")

@staticmethod
def coerce_step(value: Any) -> str:

if isinstance(value, int):
if value < 0:
raise CoercionError("Step must be greater than or equal to 0.")
else:
return str(value)
elif isinstance(value, str):
if not value.isdigit() or int(value) < 0:
raise CoercionError("Step must be greater than or equal to 0.")
return value
else:
raise CoercionError("Invalid type, expected integer or string.")

@staticmethod
def coerce_number(value: Any) -> str:

if isinstance(value, int):
if value <= 0:
raise CoercionError("Number must be a positive value.")
else:
return str(value)
elif isinstance(value, str):
if not value.isdigit() or int(value) <= 0:
raise CoercionError("Number must be a positive integer.")
return value
else:
raise CoercionError("Invalid type, expected integer or string.")

@staticmethod
def coerce_param(value: Any) -> str:
if isinstance(value, int):
return str(value)
elif isinstance(value, str):
return value
else:
raise CoercionError("Invalid param type, expected integer or string.")

@staticmethod
def coerce_time(value: Any) -> str:
if isinstance(value, int):
if value < 0:
raise CoercionError("Invalid time format, expected HHMM or HH greater than zero.")
elif value < 24:
# Treat as hour with minute=0
hour = value
minute = 0
elif 100 <= value <= 2359:
# Possible HHMM format
hour = value // 100
minute = value % 100
else:
raise CoercionError("Invalid time format, expected HHMM or HH.")
elif isinstance(value, str):
value_stripped = value.strip()
# Check for colon-separated time (e.g., "12:00")
if ":" in value_stripped:
parts = value_stripped.split(":")
if len(parts) != 2:
raise CoercionError("Invalid time format, expected HHMM or HH.")
hour_str, minute_str = parts
if not (hour_str.isdigit() and minute_str.isdigit()):
raise CoercionError("Invalid time format, expected HHMM or HH.")
hour = int(hour_str)
minute = int(minute_str)
else:
if value_stripped.isdigit():
num_digits = len(value_stripped)
if num_digits == 4:
# Format is "HHMM"
hour = int(value_stripped[:2])
minute = int(value_stripped[2:])
elif num_digits <= 2:
# Format is "H" or "HH"
hour = int(value_stripped)
minute = 0
else:
raise CoercionError("Invalid time format, expected HHMM or HH.")
else:
raise CoercionError("Invalid time format, expected HHMM or HH.")
else:
raise CoercionError("Invalid type for time, expected string or integer.")

# Validate hour and minute
if not (0 <= hour <= 23):
raise CoercionError("Invalid time format, expected HHMM or HH.")
if not (0 <= minute <= 59):
raise CoercionError("Invalid time format, expected HHMM or HH.")
if minute != 0:
raise CoercionError("Invalid time format, expected HHMM or HH.")

# Format time as HHMM
time_str = f"{hour:02d}{minute:02d}"
return time_str

# Validate hour and minute
if not (0 <= hour <= 23):
raise CoercionError("Hour must be between 0 and 23.")
if not (0 <= minute <= 59):
raise CoercionError("Minute must be between 0 and 59.")
if minute != 0:
# In your test cases, minute must be zero
raise CoercionError("Minute must be zero.")

# Format time as HHMM
time_str = f"{hour:02d}{minute:02d}"
return time_str

@staticmethod
def coerce_expver(value: Any) -> str:

# Integers accepted, converted to 4-length strings
if isinstance(value, int):
if 0 <= value <= 9999:
return f"{value:0>4d}"
else:
raise CoercionError("expver integer must be between 0 and 9999 inclusive.")

# Strings accepted if they are convertible to integer or exactly 4 characters long
elif isinstance(value, str):
if value.isdigit():
int_value = int(value.lstrip("0") or "0")
if 0 <= int_value <= 9999:
return f"{int_value:0>4d}"
else:
raise CoercionError("expver integer string must represent a number between 0 and 9999 inclusive.")
elif len(value) == 4:
return value
else:
raise CoercionError("expver string length must be 4 characters exactly.")

else:
raise CoercionError("expver must be an integer or a string.")

coercer = {
"date": coerce_date,
"step": coerce_step,
"number": coerce_number,
"param": coerce_param,
"time": coerce_time,
"expver": coerce_expver,
}
14 changes: 8 additions & 6 deletions polytope_server/common/datasource/datasource.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ def match(self, request: str) -> None:
"""Checks if the request matches the datasource, raises on failure"""
raise NotImplementedError()

def repr(self) -> str:
"""Returns a string name of the datasource, presented to the user on error"""
raise NotImplementedError

def get_type(self) -> str:
"""Returns a string stating the type of this object (e.g. fdb, mars, echo)"""
raise NotImplementedError()
Expand Down Expand Up @@ -84,9 +88,7 @@ def dispatch(self, request, input_data) -> bool:
if hasattr(self, "silent_match") and self.silent_match:
pass
else:
request.user_message += "Skipping datasource {} due to match error: {}\n".format(
self.get_type(), repr(e)
)
request.user_message += "Skipping datasource {}: {}\n".format(self.repr(), str(e))
tb = traceback.format_exception(None, e, e.__traceback__)
logging.info(tb)

Expand All @@ -97,7 +99,7 @@ def dispatch(self, request, input_data) -> bool:
datasource_role_rules = self.config.get("roles", None)
if datasource_role_rules is not None:
if not any(role in request.user.roles for role in datasource_role_rules.get(request.user.realm, [])):
request.user_message += "Skipping datasource {}. User is forbidden.\n".format(self.get_type())
request.user_message += "Skipping datasource {}: user is not authorised.\n".format(self.repr())
return False

# Retrieve/Archive/etc.
Expand All @@ -111,8 +113,8 @@ def dispatch(self, request, input_data) -> bool:
raise NotImplementedError()

except NotImplementedError as e:
request.user_message += "Skipping datasource {}. Verb {} not available: {}\n".format(
self.get_type(), request.verb, repr(e)
request.user_message += "Skipping datasource {}: method '{}' not available: {}\n".format(
self.repr(), request.verb, repr(e)
)
return False

Expand Down
3 changes: 3 additions & 0 deletions polytope_server/common/datasource/dummy.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ def retrieve(self, request):

return True

def repr(self):
return self.config.get("repr", "dummy")

def result(self, request):
chunk_size = 2 * 1024 * 1024
data_generated = 0
Expand Down
3 changes: 3 additions & 0 deletions polytope_server/common/datasource/echo.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ def retrieve(self, request):
self.data = request.user_request
return True

def repr(self):
return self.config.get("repr", "echo")

def result(self, request):
yield self.data

Expand Down
Loading
Loading