Skip to content

Commit

Permalink
WIP: modern tool shed frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Sep 26, 2023
1 parent f311349 commit 7131b99
Show file tree
Hide file tree
Showing 135 changed files with 10,784 additions and 228 deletions.
25 changes: 17 additions & 8 deletions .github/workflows/toolshed.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,23 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.7']
test-install-client: ['standalone', 'galaxy_api']
# v1 is mostly working...
shed-api: ['v1']
# lets get twill working with twill then try to
# make progress on the playwright
# shed-browser: ['twill', 'playwright']
shed-browser: ['playwright']
include:
- test-install-client: 'galaxy_api'
python-version: '3.7'
shed-api: 'v1'
shed-browser: 'twill'
- test-install-client: 'standalone'
python-version: '3.8'
shed-api: 'v1'
shed-browser: 'twill'
- test-install-client: 'galaxy_api'
python-version: '3.9'
shed-api: 'v2'
shed-browser: 'playwright'
- test-install-client: 'standalone'
python-version: '3.10'
shed-api: 'v2'
shed-browser: 'playwright'
services:
postgres:
image: postgres:13
Expand Down
41 changes: 41 additions & 0 deletions .vscode/shed.code-snippets
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"shedcomp": {
"prefix": "shed_component",
"body": [
"<script setup lang=\"ts\">",
"\t$0",
"</script>",
"<template>",
"</template>"
],
"description": "outline of a tool shed component"
},
"shedpage": {
"prefix": "shed_page",
"body": [
"<script setup lang=\"ts\">",
"import PageContainer from \"@/components/PageContainer.vue\"",
"</script>",
"<template>",
" <page-container>",
" $0",
" </page-container>",
"</template>"
],
"description": "outline of a tool shed page"
},
"shedfetcher": {
"prefix": "shed_fetcher",
"body": [
"import { fetcher } from \"@/schema\"",
"const fetcher = fetcher.path(\"$1\").method(\"get\").create()"
],
"description": "Import shed fetcher and instantiate with a path"
},
"shedrouter": {
"prefix": "shed_router",
"body": [
"import router from \"@/router\""
]
}
}
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ remove-api-schema:

update-client-api-schema: client-node-deps build-api-schema
$(IN_VENV) cd client && node openapi_to_schema.mjs ../_schema.yaml > src/schema/schema.ts && npx prettier --write src/schema/schema.ts
$(IN_VENV) cd client && node openapi_to_schema.mjs ../_shed_schema.yaml > ../lib/tool_shed/webapp/frontend/src/schema/schema.ts && npx prettier --write ../lib/tool_shed/webapp/frontend/src/schema/schema.ts
$(MAKE) remove-api-schema

lint-api-schema: build-api-schema
Expand Down
2 changes: 2 additions & 0 deletions lib/galaxy/dependencies/pinned-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ fsspec==2023.1.0 ; python_version >= "3.7" and python_version < "3.12"
future==0.18.3 ; python_version >= "3.7" and python_version < "3.12"
galaxy-sequence-utils==1.1.5 ; python_version >= "3.7" and python_version < "3.12"
galaxy2cwl==0.1.4 ; python_version >= "3.7" and python_version < "3.12"
graphene-sqlalchemy==3.0.0b3 ; python_version >= "3.7" and python_version < "3.12"
gravity==1.0.3 ; python_version >= "3.7" and python_version < "3.12"
greenlet==2.0.2 ; python_version >= "3.7" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and python_version < "3.12"
gunicorn==21.2.0 ; python_version >= "3.7" and python_version < "3.12"
Expand Down Expand Up @@ -179,6 +180,7 @@ sqlalchemy==1.4.49 ; python_version >= "3.7" and python_version < "3.12"
sqlitedict==2.1.0 ; python_version >= "3.7" and python_version < "3.12"
sqlparse==0.4.4 ; python_version >= "3.7" and python_version < "3.12"
starlette-context==0.3.5 ; python_version >= "3.7" and python_version < "3.12"
starlette_graphene3==0.6.0 ; python_version >= "3.7" and python_version < "3.12"
starlette==0.27.0 ; python_version >= "3.7" and python_version < "3.12"
supervisor==4.2.5 ; python_version >= "3.7" and python_version < "3.12"
svgwrite==1.4.3 ; python_version >= "3.7" and python_version < "3.12"
Expand Down
15 changes: 10 additions & 5 deletions lib/galaxy/managers/api_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@
TYPE_CHECKING,
)

