Skip to content

Commit 59bbda2

Browse files
committed
Add pydantic schema
1 parent 25e454c commit 59bbda2

File tree

8 files changed

+137
-81
lines changed

8 files changed

+137
-81
lines changed

.flake8

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[flake8]
2+
per-file-ignores =
3+
settings.py: E501

gravity/config_manager.py

Lines changed: 24 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,7 @@
1212
from yaml import safe_load
1313

1414
from gravity import __version__
15-
from gravity.defaults import (
16-
CELERY_DEFAULT_CONFIG,
17-
DEFAULT_INSTANCE_NAME,
18-
GUNICORN_DEFAULT_CONFIG,
19-
GXIT_DEFAULT_IP,
20-
GXIT_DEFAULT_PORT,
21-
GXIT_DEFAULT_SESSIONS,
22-
)
15+
from gravity.settings import Settings
2316
from gravity.io import debug, error, exception, info, warn
2417
from gravity.state import (
2518
ConfigFile,
@@ -76,50 +69,38 @@ def __convert_config(self):
7669
os.unlink(config_state_json)
7770

7871
def get_config(self, conf, defaults=None):
72+
defaults = defaults or {}
7973
server_section = self.galaxy_server_config_section
8074
with open(conf) as config_fh:
8175
config_dict = safe_load(config_fh)
82-
83-
default_config = {
84-
"galaxy_root": None,
85-
"log_dir": join(expanduser(self.state_dir), "log"),
86-
"virtualenv": None,
87-
"instance_name": DEFAULT_INSTANCE_NAME,
88-
"app_server": "gunicorn",
89-
"gunicorn": GUNICORN_DEFAULT_CONFIG,
90-
"celery": CELERY_DEFAULT_CONFIG,
91-
"gx_it_proxy": {},
92-
"handlers": {},
93-
}
94-
if defaults is not None:
95-
recursive_update(default_config, defaults)
76+
_gravity_config = config_dict.get(self.gravity_config_section) or {}
77+
gravity_config = Settings(**recursive_update(defaults, _gravity_config))
78+
if gravity_config.log_dir is None:
79+
gravity_config.log_dir = join(expanduser(self.state_dir), "log")
9680

9781
if server_section not in config_dict and self.gravity_config_section not in config_dict:
9882
error(f"Config file {conf} does not look like valid Galaxy, Reports or Tool Shed configuration file")
9983
return None
10084

10185
app_config = config_dict.get(server_section) or {}
102-
_gravity_config = config_dict.get(self.gravity_config_section) or {}
103-
gravity_config = recursive_update(default_config, _gravity_config)
10486

10587
config = ConfigFile()
10688
config.attribs = {}
10789
config.services = []
108-
config.instance_name = gravity_config["instance_name"]
90+
config.instance_name = gravity_config.instance_name
10991
config.config_type = server_section
110-
config.attribs["app_server"] = gravity_config["app_server"]
111-
config.attribs["log_dir"] = gravity_config["log_dir"]
112-
config.attribs["virtualenv"] = gravity_config["virtualenv"]
113-
config.attribs["gunicorn"] = gravity_config["gunicorn"]
114-
config.attribs["celery"] = gravity_config["celery"]
115-
config.attribs["handlers"] = gravity_config["handlers"]
116-
config.attribs["gx_it_proxy"] = gravity_config["gx_it_proxy"]
92+
config.attribs["app_server"] = gravity_config.app_server
93+
config.attribs["log_dir"] = gravity_config.log_dir
94+
config.attribs["virtualenv"] = gravity_config.virtualenv
95+
config.attribs["gunicorn"] = gravity_config.gunicorn.dict()
96+
config.attribs["celery"] = gravity_config.celery.dict()
97+
config.attribs["handlers"] = gravity_config.handlers
11798
# Store gravity version, in case we need to convert old setting
11899
config.attribs['gravity_version'] = __version__
119100
webapp_service_names = []
120101

121102
# shortcut for galaxy configs in the standard locations -- explicit arg ?
122-
config.attribs["galaxy_root"] = app_config.get("root") or gravity_config.get("galaxy_root")
103+
config.attribs["galaxy_root"] = app_config.get("root") or gravity_config.galaxy_root
123104
if config.attribs["galaxy_root"] is None:
124105
if os.environ.get("GALAXY_ROOT_DIR"):
125106
config.attribs["galaxy_root"] = abspath(os.environ["GALAXY_ROOT_DIR"])
@@ -156,27 +137,26 @@ def get_config(self, conf, defaults=None):
156137
self.create_gxit_services(gravity_config, app_config, config)
157138
return config
158139

159-
def create_handler_services(self, gravity_config, config):
140+
def create_handler_services(self, gravity_config: Settings, config):
160141
expanded_handlers = self.expand_handlers(gravity_config, config)
161142
for service_name, handler_settings in expanded_handlers.items():
162143
pools = handler_settings.get('pools')
163144
config.services.append(
164145
service_for_service_type("standalone")(config_type=config.config_type, service_name=service_name, server_pools=pools))
165146

166-
def create_gxit_services(self, gravity_config, app_config, config):
167-
if app_config.get("interactivetools_enable") and gravity_config["gx_it_proxy"].get("enable"):
147+
def create_gxit_services(self, gravity_config: Settings, app_config, config):
148+
if app_config.get("interactivetools_enable") and gravity_config.gx_it_proxy.enable:
168149
# TODO: resolve against data_dir, or bring in galaxy-config ?
169150
# CWD in supervisor template is galaxy_root, so this should work for simple cases as is
170-
gxit_config = gravity_config['gx_it_proxy']
171-
gxit_config["sessions"] = app_config.get("interactivetools_map", GXIT_DEFAULT_SESSIONS)
172-
gxit_config["ip"] = gxit_config.get("ip", GXIT_DEFAULT_IP)
173-
gxit_config["port"] = gxit_config.get("port", GXIT_DEFAULT_PORT)
174-
gxit_config["verbose"] = '--verbose' if gxit_config.get("verbose") else ''
175-
config.services.append(service_for_service_type("gx-it-proxy")(config_type=config.config_type, gxit=gxit_config))
151+
gxit_config = gravity_config.gx_it_proxy
152+
gxit_config.sessions = app_config.get("interactivetools_map", gxit_config.sessions)
153+
gxit_config.verbose = '--verbose' if gxit_config.verbose else ''
154+
config.services.append(service_for_service_type("gx-it-proxy")(config_type=config.config_type))
155+
config.attribs["gx_it_proxy"] = gravity_config.gx_it_proxy.dict()
176156

177157
@staticmethod
178-
def expand_handlers(gravity_config, config):
179-
handlers = gravity_config.get("handlers", {})
158+
def expand_handlers(gravity_config: Settings, config):
159+
handlers = gravity_config.handlers or {}
180160
expanded_handlers = {}
181161
default_name_template = "{name}_{process}"
182162
for service_name, handler_config in handlers.items():

gravity/defaults.py

Lines changed: 0 additions & 19 deletions
This file was deleted.

gravity/settings.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from enum import Enum
2+
from typing import (
3+
Any,
4+
Dict,
5+
Optional,
6+
)
7+
from pydantic import (
8+
BaseModel,
9+
BaseSettings,
10+
Field,
11+
)
12+
13+
14+
class LogLevel(str, Enum):
15+
debug = "DEBUG"
16+
info = "INFO"
17+
warning = "WARNING"
18+
error = "ERROR"
19+
20+
21+
class AppServer(str, Enum):
22+
gunicorn = "gunicorn"
23+
unicornherder = "unicornherder"
24+
25+
26+
class CelerySettings(BaseModel):
27+
28+
concurrency: int = Field(2, ge=0, description="Number of Celery Workers to start.")
29+
loglevel: LogLevel = Field(LogLevel.debug, description="Log Level to use for Celery Worker.")
30+
extra_args: str = Field(default="", description="Extra arguments to pass to Celery command line.")
31+
32+
class Config:
33+
use_enum_values = True
34+
35+
36+
class GunicornSettings(BaseModel):
37+
bind: str = Field(default="localhost:8080", description="The socket to bind. A string of the form: ``HOST``, ``HOST:PORT``, ``unix:PATH``, ``fd://FD``. An IP is a valid HOST.")
38+
workers: int = Field(default=1, ge=1, description=" Controls the number of Galaxy application processes Gunicorn will spawn. Increased web performance can be attained by increasing this value. If Gunicorn is the only application on the server, a good starting value is the number of CPUs * 2 + 1. 4-12 workers should be able to handle hundreds if not thousands of requests per second.")
39+
timeout: int = Field(default=300, ge=0, description="Gunicorn workers silent for more than this many seconds are killed and restarted. Value is a positive number or 0. Setting it to 0 has the effect of infinite timeouts by disabling timeouts for all workers entirely. If you disable the ``preload`` option workers need to have finished booting within the timeout.")
40+
extra_args: str = Field(default="", description="Extra arguments to pass to Gunicorn command line.")
41+
preload: bool = Field(default=True, description="Use Gunicorn's --preload option to fork workers after loading the Galaxy Application. Consumes less memory when multiple processes are configured.")
42+
43+
44+
class GxItProxySettings(BaseModel):
45+
enable: bool = Field(default=False, description="Set to true to start gx-it-proxy")
46+
ip: str = Field(default="localhost", description="Public-facing IP of the proxy")
47+
port: int = Field(default=4002, description="Public-facing port of the proxy")
48+
sessions: str = Field(default="database/interactivetools_map.sqlite", description="Routes file to monitor. Should be set to the same path as ``interactivetools_map`` in the ``galaxy:`` section.")
49+
verbose: bool = Field(default=True, description="Include verbose messages in gx-it-proxy")
50+
forward_ip: Optional[str] = Field(default=None, description="Forward all requests to IP. This is an advanced option that is only needed when proxying to remote interactive tool container that cannot be reached through the local network.")
51+
forward_port: Optional[int] = Field(default=None, description="Forward all requests to port. This is an advanced option that is only needed when proxying to remote interactive tool container that cannot be reached through the local network.")
52+
reverse_proxy: Optional[bool] = Field(default=False, description="Cause the proxy to rewrite location blocks with its own port. This is an advanced option that is only needed when proxying to remote interactive tool container that cannot be reached through the local network.")
53+
54+
55+
class Settings(BaseSettings):
56+
"""
57+
Configuration for gravity process manager. Configure gravity in this section. ``uwsgi:`` section will be ignored if Galaxy is started via gravity commands (e.g ``./run.sh``, ``galaxy`` or ``galaxyctl``).
58+
"""
59+
galaxy_root: Optional[str] = Field(None, description="Specify Galaxy's root directory. gravity will attempt to find the root directory, but you can set the directory explicitly with this option.")
60+
log_dir: Optional[str] = Field(None, description="Set to a directory that should contain log files for the processes controlled by gravity. If not specified defaults to ``<state_dir>/logs``.")
61+
virtualenv: Optional[str] = Field(None, description="Set to Galaxy's virtualenv directory. If not specified, gravity assumes all processes are on PATH.")
62+
app_server: AppServer = Field(AppServer.gunicorn, description="Select the application server. ``gunicorn`` is the default application server. ``unicornherder`` is a production-oriented manager for (G)unicorn servers that automates zero-downtime Galaxy server restarts, similar to uWSGI Zerg Mode used in the past.")
63+
instance_name: str = Field(default="_default_", description="Override the default instance name, this is hidden from you when running a single instance.")
64+
gunicorn: GunicornSettings = Field(default={}, description="Configuration for Gunicorn.")
65+
celery: CelerySettings = Field(default={}, description="Configuration for Celery Processes.")
66+
gx_it_proxy: GxItProxySettings = Field(default={}, description="Configuration for gx-it-proxy.")
67+
handlers: Dict[str, Dict[str, Any]] = Field(default={}, description="Configure dynamic handlers in this section. See https://docs.galaxyproject.org/en/latest/admin/scaling.html#dynamically-defined-handlers for details.")
68+
69+
class Config:
70+
env_prefix = "gravity_"
71+
env_nested_delimiter = "."
72+
case_sensitive = False
73+
use_enum_values = True

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def get_var(var_name):
3636
license="MIT",
3737
keywords="gravity galaxy",
3838
python_requires=">=3.6",
39-
install_requires=["Click", "supervisor", "pyyaml", "ruamel.yaml"],
39+
install_requires=["Click", "supervisor", "pyyaml", "ruamel.yaml", "pydantic"],
4040
entry_points={"console_scripts": [
4141
"galaxy = gravity.cli:galaxy",
4242
"galaxyctl = gravity.cli:galaxyctl",

tests/test_config_manager.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,27 @@
11
import json
22
from pathlib import Path
33

4-
from gravity.defaults import (
5-
DEFAULT_GUNICORN_BIND,
6-
DEFAULT_GUNICORN_TIMEOUT,
7-
DEFAULT_GUNICORN_WORKERS,
8-
DEFAULT_INSTANCE_NAME,
9-
CELERY_DEFAULT_CONFIG
10-
11-
)
4+
from gravity.settings import Settings
125

136

147
def test_register_defaults(galaxy_yml, galaxy_root_dir, state_dir, default_config_manager):
158
default_config_manager.add([str(galaxy_yml)])
169
assert str(galaxy_yml) in default_config_manager.state['config_files']
1710
state = default_config_manager.state['config_files'][str(galaxy_yml)]
11+
default_settings = Settings()
1812
assert state['config_type'] == 'galaxy'
19-
assert state['instance_name'] == DEFAULT_INSTANCE_NAME
13+
assert state['instance_name'] == default_settings.instance_name
2014
assert state['services'] == []
2115
attributes = state['attribs']
2216
assert attributes['app_server'] == 'gunicorn'
2317
assert Path(attributes['log_dir']) == Path(state_dir) / 'log'
2418
assert Path(attributes['galaxy_root']) == galaxy_root_dir
2519
gunicorn_attributes = attributes['gunicorn']
26-
assert gunicorn_attributes['bind'] == DEFAULT_GUNICORN_BIND
27-
assert gunicorn_attributes['workers'] == DEFAULT_GUNICORN_WORKERS
28-
assert gunicorn_attributes['timeout'] == DEFAULT_GUNICORN_TIMEOUT
29-
assert gunicorn_attributes['extra_args'] == ""
30-
assert attributes['celery'] == CELERY_DEFAULT_CONFIG
20+
assert gunicorn_attributes['bind'] == default_settings.gunicorn.bind
21+
assert gunicorn_attributes['workers'] == default_settings.gunicorn.workers
22+
assert gunicorn_attributes['timeout'] == default_settings.gunicorn.timeout
23+
assert gunicorn_attributes['extra_args'] == default_settings.gunicorn.extra_args
24+
assert attributes['celery'] == default_settings.celery.dict()
3125

3226

3327
def test_register_non_default(galaxy_yml, default_config_manager):
@@ -48,7 +42,8 @@ def test_register_non_default(galaxy_yml, default_config_manager):
4842
state = default_config_manager.state['config_files'][str(galaxy_yml)]
4943
gunicorn_attributes = state['attribs']['gunicorn']
5044
assert gunicorn_attributes['bind'] == new_bind
51-
assert gunicorn_attributes['workers'] == DEFAULT_GUNICORN_WORKERS
45+
default_settings = Settings()
46+
assert gunicorn_attributes['workers'] == default_settings.gunicorn.workers
5247
celery_attributes = state['attribs']['celery']
5348
assert celery_attributes['concurrency'] == concurrency
5449

tests/test_operations.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ def wait_for_gxit_proxy(state_dir):
4242
startup_logs = ""
4343
with open(state_dir / "log" / 'gx-it-proxy.log') as fh:
4444
for _ in range(STARTUP_TIMEOUT * 4):
45-
startup_logs = fh.read()
46-
if 'Listening' in startup_logs:
45+
startup_logs = f"{startup_logs}{fh.read()}"
46+
if 'Watching path' in startup_logs:
4747
return True
4848
time.sleep(0.25)
4949
return startup_logs

tests/test_settings.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import json
2+
3+
from gravity.settings import Settings
4+
5+
6+
def test_schema_json():
7+
schema = Settings.schema_json(indent=2)
8+
assert "Configuration for gravity process manager" in json.loads(schema)['description']
9+
10+
11+
def test_defaults_loaded():
12+
settings = Settings()
13+
assert settings.gunicorn.bind == 'localhost:8080'
14+
15+
16+
def test_defaults_override_constructor():
17+
settings = Settings(**{'gunicorn': {'bind': 'localhost:8081'}})
18+
assert settings.gunicorn.bind == 'localhost:8081'
19+
20+
21+
def test_defaults_override_env_var(monkeypatch):
22+
monkeypatch.setenv("GRAVITY_GUNICORN.BIND", "localhost:8081")
23+
settings = Settings()
24+
assert settings.gunicorn.bind == 'localhost:8081'

0 commit comments

Comments
 (0)