Skip to content

Commit

Permalink
Merge pull request #81 from specifysystems/find-circular-imports
Browse files Browse the repository at this point in the history
Find circular imports
  • Loading branch information
zzeppozz authored Apr 1, 2024
2 parents 4a41a0e + 759f21e commit a36dd85
Show file tree
Hide file tree
Showing 33 changed files with 2,336 additions and 1,180 deletions.
2 changes: 1 addition & 1 deletion .env.broker.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
SECRET_KEY=dev
WORKING_DIRECTORY=/scratch-path

FQDN=analyst.localhost
FQDN=broker.localhost
PYTHONPATH=/home/specify/flask_app
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# ........................................................
# Backend base image
FROM python:3.10.0rc2-alpine3.14 as base
FROM python:3.12.2-alpine3.19 as base

LABEL maintainer="Specify Collections Consortium <github.com/specify>"

Expand All @@ -20,7 +20,8 @@ USER specify

COPY --chown=specify:specify ./requirements.txt .

RUN python -m venv venv \
RUN python3 -m venv venv \
&& venv/bin/pip install --upgrade pip \
&& venv/bin/pip install --no-cache-dir -r ./requirements.txt

COPY --chown=specify:specify ./sppy ./sppy
Expand Down
19 changes: 5 additions & 14 deletions config/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ server {
return 301 https://$host$request_uri;
}