from galaxy.model import User
from typing_extensions import Protocol

from galaxy.model.base import transaction
from galaxy.structured_app import BasicSharedApp

if TYPE_CHECKING:
from galaxy.model import APIKeys


class IsUserModel(Protocol):
id: str


class ApiKeyManager:
def __init__(self, app: BasicSharedApp):
self.app = app

def get_api_key(self, user: User) -> Optional["APIKeys"]:
def get_api_key(self, user: IsUserModel) -> Optional["APIKeys"]:
sa_session = self.app.model.context
api_key = (
sa_session.query(self.app.model.APIKeys)
Expand All @@ -25,7 +30,7 @@ def get_api_key(self, user: User) -> Optional["APIKeys"]:
)
return api_key

def create_api_key(self, user: User) -> "APIKeys":
def create_api_key(self, user: IsUserModel) -> "APIKeys":
guid = self.app.security.get_new_guid()
new_key = self.app.model.APIKeys()
new_key.user_id = user.id
Expand All @@ -36,15 +41,15 @@ def create_api_key(self, user: User) -> "APIKeys":
sa_session.commit()
return new_key

def get_or_create_api_key(self, user: User) -> str:
def get_or_create_api_key(self, user: IsUserModel) -> str:
# Logic Galaxy has always used - but it would appear to have a race
# condition. Worth fixing? Would kind of need a message queue to fix
# in multiple process mode.
api_key = self.get_api_key(user)
key = api_key.key if api_key else self.create_api_key(user).key
return key

def delete_api_key(self, user: User) -> None:
def delete_api_key(self, user: IsUserModel) -> None:
"""Marks the current user API key as deleted."""
sa_session = self.app.model.context
# Before it was possible to create multiple API keys for the same user although they were not considered valid
Expand Down
4 changes: 3 additions & 1 deletion lib/galaxy/managers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,9 @@ def change_password(self, trans, password=None, confirm=None, token=None, id=Non
trans.sa_session.add(token_result)
return user, "Password has been changed. Token has been invalidated."
else:
user = self.by_id(self.app.security.decode_id(id))
if not isinstance(id, int):
id = self.app.security.decode_id(id)
user = self.by_id(id)
if user:
message = self.app.auth_manager.check_change_password(user, current, trans.request)
if message:
Expand Down
41 changes: 27 additions & 14 deletions lib/galaxy/webapps/base/webapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -735,21 +735,9 @@ def __create_new_session(self, prev_galaxy_session=None, user_for_new_session=No
Caller is responsible for flushing the returned session.
"""
session_key = self.security.get_new_guid()
galaxy_session = self.app.model.GalaxySession(
session_key=session_key,
is_valid=True,
remote_host=self.request.remote_host,
remote_addr=self.request.remote_addr,
referer=self.request.headers.get("Referer", None),
return create_new_session(
self, prev_galaxy_session=prev_galaxy_session, user_for_new_session=user_for_new_session
)
if prev_galaxy_session:
# Invalidated an existing session for some reason, keep track
galaxy_session.prev_session_id = prev_galaxy_session.id
if user_for_new_session:
# The new session should be associated with the user
galaxy_session.user = user_for_new_session
return galaxy_session

@property
def cookie_path(self):
Expand Down Expand Up @@ -1106,6 +1094,31 @@ def qualified_url_for_path(self, path):
return url_for(path, qualified=True)


def create_new_session(trans, prev_galaxy_session=None, user_for_new_session=None):
"""
Create a new GalaxySession for this request, possibly with a connection
to a previous session (in `prev_galaxy_session`) and an existing user
(in `user_for_new_session`).
Caller is responsible for flushing the returned session.
"""
session_key = trans.security.get_new_guid()
galaxy_session = trans.app.model.GalaxySession(
session_key=session_key,
is_valid=True,
remote_host=trans.request.remote_host,
remote_addr=trans.request.remote_addr,
referer=trans.request.headers.get("Referer", None),
)
if prev_galaxy_session:
# Invalidated an existing session for some reason, keep track
galaxy_session.prev_session_id = prev_galaxy_session.id
if user_for_new_session:
# The new session should be associated with the user
galaxy_session.user = user_for_new_session
return galaxy_session


def default_url_path(path):
return os.path.abspath(os.path.join(os.path.dirname(__file__), path))

Expand Down
50 changes: 50 additions & 0 deletions lib/galaxy/webapps/galaxy/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
NoMatchFound,
)
from starlette.types import Scope
from typing_extensions import Literal

try:
from starlette_context import context as request_context
Expand Down Expand Up @@ -200,6 +201,8 @@ class GalaxyASGIRequest(GalaxyAbstractRequest):
Implements the GalaxyAbstractRequest interface to provide access to some properties
of the request commonly used."""

__request: Request

def __init__(self, request: Request):
self.__request = request
self.__environ: Optional[MutableMapping[str, Any]] = None
Expand Down Expand Up @@ -232,6 +235,28 @@ def environ(self) -> MutableMapping[str, Any]:
self.__environ = build_environ(self.__request.scope, None) # type: ignore[arg-type]
return self.__environ

@property
def headers(self):
return self.__request.headers

@property
def remote_host(self) -> str:
# was available in wsgi and is used create_new_session
return self.host

@property
def remote_addr(self) -> Optional[str]:
# was available in wsgi and is used create_new_session
# not sure what to do here...
return None

@property
def is_secure(self) -> bool:
return self.__request.url.scheme == "https"

def get_cookie(self, name):
return self.__request.cookies.get(name)


class GalaxyASGIResponse(GalaxyAbstractResponse):
"""Wrapper around Starlette/FastAPI Response object.
Expand All @@ -246,6 +271,31 @@ def __init__(self, response: Response):
def headers(self):
return self.__response.headers

def set_cookie(
self,
key: str,
value: str = "",
max_age: Optional[int] = None,
expires: Optional[int] = None,
path: str = "/",
domain: Optional[str] = None,
secure: bool = False,
httponly: bool = False,
samesite: Optional[Literal["lax", "strict", "none"]] = "lax",
) -> None:
"""Set a cookie."""
self.__response.set_cookie(
key,
value,
max_age=max_age,
expires=expires,
path=path,
domain=domain,
secure=secure,
httponly=httponly,
samesite=samesite,
)


DependsOnUser = cast(Optional[User], Depends(get_user))

Expand Down
2 changes: 1 addition & 1 deletion lib/galaxy/webapps/galaxy/controllers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def __validate_login(self, trans, payload=None, **kwd):
message, status = self.resend_activation_email(trans, user.email, user.username)
return self.message_exception(trans, message, sanitize=False)
else: # activation is OFF
pw_expires = trans.app.config.password_expiration_period
pw_expires = getattr(trans.app.config, "password_expiration_period", None)
if pw_expires and user.last_password_change < datetime.today() - pw_expires:
# Password is expired, we don't log them in.
return {
Expand Down
25 changes: 25 additions & 0 deletions lib/galaxy/work/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
Optional,
)

from typing_extensions import Literal

from galaxy.managers.context import ProvidesHistoryContext
from galaxy.model import (
GalaxySession,
Expand Down Expand Up @@ -85,6 +87,14 @@ def base(self) -> str:
def host(self) -> str:
"""The host address."""

@abc.abstractproperty
def is_secure(self) -> bool:
"""Was this a secure (https) request."""

@abc.abstractmethod
def get_cookie(self, name):
"""Return cookie."""


class GalaxyAbstractResponse:
"""Abstract interface to provide access to some response utilities."""
Expand All @@ -102,6 +112,21 @@ def set_content_type(self, content_type: str):
def get_content_type(self):
return self.headers.get("content-type", None)

@abc.abstractmethod
def set_cookie(
self,
key: str,
value: str = "",
max_age: Optional[int] = None,
expires: Optional[int] = None,
path: str = "/",
domain: Optional[str] = None,
secure: bool = False,
httponly: bool = False,
samesite: Optional[Literal["lax", "strict", "none"]] = "lax",
) -> None:
"""Set a cookie."""


class SessionRequestContext(WorkRequestContext):
"""Like WorkRequestContext, but provides access to request."""
Expand Down
Loading

0 comments on commit 7131b99

Please sign in to comment.