# Broker
server {
listen 443 ssl;
index index.html;
server_name broker-dev.spcoco.org;
server_name broker.localhost;

ssl_certificate /etc/letsencrypt/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/privkey.pem;
Expand All @@ -31,12 +32,6 @@ server {
proxy_set_header Origin "${scheme}://${http_host}";
}

location / {
root /var/www/;
try_files $uri $uri/ = 404;
gzip_static on;
}

location /static/js {
root /volumes/webpack-output;
rewrite ^/static/js/(.*)$ /$1 break;
Expand All @@ -48,11 +43,13 @@ server {
rewrite ^/static/(.*)$ /$1 break;
gzip_static on;
}
}

# Analyst
server {
listen 443 ssl;
index index.html;
server_name analyst-dev.spcoco.org;
server_name analyst.localhost;

ssl_certificate /etc/letsencrypt/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/privkey.pem;
Expand All @@ -76,12 +73,6 @@ server {
proxy_set_header Origin "${scheme}://${http_host}";
}

location / {
root /var/www/;
try_files $uri $uri/ = 404;
gzip_static on;
}

location /static/js {
root /volumes/webpack-output;
rewrite ^/static/js/(.*)$ /$1 break;
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ services:
ports:
- "5002:5002"
environment:
- FLASK_APP=flask_app.analyst:app
- FLASK_APP=flask_app.analyst.routes:app
- FLASK_MANAGE=flask_app.analyst.manage
- DEBUG_PORT=5002
volumes:
Expand Down
232 changes: 37 additions & 195 deletions flask_app/analyst/base.py
Original file line number Diff line number Diff line change
@@ -1,71 +1,19 @@
"""Parent Class for the Specify Network API services."""
from flask import Flask
from werkzeug.exceptions import BadRequest

import sppy.tools.s2n.utils as lmutil
from flask_app.common.s2n_type import AnalystOutput, APIEndpoint, APIService
from flask_app.common.base import _SpecifyNetworkService
from flask_app.common.s2n_type import AnalystOutput, APIService

app = Flask(__name__)
from sppy.tools.s2n.utils import get_traceback
from sppy.tools.provider.spnet import SpNetAnalyses


# .............................................................................
class _AnalystService:
class _AnalystService(_SpecifyNetworkService):
"""Base S-to-the-N service, handles parameter names and acceptable values."""
# overridden by subclasses
SERVICE_TYPE = APIService.AnalystRoot

# ...............................................
@classmethod
def _get_valid_requested_params(cls, user_params_string, valid_params):
"""Return valid and invalid options for parameters that accept >1 values.
Args:
user_params_string: user-requested parameters as a string.
valid_params: valid parameter values
Returns:
valid_requested_params: list of valid params from the provided query string
invalid_params: list of invalid params from the provided query string
Note:
For the badge service, exactly one provider is required. For all other
services, multiple providers are accepted, and None indicates to query all
valid providers.
"""
valid_requested_params = invalid_params = []

if user_params_string:
tmplst = user_params_string.split(",")
user_params = {tp.lower().strip() for tp in tmplst}

valid_requested_params = set()
invalid_params = set()
# valid_requested_providers, invalid_providers =
# cls.get_multivalue_options(user_provs, valid_providers)
for param in user_params:
if param in valid_params:
valid_requested_params.add(param)
else:
invalid_params.add(param)

invalid_params = list(invalid_params)
if valid_requested_params:
valid_requested_params = list(valid_requested_params)
else:
valid_requested_params = []

return valid_requested_params, invalid_params

# .............................................................................
@classmethod
def endpoint(cls):
"""Return the URL endpoint for this class.
Returns:
URL endpoint for the service
"""
endpoint = f"{APIEndpoint.Root}/{cls.SERVICE_TYPE['endpoint']}"
return endpoint

# ...............................................
@classmethod
def get_endpoint(cls, **kwargs):
Expand All @@ -75,7 +23,7 @@ def get_endpoint(cls, **kwargs):
**kwargs: keyword arguments are accepted but ignored
Returns:
flask_app.broker.s2n_type.S2nOutput object
flask_app.analyst.s2n_type.S2nOutput object
Raises:
Exception: on unknown error.
Expand Down Expand Up @@ -106,155 +54,49 @@ def _show_online(cls):

# ...............................................
@classmethod
def _fix_type_new(cls, key, provided_val):
"""Modify a parameter value to a valid type and value.
Args:
key: parameter key
provided_val: user-provided parameter value
Returns:
usr_val: a valid value for the parameter
valid_options: list of valid options (for error message)
Note:
Corrections:
* cast to correct type
* validate with any options
* if value is invalid (type or value), return the default.
"""
valid_options = None
if provided_val is None:
return None
# all strings are lower case
try:
provided_val = provided_val.lower()
except Exception:
pass

# First see if restricted to options
default_val = cls.SERVICE_TYPE["params"][key]["default"]
type_val = cls.SERVICE_TYPE["params"][key]["type"]
# If restricted options, check
try:
options = cls.SERVICE_TYPE["params"][key]["options"]
except KeyError:
options = None
else:
# Invalid option returns default value
if provided_val in options:
usr_val = provided_val
else:
valid_options = options
usr_val = default_val

# If not restricted to options
if options is None:
# Cast values to correct type. Failed conversions return default value
if isinstance(type_val, str) and not options:
usr_val = str(provided_val)

elif isinstance(type_val, float):
try:
usr_val = float(provided_val)
except ValueError:
usr_val = default_val

# Boolean also tests as int, so try boolean first
elif isinstance(type_val, bool):
if provided_val in (0, "0", "n", "no", "f", "false"):
usr_val = False
elif provided_val in (1, "1", "y", "yes", "t", "true"):
usr_val = True
else:
valid_options = (True, False)
usr_val = default_val

elif isinstance(type_val, int):
try:
usr_val = int(provided_val)
except ValueError:
usr_val = default_val

else:
usr_val = provided_val

return usr_val, valid_options

# ...............................................
@classmethod
def _process_params(cls, user_kwargs=None):
"""Modify all user provided keys to lowercase and values to correct types.
Args:
user_kwargs: dictionary of keywords and values sent by the user for
the current service.
Returns:
good_params: dictionary of valid parameters and values
errinfo: dictionary of errors for different error levels.
Note:
A list of valid values for a keyword can include None as a default
if user-provided value is invalid
Todo:
Do we need not_in_valid_options for error message?
"""
good_params = {}
errinfo = {}

# Correct all parameter keys/values present
for key in cls.SERVICE_TYPE["params"]:
val = user_kwargs[key]
# Done in calling function
if val is not None:
usr_val, valid_options = cls._fix_type_new(key, val)
if valid_options is not None and val not in valid_options:
errinfo = lmutil.add_errinfo(
errinfo, "error",
f"Value {val} for parameter {key} is not in valid options "
f"{cls.SERVICE_TYPE['params'][key]['options']}")
good_params[key] = None
else:
good_params[key] = usr_val

# Fill in defaults for missing parameters
for key in cls.SERVICE_TYPE["params"]:
param_meta = cls.SERVICE_TYPE["params"][key]
try:
_ = good_params[key]
except KeyError:
good_params[key] = param_meta["default"]

return good_params, errinfo

# ...............................................
@classmethod
def _standardize_params(cls, collection_id=None, organization_id=None):
def _standardize_params(
cls, dataset_key=None, pub_org_key=None, count_by=None, order=None,
limit=10):
"""Standardize query parameters to send to appropriate service.
Args:
collection_id: collection identifier for comparisons
organization_id: organization identifier for comparisons
dataset_key: unique GBIF dataset identifier for comparisons
pub_org_key: unique publishing organization identifier for comparisons
count_by: counts of "occurrence" or "species"
order: sort records "descending" or "ascending"
limit: integer indicating how many ranked records to return, value must
be less than QUERY_LIMIT.
Raises:
BadRequest: on invalid query parameters.
BadRequest: on unknown exception parsing parameters.
Returns:
a dictionary containing keys and properly formatted values for the
user specified parameters.
"""
user_kwargs = {
"collection_id": collection_id,
"organization_id": organization_id
"dataset_key": dataset_key,
"pub_org_key": pub_org_key,
"count_by": count_by,
"order": order,
"limit": limit
}

usr_params, errinfo = cls._process_params(user_kwargs)
try:
usr_params, errinfo = cls._process_params(user_kwargs)
except Exception:
error_description = get_traceback()
raise BadRequest(error_description)

return usr_params, errinfo
# errinfo["error"] indicates bad parameters, throws exception
try:
error_description = "; ".join(errinfo["error"])
raise BadRequest(error_description)
except KeyError:
pass

# ..........................
@staticmethod
def OPTIONS():
"""Common options request for all services (needed for CORS)."""
return
return usr_params, errinfo


# .............................................................................
Expand Down
1 change: 1 addition & 0 deletions flask_app/analyst/constants.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
"""Constants for the Specify Network Analyst API services."""
QUERY_LIMIT = 500
Loading

0 comments on commit a36dd85

Please sign in to comment.