This commit is contained in:
paperbenni
2026-06-15 17:20:14 +02:00
52 changed files with 3006 additions and 1473 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ ecosystem.
## Mount Functions
In what most people think of as a "standard" Vue application there would be only one place that Vue
is mounted to the HTML environment, and that would be in a main.j or an app.js. Most modern
is mounted to the HTML environment, and that would be in a main.js or an app.js. Most modern
single-page applications only have one starting point like that.
However, we are incrementally replacing old Backbone views, so in its current state, Galaxy may have
+1 -1
View File
@@ -10,7 +10,7 @@ Heading Levels are used by screen readers, and other software, to get a rough id
- When increasing a heading level, do not skip Levels.
- Do not use a headings level to determine it's size. Use one of the heading utility classes (e.g. `h-lg`) instead.
- Do not use a headings level to determine its size. Use one of the heading utility classes (e.g. `h-lg`) instead.
- Do not use a heading tag to make a non-heading text large. Use a heading utility class on a `<span>` instead.
+3 -3
View File
@@ -3,7 +3,7 @@
---
We are using components in two very distinct ways. The first, "normal", kind of component will
probably look familiar to anybody whis is already passingly familiar with Vue. Here the relevant
probably look familiar to anybody who is already passingly familiar with Vue. Here the relevant
information comes in as properties, any internal variables get defined in "data", changes go out as
events.
@@ -18,7 +18,7 @@ events.
In this example, we've created a component whose job is to deal with loading and updating the doodad
object. Notice that there is no markup inside the DoodadProvider other than the explicit renderless
component we previously made, but you are free to putput whatever you want in there, accessing the
component we previously made, but you are free to put whatever you want in there, accessing the
doodad and saveDoddad properties as desired, as well as any other local data with the only
restriction that Vue needs a single root element in which to render.
@@ -57,7 +57,7 @@ update:propname event syntax whenever possible [to facilitate .sync
binds](https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)).
Whatever happens to that new object is somebody else's job. As soon as you tie the data management
to the rendering, the re-usability of your components craters.
to the rendering, the re-usability of your components crater.
## The Provider
+2 -2
View File
@@ -231,7 +231,7 @@ Add spaces between the `script`, `template` and `style` blocks.
> </template>
>
> <style lang="scss" scoped>
> // stlye stuff...
> // style stuff...
> </style>
> ```
>
@@ -245,7 +245,7 @@ Add spaces between the `script`, `template` and `style` blocks.
> <!--template stuff-->
> </template>
> <style lang="scss" scoped>
> // stlye stuff...
> // style stuff...
> </style>
> ```
+9 -10
View File
@@ -72,13 +72,13 @@
"d3-zoom": "^3.0.0",
"d3v3": "npm:d3@3",
"date-fns": "^2.30.0",
"decode-uri-component": "^0.2.1",
"decode-uri-component": "^0.4.1",
"dexie": "^3.2.5",
"diff": "^8.0.3",
"dom-to-image": "^2.6.0",
"dompurify": "^3.0.6",
"echarts": "^5.5.1",
"elkjs": "^0.8.2",
"elkjs": "^0.11.1",
"events": "^3.3.0",
"file-saver": "^2.0.5",
"flush-promises": "^1.0.2",
@@ -91,7 +91,7 @@
"jquery-migrate": "~1.4",
"jquery-mousewheel": "^3.1.13",
"jquery-ui": "^1.13.2",
"katex": "^0.16.22",
"katex": "^0.17.0",
"linkify-html": "^4.1.1",
"linkifyjs": "^4.1.1",
"lodash": "^4.17.21",
@@ -129,7 +129,7 @@
"vscode-languageserver-types": "^3.17.5",
"vue": "^2.7.16",
"vue-class-component": "^7.2.6",
"vue-demi": "0.13.11",
"vue-demi": "^0.14.10",
"vue-echarts": "^7.0.3",
"vue-infinite-scroll": "^2.0.2",
"vue-json-pretty": "^1.9.5",
@@ -173,11 +173,10 @@
"@types/d3": "^7.4.2",
"@types/d3-selection": "^3.0.11",
"@types/d3-zoom": "^3.0.8",
"@types/dompurify": "^3.0.2",
"@types/jquery": "^3.5.24",
"@types/lodash": "^4.14.200",
"@types/lodash.isequal": "^4.5.7",
"@types/markdown-it": "^13.0.4",
"@types/markdown-it": "^14.1.2",
"@typescript-eslint/eslint-plugin": "^6.8.0",
"@typescript-eslint/parser": "^6.8.0",
"@vitejs/plugin-vue2": "^2.3.4",
@@ -186,10 +185,10 @@
"@vitest/ui": "^4.1.5",
"@vue/test-utils": "^1.3.6",
"@vue/tsconfig": "^0.4.0",
"autoprefixer": "10.4.16",
"autoprefixer": "10.5.0",
"buffer": "^6.0.3",
"cpy-cli": "^5.0.0",
"esbuild": "^0.27.4",
"esbuild": "^0.28.1",
"eslint": "^8.52.0",
"eslint-plugin-compat": "^4.2.0",
"eslint-plugin-import": "^2.28.1",
@@ -198,8 +197,8 @@
"eslint-plugin-vuejs-accessibility": "^2.2.0",
"fake-indexeddb": "^6.0.0",
"fs-extra": "^11.2.0",
"happy-dom": "^20.0.10",
"msw": "^2.3.4",
"happy-dom": "20.6.2",
"msw": "~2.10.5",
"openapi-msw": "^0.7.0",
"openapi-typescript": "^7.3.0",
"prettier": "^3.6.2",
@@ -13092,6 +13092,7 @@ export interface components {
| "dataverse"
| "huggingface"
| "iiif"
| "mavedb"
| "omero"
| "ssh";
/** Variables */
@@ -19995,6 +19996,12 @@ export interface components {
PageRevisionDetails: {
/** Content */
content?: string | null;
/**
* Content for Editor
* @description Raw text contents of the last page revision (type dependent on content_format).
* @default
*/
content_editor: string | null;
content_format?: components["schemas"]["PageContentFormat"] | null;
/**
* Create Time
@@ -25381,6 +25388,7 @@ export interface components {
| "dataverse"
| "huggingface"
| "iiif"
| "mavedb"
| "omero"
| "ssh";
/** Uri Root */
+2101 -1396
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -32,6 +32,10 @@ export const templateTypes: FileSourceTypesDetail = {
icon: faGoogleDrive,
message: "This is a repository plugin that connects with the commercial Google Drive service.",
},
mavedb: {
icon: faNetworkWired,
message: "This is a repository plugin that connects with MaveDB score set files.",
},
onedrive: {
icon: faCloud,
message: "This is a repository plugin that connects with Microsoft OneDrive.",
@@ -16,6 +16,7 @@ const REVISION: PageRevisionDetails = {
update_time: "2025-01-01T00:00:00Z",
title: "Test Revision",
content: "# Title\nOld line 1\nOld line 2",
content_editor: "# Title\nOld line 1\nOld line 2",
content_format: "markdown",
};
+1 -1
View File
@@ -1,5 +1,5 @@
import MarkdownIt from "markdown-it";
import type Token from "markdown-it/lib/token";
import type Token from "markdown-it/lib/token.mjs";
import { readonly } from "vue";
/**
+1 -1
View File
@@ -35,7 +35,7 @@ These modules must be enabled in the Apache config. The main proxy directives, `
Additionally, these directions are written for Apache 2.4+. Apache 2.4 for EL 6 can be obtained from the [CentOS SCLo
SIG Repo][sclo-sig-repo]. Otherwise, your system package manager's version of Apache should be suitable.
Ensure that the `mod_headers`, `mod_rewrite` and `mod_proxy`modules are loaded. Although not
Ensure that the `mod_headers`, `mod_rewrite` and `mod_proxy` modules are loaded. Although not
required, the configuration examples also use `mod_deflate` and `mod_expires` for increased client/server performance,
so these should also be enabled.
+1 -1
View File
@@ -34,7 +34,7 @@ Currently, we provide two variants of the LDAP authenticator namely, `ldap` and
`ldap` is based on the [python-ldap](https://www.python-ldap.org/) module which is a wrapper around the
OpenLDAP's client library `libldap`. Currently, `python-ldap` does not provide pre-built Python wheel packages and hence, the OpenLDAP client libraries are needed to install `python-ldap` on the Galaxy server.
On the otherhand, `ldap3` is a pure-Python implementation of the OpenLDAP client library and has no external dependencies. This package can be installed out-of-the-box, so we recommend to use it when the OpenLDAP client libraries are not available on the Galaxy server.
On the other hand, `ldap3` is a pure-Python implementation of the OpenLDAP client library and has no external dependencies. This package can be installed out-of-the-box, so we recommend to use it when the OpenLDAP client libraries are not available on the Galaxy server.
These same mechanisms can also be configured by proxies serving Galaxy (e.g. nginx or Apache), but configuring them
within Galaxy allows users to use the Galaxy UI for logging in instead of relying on a proxy.
+3 -3
View File
@@ -1,7 +1,7 @@
# Carbon Emissions Reporting
Galaxy can estimate the carbon footprint of each job you run. The carbon emissions reporting can be
configured for each galaxy instance to allow for more accurate results. The following options can
configured for each Galaxy instance to allow for more accurate results. The following options can
be set:
* [Geographical server location](#geographical-server-location)
@@ -15,7 +15,7 @@ United States [Environmental Protection Agency (EPA)](https://www.epa.gov/energy
## Geographical server location
The `geographical_server_location_code` flag is an [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2) string specifying the
location of your galaxy instance, with the only exception being `GLOBAL`, which specifies that global average values are to be used.
location of your Galaxy instance, with the only exception being `GLOBAL`, which specifies that global average values are to be used.
Each valid code corresponds to a location name and a specific carbon intensity value. The corresponding flags
(`geographical_server_location_name` and `carbon_intensity`) are automatically set and will be displayed to the client when carbon
emissions are reported. The more accurate the location the better the estimates. In all cases, it is assumed that all jobs are run on
@@ -25,7 +25,7 @@ a server in the same location as the one specified in `geographical_server_locat
## Power usage effectiveness
[Power usage effectiveness](https://en.wikipedia.org/wiki/Power_usage_effectiveness), or PUE for short, is a ratio specifying
how efficiently the data centre hosting your galaxy server used energy. It is essentially your server's 'indirect' energy usage.
how efficiently the data centre hosting your Galaxy server used energy. It is essentially your server's 'indirect' energy usage.
Galaxy uses a default PUE of `1.67`. If you would like to set a custom value, you can [calculate a PUE value as follows](https://en.wikipedia.org/wiki/Power_usage_effectiveness).
## Feature toggling
+4 -4
View File
@@ -113,7 +113,7 @@ galaxy_server% export DRMAA_LIBRARY_PATH=/galaxy/sge/lib/lx24-amd64/libdrmaa.so
#### DRM Notes
**Limitations**: The DRMAA runner does not work if Galaxy is configured to run jobs as real user, because in this setting jobs are submitted with an external script, i.e. in an extra DRMAA session, and the session based (python) DRMAA library can only query jobs within the session in which started them. Furthermore, the DRMAA job runner only distinguishes successful and failed jobs and ignores information about possible failure sources, e.g. runtime / memory violation, which could be used for job resubmission. Specialized job runners are abvailable that are not affected by these limitations, e.g. univa and slurm runners.
**Limitations**: The DRMAA runner does not work if Galaxy is configured to run jobs as real user, because in this setting jobs are submitted with an external script, i.e. in an extra DRMAA session, and the session based (python) DRMAA library can only query jobs within the session in which started them. Furthermore, the DRMAA job runner only distinguishes successful and failed jobs and ignores information about possible failure sources, e.g. runtime / memory violation, which could be used for job resubmission. Specialized job runners are available that are not affected by these limitations, e.g. univa and slurm runners.
**TORQUE**: The DRMAA runner can also be used (instead of the [PBS](#pbs) runner) to submit jobs to TORQUE, however, problems have been reported when using the `libdrmaa.so` provided with TORQUE. Using this library will result in a segmentation fault when the drmaa runner attempts to write the job template, and any native job runner options will not be passed to the DRM. Instead, you should compile the [pbs-drmaa](https://apps.man.poznan.pl/trac/pbs-drmaa/wiki) library and use this as the value for `$DRMAA_LIBRARY_PATH`.
@@ -456,7 +456,7 @@ The `RemoteShell` plugin uses `rsh(1)` to connect to a remote system and execute
: Remote system hostname to log in to.
``shell_rsh``
: ``rsh``-like command to excute (e.g. ``<param id="shell_rsh">/opt/example/bin/remsh</param>``) - just defaults to ``rst``.
: ``rsh``-like command to execute (e.g. ``<param id="shell_rsh">/opt/example/bin/remsh</param>``) - just defaults to ``rst``.
The `RemoteShell` parameters translate to a command line of `% <shell_rsh> [-l <shell_username>] <shell_hostname> "<remote_command_with_args>"`, where the inclusion of `-l` is dependent on whether `shell_username` is set. Alternate values for `shell_rsh` must be compatible with this syntax.
@@ -571,7 +571,7 @@ Since this is a complex problem, the current solution does have some caveats:
* All of the datasets stored in Galaxy will have to be readable on the underlying filesystem by all Galaxy users. Said users need not have direct access to any systems which mount these filesystems, only the ability to run jobs on clusters that mount them. But I expect that in most environments, users will have the ability to submit jobs to these clusters or log in to these clusters outside of Galaxy, so this will be a security concern to evaluate for most environments.
* *Technical details* - Since Galaxy maintains dataset sharing internally and all files are owned by the Galaxy user, when running jobs only under a single user, permissions can be set such that only the Galaxy user can read all datasets. Since the dataset may be shared by multiple users, it is not suitable to simply change ownership of inputs before a job runs (what if another user tried to use the same dataset as an input during this time?). This could possibly be solved if Galaxy had tight control over extended ACLs on the file, but since many different ACL schemes exist, Galaxy would need a module for each scheme to be supported.
* The real user system works by changing ownership of the job's working directory to the system prior to running the job, and back to the Galaxy user once the job has completed. It does this by executing a site-customizable script via [sudo](https://www.sudo.ws/).
* Two possibilities to determine the system user that corresponds to a galaxy user are implemented: i) the user whos name matches the Galaxy user's email address (with the @domain stripped off) and ii) the user whos name is equal to the galaxy user name. Until release 17.05 only the former option is available. The latter option is suitable for Galaxy installations that user external authentification (e.g. LDAP) against a source that is also the source of the system users.
* Two possibilities to determine the system user that corresponds to a galaxy user are implemented: i) the user whose name matches the Galaxy user's email address (with the @domain stripped off) and ii) the user whos name is equal to the galaxy user name. Until release 17.05 only the former option is available. The latter option is suitable for Galaxy installations that user external authentification (e.g. LDAP) against a source that is also the source of the system users.
* The script accepts a path and does nothing to ensure that this path is a Galaxy working directory per default (and not at all up to release 17.05). So anyone who has access to the Galaxy user could use this script and sudo to change the ownership of any file or directory. Furthermore, anyone with write access to the script could introduce arbitrary (harmful) code -- so it might be a good idea to give write access only to trustworthy users, e.g., root.
### Configuration
@@ -605,7 +605,7 @@ For Galaxy releases > 17.05, the sudo call has been moved to `galaxy.yml` and is
drmaa_external_runjob_script: sudo -E .venv/bin/python scripts/drmaa_external_runner.py --assign_all_groups
```
Also for Galaxy releases > 17.05: In order to allow `external_chown_script.py` to chown only path below certain entry points the variable `ALLOWED_PATHS` in the python script can be adapted. It is sufficient to include the directorries `job_working_directory` and `new_file_path` as configured in `galaxy.yml`.
Also for Galaxy releases > 17.05: In order to allow `external_chown_script.py` to chown only path below certain entry points the variable `ALLOWED_PATHS` in the python script can be adapted. It is sufficient to include the directories `job_working_directory` and `new_file_path` as configured in `galaxy.yml`.
It is also a good idea to make sure that only trusted users, e.g. root, have write access to all three scripts.
+1 -1
View File
@@ -4,7 +4,7 @@
Conda for Tool Dependencies
===========================
.. note:: This document describes configuring Galaxy using YAML based configuraiton
.. note:: This document describes configuring Galaxy using YAML based configuration
options. For Galaxy instances before version 18.01, `this variant
<https://docs.galaxyproject.org/en/release_17.09/admin/conda_faq.html>`__ of this
document will be more directly relatable.
+1 -1
View File
@@ -64,7 +64,7 @@ Additional configuration files and their purposes are:
internally.
- ``build_sites.yml``: Controls which display applications are available and their configuration paths
- ``containers_conf.yml``: Configures the beta Galaxy containers interface, currently only used by Galaxy Interactive
Environments, and only neccesary for Docker Swarm support.
Environments, and only necessary for Docker Swarm support.
- ``dependency_resolvers_conf.xml``: Describes how Galaxy tools (which are typically just descriptions of how to run a
particular command line tool) should locate their dependencies (the command line tool) that are not part of the tool.
See the `Dependency Resolvers documentation <dependency_resolvers>` for more.
+1 -1
View File
@@ -364,7 +364,7 @@ admins might want to control.
.. note::
For the execution of jobs Galaxy already implement the `support for using
For the execution of jobs Galaxy already implements the `support for using
tarballs of container images
<https://github.com/galaxyproject/galaxy/blob/c517e805771cc16807dfe675075a13fe6343f01f/lib/galaxy/tool_util/deps/container_classes.py#L319>`_.
from ``container_image_cache_path`` (set in galaxy.yml) or the destination
+2 -2
View File
@@ -2,7 +2,7 @@
Galaxy has countless ways for users to connect with things that might be considered their "data" - file sources (aka "remote files"), object stores (aka "storage locations"), data libraries, the upload API, visualizations, display applications, custom tools, etc...
This document is going to discuss two of these (file sources and object stores) that are most important Galaxy administrators and how to build Galaxy configurations that allow administrators to let users tie into various pieces of infrastructure (local and publicly available).
This document is going to discuss two of these (file sources and object stores) that are most important to Galaxy administrators and how to build Galaxy configurations that allow administrators to let users tie into various pieces of infrastructure (local and publicly available).
```{contents} Table of Contents
:depth: 4
@@ -17,7 +17,7 @@ Galaxy object stores (called "storage locations" in the UI) store datasets and g
Some of Galaxy's most updated and complete administrator documentation can be found in configuration sample files - this is definitely the case for object stores and file sources. The relevant sample configuration files include [file_sources_conf.yml.sample](https://github.com/galaxyproject/galaxy/blob/dev/lib/galaxy/config/sample/file_sources_conf.yml.sample) and [object_store_conf.sample.yml](https://github.com/galaxyproject/galaxy/blob/dev/lib/galaxy/config/sample/object_store_conf.sample.yml).
File sources and object stores configured with the above files essentially are available to all users of your Galaxy instance - hence this document describes them as "global" file sources and object stores. File source configurations do allow some templating that does allow the a global file source to be materialized differently for different users. For instance, you as an admin may setup a Dropbox file source and may explicitly add custom user properties that allow that single Dropbox file source to read from a user's preferences. Since there is just one Dropbox service and most people only have a single Dropbox account, this use case can be somewhat adequately addressed by the global file source and the global user preferences file. For a use case like Amazon S3 buckets though for instance, a single bucket file source that is parameterized one way is probably more clearly inadequate. For instance, users would very likely want to attach different buckets for different projects. Additionally, the Galaxy user interface doesn't tie the user preferences to the particular file source and so this method introduces a huge education burden on your Galaxy instance. Finally, the templating available to file sources are not available for object stores - and allowing users to describe how they would like datasets stored and to pay for their own dataset storage are important use cases.
File sources and object stores configured with the above files essentially are available to all users of your Galaxy instance - hence this document describes them as "global" file sources and object stores. File source configurations do allow some templating that does allow a global file source to be materialized differently for different users. For instance, you as an admin may setup a Dropbox file source and may explicitly add custom user properties that allow that single Dropbox file source to read from a user's preferences. Since there is just one Dropbox service and most people only have a single Dropbox account, this use case can be somewhat adequately addressed by the global file source and the global user preferences file. For a use case like Amazon S3 buckets though for instance, a single bucket file source that is parameterized one way is probably more clearly inadequate. For instance, users would very likely want to attach different buckets for different projects. Additionally, the Galaxy user interface doesn't tie the user preferences to the particular file source and so this method introduces a huge education burden on your Galaxy instance. Finally, the templating available to file sources are not available for object stores - and allowing users to describe how they would like datasets stored and to pay for their own dataset storage are important use cases.
This document is going to describe Galaxy configuration template libraries that allow the
administrator to setup templates for file sources and object stores that your users may instantiate
+1 -1
View File
@@ -16,7 +16,7 @@ In order to understand the naming and structure of these three components it mig
to look in the history. Tool data was organized in tabular `loc` that contained metadata and paths
of the data. Those files were installed with the tool and could be accessed with the
[`from_file`](https://docs.galaxyproject.org/en/master/dev/schema.html#from-file) mechanism from tools.
Since each tool version had it's own `loc` file the maintenance was difficult. With tool data tables
Since each tool version had its own `loc` file the maintenance was difficult. With tool data tables
an additional abstraction layer was introduced that is used from tools via
[`from_datatable`](https://docs.galaxyproject.org/en/master/dev/schema.html#from-data-table).
+1 -1
View File
@@ -146,7 +146,7 @@ Galaxy's data model includes the [galaxy model](https://github.com/galaxyproject
These two models may be persisted in one combined database (which is the default) or two separate databases (which is enabled by setting the
[`install_database_connection`](https://github.com/galaxyproject/galaxy/blob/dev/lib/galaxy/webapps/galaxy/config_schema.yml#L157) configuration option).
These models are represented by migration [branches](https://alembic.sqlalchemy.org/en/latest/branches.html) (versioning lineages with a common base) labeled as *gxy* for the galaxy model and *tsi* for the install model. If both models are hosted in the same databases, the branches will share the same Alembic version table; otherwise, each database has its own version table.
These models are represented by migration [branches](https://alembic.sqlalchemy.org/en/latest/branches.html) (versioning lineages with a common base) labeled as *gxy* for the galaxy model and *tsi* for the install model. If both models are hosted in the same database, the branches will share the same Alembic version table; otherwise, each database has its own version table.
Each branch has its own version history, represented by revision scripts located in the branch version directory (`migrations/alembic/versions_gxy` for *gxy* and `migrations/alembic/versions_tsi` for *tsi*).
+3 -3
View File
@@ -294,7 +294,7 @@ mapping_files
skip_availability_check
Set to ``true`` to skip filtering module dependencies using ``module avail`` and attempt to load each
module listed in the depedencies (default: ``false``).
module listed in the dependencies (default: ``false``).
This setting should be used when during dependency resolution modules are not listed in ``module avail``,
but can be expected to be present when using ``module load`` in the job script, such as child modules
in hierarchical module systems. Note: the order in which dependencies are listed could be important.
@@ -342,7 +342,7 @@ default_indicator
skip_availability_check
Set to ``true`` to skip filtering module dependencies using ``module avail`` and attempt to load each
module listed in the depedencies (default: ``false``).
module listed in the dependencies (default: ``false``).
This setting should be used when during dependency resolution modules are not listed in ``module avail``,
but can be expected to be present when using ``module load`` in the job script, such as child modules
in hierarchical module systems. Note: the order in which dependencies are listed could be important.
@@ -376,6 +376,6 @@ It is highly experimental, undocumented, and unmaintained, and likely to be drop
Brewed Tool Shed Package Resolver
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``brewed_tool_shed`` dependency resolver was an attmept to resolve tool shed packages that had been auto converted
The ``brewed_tool_shed`` dependency resolver was an attempt to resolve tool shed packages that had been auto converted
to the tool shed. It is highly experimental, undocumented, unmaintained, and will almost certainly be removed from the
code base.
+4 -4
View File
@@ -697,7 +697,7 @@
:Description:
Location of files available for a short time as downloads (short
term storage). This directory is exclusively used for serving
dynamically generated downloadable content. Galaxy may uses the
dynamically generated downloadable content. Galaxy may use the
new_file_path parameter as a general temporary directory and that
directory should be monitored by a tool such as tmpwatch in
production environments. short_term_storage_dir on the other hand
@@ -1008,7 +1008,7 @@
:Description:
XML config file that contains data table entries for the
ToolDataTableManager. This file is manually # maintained by the
ToolDataTableManager. This file is manually maintained by the
Galaxy administrator (.sample used if default does not exist).
The value of this option will be resolved with respect to
<config_dir>.
@@ -1288,7 +1288,7 @@
destination level for heterogeneous clusters. conda job resolution
requires bash or zsh so if this is switched to /bin/sh for
instance - conda resolution should be disabled. Containerized jobs
always use /bin/sh - so more maximum portability tool authors
always use /bin/sh - for maximum portability tool authors
should assume generated commands run in sh.
:Default: ``/bin/bash``
:Type: str
@@ -1323,7 +1323,7 @@
:Description:
Set this to true to attempt to resolve bio.tools metadata for
tools for tool not resovled via biotools_content_directory.
tools for tool not resolved via biotools_content_directory.
:Default: ``false``
:Type: bool
+1 -1
View File
@@ -279,7 +279,7 @@ def ncbi_blastn_wrapper(app, user_email):
```
The first example above delegates to the PBS job runner and allocates extra walltime for larger input files (based on tool input parameter named `query`). The second example delegates to the DRMAA job runner and assigns users in the in the admin list to a special project (perhaps configured to have a higher priority or extended walltime).
The first example above delegates to the PBS job runner and allocates extra walltime for larger input files (based on tool input parameter named `query`). The second example delegates to the DRMAA job runner and assigns users in the admin list to a special project (perhaps configured to have a higher priority or extended walltime).
The above examples demonstrate that the dynamic job destination framework will pass in the arguments to your function that are needed based on the argument names. The valid argument names at this time are:
+1 -1
View File
@@ -7,7 +7,7 @@ We removed support for running Galaxy under uWSGI in Galaxy release 22.05.
We made this move because uWSGI support for newer Python versions has been problematic and because
we have modernized the API portion of our backend. The Galaxy web application is now an ASGI application
driven by [FastAPI](https://fastapi.tiangolo.com/) and [Starlette](https://www.starlette.io/).
This enables numerous important features that were not be possible (or were more complicated to set up)
This enables numerous important features that were not possible (or were more complicated to set up)
otherwise. Importantly, [FastAPI's documentation](https://fastapi.tiangolo.com/tutorial/) is very good and
should increase developer productivity.
+1 -1
View File
@@ -27,7 +27,7 @@ Instructions for [proxying with Apache](apache.md) are also available.
```{include} _inc_proxy_prereq.md
```
### NGINX Proxy Prerequisities
### NGINX Proxy Prerequisites
If you are **not** planning to use the recommended [tus.io method](#receiving-files-via-the-tus-protocol) to handle file uploads but want to use nginx to handle uploads, you will (most likely) not be able to use your package manager's version of nginx. The [Receiving Files With NGINX](#receiving-files-with-nginx-legacy) section explains this in detail and provides some options for installing *nginx + upload module* packages maintained by the Galaxy Committers Team.
+1 -1
View File
@@ -150,7 +150,7 @@ To get started with setting up local data, please see [Data Integration](https:/
* The `galaxy_dist/tool-data/` directory contains a set of sample location (`<data_label>.loc`) files that describe the metadata and path to local data and indexes.
* Installed tool packages from the [Tool Shed](https://galaxyproject.org/toolshed/) may also include location files.
* Comments in location files explain the expected format.
* [Data Integration](https://galaxyproject.org/admin/data-integration/) explain how to obtain, create, or rysnc many common data and indexes. See an individual Tool Shed repository's documentation for more details.
* [Data Integration](https://galaxyproject.org/admin/data-integration/) explain how to obtain, create, or rsync many common data and indexes. See an individual Tool Shed repository's documentation for more details.
### Enable upload via FTP
+1 -1
View File
@@ -26,7 +26,7 @@ If Galaxy complains about the version of Python you are using:
```sh
% rm -rf /path/to/galaxy/database/compiled_templates/
```
These templated will be regenerated automatically when starting Galaxy.
These templates will be regenerated automatically when starting Galaxy.
5. Start Galaxy again.
+1 -1
View File
@@ -16,7 +16,7 @@
:Description:
Database connection. Galaxy Reports are intended for production
Galaxy instances, so sqlite (and the default value below) is not
supported. An SQLAlchemy connection string should be used specify
supported. An SQLAlchemy connection string should be used to specify
an external database.
:Default: ``sqlite:///./database/universe.sqlite?isolation_level=IMMEDIATE``
:Type: str
+1 -1
View File
@@ -411,7 +411,7 @@ If you omit the `processes` argument this will default to a single process.
You can further customize the handler names using the `name_template` section,
for a complete example see [this gravity test case](https://github.com/galaxyproject/gravity/blob/a00df1c671fdc01e3fbc42f64a851a45a37a1870/tests/test_process_manager.py#L28).
You can define arbitrary environment variables for dynamic handlers using the ``environnment`` key on a handler
You can define arbitrary environment variables for dynamic handlers using the ``environment`` key on a handler
definition:
```yaml
+1 -1
View File
@@ -12,7 +12,7 @@ Galaxy. Thus it is possible that a tool modifies data in Galaxy's ``file_path``.
Examples of such potential changes are:
- Creation of additional files, e.g. indices, which is a problem for cleaning up data, because Galaxy does not know about these files.
- Removal of tool input or output files. This will create problems with other tools using these datasets (note that most tool repositories use CI tests to to avoid this, but the problem may still occur).
- Removal of tool input or output files. This will create problems with other tools using these datasets (note that most tool repositories use CI tests to avoid this, but the problem may still occur).
Note that a tool only knows the paths to its inputs and outputs, but if using the default configuration for other paths (e.g. the configuration directory) also these paths can be calculated and accessed.
+1 -1
View File
@@ -50,7 +50,7 @@ Although any FTP server should work, our public site uses ProFTPD. You'll need
* mod_sql_postgres or mod_sql_mysql
* mod_sql_passwd
We compile by hand using the following configure arguments (OpenSSL is prebuilt and statically linked), you should read the INSTALL file that come with the proftpd source distribution. At least you should consider if you need to use any of these options "install_user=<user> install_group=<group> ./configure --sysconfdir=/etc --localstatedir=/var":
We compile by hand using the following configure arguments (OpenSSL is prebuilt and statically linked), you should read the INSTALL file that comes with the proftpd source distribution. At least you should consider if you need to use any of these options "install_user=<user> install_group=<group> ./configure --sysconfdir=/etc --localstatedir=/var":
```console
./configure --prefix=/foo --disable-auth-file --disable-ncurses --disable-ident --disable-shadow --enable-openssl --with-modules=mod_sql:mod_sql_postgres:mod_sql_passwd --with-includes=/usr/postgres/9.1-pgdg/include:`pwd`/../openssl/.openssl/include --with-libraries=/usr/postgres/9.1-pgdg/lib/64:`pwd`/../openssl/.openssl/lib
@@ -10,7 +10,7 @@ and enabling a Galaxy User to gain access to content inside in real-time.
How Galaxy InteractiveTools work
--------------------------------
A InteractiveTool is defined in the same familiar way as standard Galaxy Tools,
An InteractiveTool is defined in the same familiar way as standard Galaxy Tools,
but are specified with ``tool_type="interactive"``, and providing additional
entry point information:
@@ -202,7 +202,7 @@ An example ``job_conf.yml`` file as seen in ``config/job_conf.yml.interactivetoo
default_destination_id: local
The Galaxy currently contains a sizable collection of **InteractiveTools** directly in the
Galaxy currently contains a sizable collection of **InteractiveTools** directly in the
code base. To be enabled, they need to be commented in or added to the ``config/tool_conf.xml``:
.. code-block:: xml
@@ -213,7 +213,7 @@ code base. To be enabled, they need to be commented in or added to the ``config/
</toolbox>
A InteractiveTool is defined in the same familiar way as standard Galaxy Tools,
An InteractiveTool is defined in the same familiar way as standard Galaxy Tools,
but are specified with ``tool_type="interactive"``, and providing additional
entry point information:
@@ -268,7 +268,7 @@ server within an InteractiveTool container now must serve the contents under a p
2. Absolute links. Unfortunately many relevant services are implemented with absolute links, i.e. starting
at the top-level ``/``. For such InteractiveTools to work with path-based URLs the contained web server
needs to be configured with the path prefix/entry point path under which the content should be served. Two issues then
needs to be considered:
need to be considered:
a. How to inject the path prefix into the InteractiveTool at run-time?
@@ -326,7 +326,7 @@ server within an InteractiveTool container now must serve the contents under a p
When ``requires_path_in_url="True"`` in the ``entry_point`` tag, the InteractiveTool proxy service forwards the HTTP requests
with the full path intact.
Both values of ``requires_path_in_url`` can be combined with both injection mechanisms, leading two four configuration variants
Both values of ``requires_path_in_url`` can be combined with both injection mechanisms, leading to four configuration variants
for path-based InteractiveTools. Choosing the correct one depends on the implementation of the web server contained in the
InteractiveTool and can be a bit tricky to get correct. In some cases, none of these options will work. One solution can then
be to configure another highly customized proxy web server within the InteractiveTool, e.g. using NGINX.
@@ -395,7 +395,7 @@ In both nginx config examples, you might want to replace localhost with your ser
Job runner configuration in production
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
InteractiveTools have been enabled for the Condor, Slurm, Pulsar and Kuberneters job runner.
InteractiveTools have been enabled for the Condor, Slurm, Pulsar and Kubernetes job runner.
A destination configuration for Condor may look like this:
.. code-block:: xml
@@ -18,7 +18,7 @@ Job metrics can be collected for Pulsar jobs - but there is added complexity. Th
See `job_metrics_conf.xml.sample` for the most updated information.
The `core` plugin is one enabled by default and it is a cross-platform plugin used to caputre job run time and core allocation. `env` is the only other cross-platform plugin (should work with any *nix) and can be used to record all environment variables set at job runtime or just specific variables (e.g. `PATH`) - this can potentially be useful for debugging cluster issues.
The `core` plugin is one enabled by default and it is a cross-platform plugin used to capture job run time and core allocation. `env` is the only other cross-platform plugin (should work with any *nix) and can be used to record all environment variables set at job runtime or just specific variables (e.g. `PATH`) - this can potentially be useful for debugging cluster issues.
There are a number of remaining Linux-only plugins - there include `cpuinfo` and `meminfo` to collect CPU and memory about the node the Galaxy job runs on and `uname` to likewise collect host and operating system information.
+2 -2
View File
@@ -5,7 +5,7 @@ Tiny plugin interface to extend the Galaxy client.
Galaxy webhooks provides a simple way of inserting icons, links, or other HTML elements into predefined locations.
For this Galaxy provides some entry points which can be used to extend the client with content. This content
can consists out of simple HTML, JS or dynamically generated content from a python function.
can consist of simple HTML, JS or dynamically generated content from a python function.
Please note that the webhooks interface is new and can change in the coming releases. Consider it as beta as we don't
make promises to keep the API stable at the moment.
@@ -82,7 +82,7 @@ The configuration file is just a .yml (or .yaml) file with a few options. The fo
- **activate** - *true* or *false* - whether show the plugin on a page or not
- **icon** Icon to show (if masthead)
- **tooltip** - Tooltip to show on hover
- **function** - A javascript function to be executed. Any calls to Galaxy APIs should be sure to use Galaxy.root when constructing the URL to ensure compatability across Galaxy deployments.
- **function** - A javascript function to be executed. Any calls to Galaxy APIs should be sure to use Galaxy.root when constructing the URL to ensure compatibility across Galaxy deployments.
All other options can be anything used by the plugin and accessed later via *webhook.config['...']*.
+2 -2
View File
@@ -40,9 +40,9 @@ To safely delete unused histories and their associated records, please use the `
Deleting old galaxy_session records
-----------------------------------
Each time Galaxy is accessed, a galaxy_session record is created, even when the user is annonymous. Over time, Galaxy accumulates such records. Deleting such records will declutter the database and free up space.
Each time Galaxy is accessed, a galaxy_session record is created, even when the user is anonymous. Over time, Galaxy accumulates such records. Deleting such records will declutter the database and free up space.
To safely delete such records, please use the galaxy-delete-sessions script. By default, a galaxy_session record should be at least a month old to be considered safe to delete (which is determinded by the value of its ``update_time`` field).
To safely delete such records, please use the galaxy-delete-sessions script. By default, a galaxy_session record should be at least a month old to be considered safe to delete (which is determined by the value of its ``update_time`` field).
.. code-block:: console
+2 -2
View File
@@ -77,7 +77,7 @@ While the structure is similar, several key differences exist:
To enable this feature:
1. Set `enable_beta_tool_formats: true` in your Galaxy configuration.
2. Create a role of type `Custom Tool Execution` in the admin user interdace.
2. Create a role of type `Custom Tool Execution` in the admin user interface.
3. Assign users or groups to this role.
## Sharing User-Defined Tools
@@ -88,7 +88,7 @@ These tools can also be exported to disk and loaded like regular tools, enabling
## Security considerations
User-defined tools share the same security risks as interactive tools..
User-defined tools share the same security risks as interactive tools.
See https://training.galaxyproject.org/training-material/topics/admin/tutorials/interactive-tools/tutorial.html#securing-interactive-tools for an extended discussion,
and see https://github.com/galaxyproject/galaxy/blob/dev/test/integration/embedded_pulsar_job_conf.yml#L29 for a simple example that uses embedded pulsar to isolate mounts and disables network access.
While the feature is in beta we recommend that only trusted users are allowed to use this feature.
+2 -2
View File
@@ -44,7 +44,7 @@ of the static checking that can be done for the Galaxy backend.
To further document your API and allow other developers to see how it functions,
new API functionality should include functional tests. These functional
tests should be implemented in Python and added to either the API test suite
or the integration test suite. Checkout the :doc:`Writing Tests for Galaxy <writing_tests>`
or the integration test suite. Check out the :doc:`Writing Tests for Galaxy <writing_tests>`
documentation for more information on developing tests for Galaxy.
.. note::
@@ -131,7 +131,7 @@ in :mod:`galaxy.webapps.galaxy.api` receive or produce Pydantic_
models and pass them to manager components defined in
:mod:`galaxy.managers`. The manager is then responsible for
interacting with the whole backend or specific components to
fullfil the desired request.
fulfill the desired request.
Galaxy work contexts provide a way for API requests (and other
components) to describe what they require from the framework
+2 -2
View File
@@ -508,7 +508,7 @@ Data Manager can add entries to one or more Tool Data Tables. For
each Tool Data Table under consideration, the expected output entry
columns, and how to handle the Data Manager Tool results, are defined.
Upon installation, Galaxy will recursivly search for data_manager_conf.xml in the repository.
Upon installation, Galaxy will recursively search for data_manager_conf.xml in the repository.
data_manager_conf.xml should be located in the root of the repository but can be located in a sub-directory.
The first instance of data_manager_conf.xml found will be used.
This file defines the paths to the data manager tools within the repository directory structure.
@@ -648,7 +648,7 @@ the ``extra_files_path`` of ``out_file``.
</when>
<when value="directory">
<param type="text" name="fasta_filename" value="" label="Full path to FASTA File on disk" optional="False" />
<param type="boolean" name="create_symlink" truevalue="create_symlink" falsevalue="copy_file" label="Create symlink to orignal data instead of copying" checked="False" />
<param type="boolean" name="create_symlink" truevalue="create_symlink" falsevalue="copy_file" label="Create symlink to original data instead of copying" checked="False" />
</when>
</conditional>
</inputs>
+1 -1
View File
@@ -40,5 +40,5 @@ should work fine.
2. Re-start VS Code
3. Add a breakpoint somewhere in your code
4. Select Run and Debug on Activity Bar, on the far left hand side. In the RUN AND DEBUG drop down, select `GalaxyFastAPI uvicorn`, and click the Start Debugging button (green arrow)
5. Galaxy should stop right on the break point you added.
5. Galaxy should stop right on the breakpoint you added.
6. Enjoy your debugging session :-)
+2 -2
View File
@@ -6,7 +6,7 @@ default LocalRunner). If you have a MacOS machine, you need to create a Linux VM
VirtualBoxVM and run the instructions in the VM. For reference, I created a 64-bit Linux VM with
4096 MB of RAM and 32 GB of disk space (all other parameters were left as default) and managed to
successfully run the instructions. If using a Linux VM, you may need to install Git, Python, VSCode,
and Docker (if using Irods storage). Finally, These instructions are intended for developers who
and Docker (if using Irods storage). Finally, these instructions are intended for developers who
want to debug their Galaxy instance and are not meant for deployment purposes (We have nice Ansible
playbooks for that).
@@ -73,4 +73,4 @@ playbooks for that).
6. Follow the instructions [here](debugging_galaxy) to setup VSCode for debugging.
Enjoy debugging session your Galaxy instance backed by a Slurm cluster!
Enjoy debugging your Galaxy instance backed by a Slurm cluster!
+4 -4
View File
@@ -77,13 +77,13 @@ find the right documentation for a given test one wishes to write.
- **Client/ES6**
These tests should be placed in ``client/src`` directly and executed
via Vitest. Checkout [Frontend/ES6 Unit Tests](#es6_unit) below for more
via Vitest. Check out [Frontend/ES6 Unit Tests](#es6_unit) below for more
information.
- **Backend/Python**
These tests should be placed in ``test/unit`` or doctests and
executed via pytest. Checkout [Backend/Python Unit Tests](#python_unit) below for more
executed via pytest. Check out [Backend/Python Unit Tests](#python_unit) below for more
information.
- **Yes**
@@ -1212,8 +1212,8 @@ see the [Browser Automation README](https://github.com/galaxyproject/galaxy/blob
### Jupyter + Selenium
Jupyter can leveraged to develop Selenium test cases interactively,
checkout out the [``galaxy_test.selenium.jupyter`` documentation](../lib/galaxy_test.selenium.jupyter)
for more a full discussion of this.
check out the [``galaxy_test.selenium.jupyter`` documentation](../lib/galaxy_test.selenium.jupyter)
for a full discussion of this.
### API Abstractions Available
+136
View File
@@ -31,8 +31,11 @@ from galaxy.schema.fetch_data import (
from galaxy.schema.invocation import InvocationSerializationParams
from galaxy.schema.schema import (
CreateHistoryPayload,
CreatePagePayload,
DatasetSourceType,
InvocationIndexPayload,
PageIndexQueryPayload,
UpdatePagePayload,
WorkflowIndexPayload,
)
from galaxy.schema.workflows import InvokeWorkflowPayload
@@ -72,6 +75,7 @@ class AgentOperationsManager:
self._dataset_collections_service: Optional[Any] = None
self._dynamic_tools_manager: Optional[Any] = None
self._file_source_instances_manager: Optional[Any] = None
self._pages_service: Optional[Any] = None
def _encode_id(self, value: int) -> str:
return self.trans.security.encode_id(value)
@@ -173,6 +177,14 @@ class AgentOperationsManager:
self._file_source_instances_manager = self.app[FileSourceInstancesManager]
return self._file_source_instances_manager
@property
def pages_service(self):
if self._pages_service is None:
from galaxy.webapps.galaxy.services.pages import PagesService
self._pages_service = self.app[PagesService]
return self._pages_service
def connect(self) -> dict[str, Any]:
config = self.app.config
user = self.trans.user
@@ -1089,3 +1101,127 @@ class AgentOperationsManager:
"file_sources": file_sources,
"count": len(file_sources),
}
# ==================== Pages (notebooks and reports) ====================
def _dump_page(self, model, include_rendered: bool = False) -> dict[str, Any]:
"""Serialize a page/revision schema, dropping the large rendered form by default.
content_editor (editable encoded-id markdown) is always kept; content (the
embed-expanded render form) is included only when include_rendered is True.
"""
result = model.model_dump(mode="json")
if not include_rendered:
result.pop("content", None)
return result
def list_pages(
self,
history_id: Optional[str] = None,
search: Optional[str] = None,
limit: int = 100,
offset: int = 0,
show_published: bool = False,
show_shared: bool = False,
deleted: bool = False,
) -> dict[str, Any]:
"""List pages viewable by the current user.
When history_id is set, only pages attached to that history (Galaxy
Notebooks) are returned. Defaults to the user's own pages; published or
shared pages are included only when explicitly requested.
"""
payload = PageIndexQueryPayload(
history_id=history_id,
search=search,
limit=limit,
offset=offset,
show_own=True,
show_published=show_published,
show_shared=show_shared,
deleted=deleted,
)
pages, total_matches = self.pages_service.index(self.trans, payload, include_total_count=True)
return {
"pages": pages.model_dump(mode="json"),
"count": len(pages.root),
"total_matches": total_matches,
}
def get_page(self, page_id: str, include_rendered: bool = False) -> dict[str, Any]:
"""Return a page with its latest-revision content.
content_editor (editable markdown with encoded-id directives) is always
returned. The embed-expanded render form (content) can be large and is
included only when include_rendered is True.
"""
decoded_page_id = self.trans.security.decode_id(page_id)
details = self.pages_service.show(self.trans, decoded_page_id)
return self._dump_page(details, include_rendered)
def create_page(
self,
history_id: Optional[str] = None,
title: Optional[str] = None,
content: Optional[str] = None,
annotation: Optional[str] = None,
slug: Optional[str] = None,
) -> dict[str, Any]:
"""Create a markdown page. Attach it to a history (notebook) by passing history_id.
Standalone reports (no history_id) require a unique slug and a title.
"""
payload = CreatePagePayload(
history_id=history_id,
title=title,
content=content,
content_format="markdown",
annotation=annotation,
slug=slug,
)
details = self.pages_service.create(self.trans, payload)
return self._dump_page(details)
def update_page(
self,
page_id: str,
content: Optional[str] = None,
title: Optional[str] = None,
) -> dict[str, Any]:
"""Update a page. Supplying content creates a new revision tagged edit_source=agent."""
decoded_page_id = self.trans.security.decode_id(page_id)
payload = UpdatePagePayload(
content=content,
title=title,
edit_source="agent",
)
details = self.pages_service.update(self.trans, decoded_page_id, payload)
return self._dump_page(details)
def list_page_revisions(self, page_id: str, sort_desc: bool = False) -> dict[str, Any]:
"""List the revision history of a page (provenance via edit_source)."""
decoded_page_id = self.trans.security.decode_id(page_id)
revisions = self.pages_service.list_revisions(self.trans, decoded_page_id, sort_desc=sort_desc)
return {
"revisions": revisions.model_dump(mode="json"),
"count": len(revisions.root),
}
def get_page_revision(self, page_id: str, revision_id: str, include_rendered: bool = False) -> dict[str, Any]:
"""Return a single page revision with its content.
Mirrors get_page: content_editor (editable encoded-id markdown) is always
returned; the embed-expanded render form (content) is included only when
include_rendered is True.
"""
decoded_page_id = self.trans.security.decode_id(page_id)
decoded_revision_id = self.trans.security.decode_id(revision_id)
revision = self.pages_service.show_revision(self.trans, decoded_page_id, decoded_revision_id)
return self._dump_page(revision, include_rendered)
def revert_page_revision(self, page_id: str, revision_id: str) -> dict[str, Any]:
"""Roll a page back to an earlier revision (creates a new 'restore' revision)."""
decoded_page_id = self.trans.security.decode_id(page_id)
decoded_revision_id = self.trans.security.decode_id(revision_id)
revision = self.pages_service.revert_revision(self.trans, decoded_page_id, decoded_revision_id)
return self._dump_page(revision)
+3
View File
@@ -363,6 +363,9 @@ class ConditionalDependencies:
def check_iiif_fsspec(self):
return "iiif" in self.file_sources
def check_mavedb_fsspec(self):
return "mavedb" in self.file_sources
def strip_comment(line):
# lifted from https://github.com/tox-dev/tox/commit/3c6b4f204e89852c4b7536b246a66d20be6d39ec
@@ -36,6 +36,7 @@ huggingface_hub
omero-py #type: omero
iiif-fsspec # type: iiif
fs-irods # type: irods, depends on python-irodsclient
mavedb-fsspec>=0.1.0 # type: mavedb
# Vault backend
hvac
+125
View File
@@ -0,0 +1,125 @@
from typing import (
Optional,
Union,
)
from galaxy.exceptions import (
AuthenticationRequired,
MessageException,
)
from galaxy.files.models import (
AnyRemoteEntry,
FilesSourceRuntimeContext,
)
from galaxy.files.sources._defaults import DEFAULT_SCHEME
from galaxy.files.sources._fsspec import (
CacheOptionsDictType,
FsspecBaseFileSourceConfiguration,
FsspecBaseFileSourceTemplateConfiguration,
FsspecFilesSource,
)
from galaxy.util.config_templates import TemplateExpansion
try:
from mavedb_fsspec import MaveDBFileSystem
from mavedb_fsspec.client import DEFAULT_BASE_URL
except ImportError:
MaveDBFileSystem = None
DEFAULT_BASE_URL = "https://api.mavedb.org/api/v1"
class MaveDBFileSourceTemplateConfiguration(FsspecBaseFileSourceTemplateConfiguration):
base_url: Union[str, TemplateExpansion] = DEFAULT_BASE_URL
api_key: Union[str, TemplateExpansion, None] = None
timeout: Union[float, TemplateExpansion] = 30.0
class MaveDBFileSourceConfiguration(FsspecBaseFileSourceConfiguration):
base_url: str = DEFAULT_BASE_URL
api_key: Optional[str] = None
timeout: float = 30.0
class MaveDBFilesSource(FsspecFilesSource[MaveDBFileSourceTemplateConfiguration, MaveDBFileSourceConfiguration]):
plugin_type = "mavedb"
required_module = MaveDBFileSystem
required_package = "mavedb-fsspec"
template_config_class = MaveDBFileSourceTemplateConfiguration
resolved_config_class = MaveDBFileSourceConfiguration
def _open_fs(
self,
context: FilesSourceRuntimeContext[MaveDBFileSourceConfiguration],
cache_options: CacheOptionsDictType,
) -> "MaveDBFileSystem":
if MaveDBFileSystem is None:
raise self.required_package_exception
config = context.config
return MaveDBFileSystem(
base_url=config.base_url,
api_key=config.api_key,
timeout=config.timeout,
skip_instance_cache=True,
**cache_options,
)
def _list(
self,
context: FilesSourceRuntimeContext[MaveDBFileSourceConfiguration],
path="/",
recursive=False,
write_intent: bool = False,
limit: Optional[int] = None,
offset: Optional[int] = None,
query: Optional[str] = None,
sort_by: Optional[str] = None,
) -> tuple[list[AnyRemoteEntry], int]:
collection = path.strip("/")
if recursive or collection not in {"score-sets", "my-score-sets"}:
return super()._list(context, path, recursive, write_intent, limit, offset, query, sort_by)
try:
cache_options = self._get_cache_options(context.config)
fs = self._open_fs(context, cache_options)
try:
# Use the MaveDB-specific API for server-side pagination and the total result count.
entries, total_count = fs.list_score_sets(
collection=collection,
limit=limit,
offset=offset,
query=query,
)
finally:
close = getattr(fs, "close", None)
if close is not None:
close()
return [self._info_to_entry(entry, context.config) for entry in entries], total_count
except PermissionError as e:
raise AuthenticationRequired(
f"Permission Denied. Reason: {e}. Please check your credentials in your preferences for {self.label}."
)
except Exception as e:
raise MessageException(f"Problem listing file source path {path}. Reason: {e}") from e
def _info_to_entry(self, info: dict, config: MaveDBFileSourceConfiguration) -> AnyRemoteEntry:
entry = super()._info_to_entry(info, config)
display_name = info.get("display_name")
if display_name:
entry.name = display_name
return entry
def _write_from(
self,
_target_path: str,
_native_path: str,
_context: FilesSourceRuntimeContext[MaveDBFileSourceConfiguration],
):
raise MessageException("MaveDB file sources are read-only and do not support exporting files.")
def get_scheme(self) -> str:
return self.scheme if self.scheme and self.scheme != DEFAULT_SCHEME else "mavedb"
__all__ = ("MaveDBFilesSource",)
@@ -0,0 +1,19 @@
- id: mavedb
version: 0
name: MaveDB
description: |
[MaveDB](https://www.mavedb.org) provides Multiplexed Assays of Variant Effect datasets.
This file source exposes score set files through the public MaveDB API.
secrets:
api_key:
label: MaveDB API key
optional: true
help: |
Optional MaveDB API key. Public score set files do not require an API key.
A user API key can be generated in the MaveDB settings page for your account
and is needed to access private datasets.
configuration:
type: mavedb
base_url: https://api.mavedb.org/api/v1
api_key: "{{ secrets.api_key }}"
timeout: 30
+20
View File
@@ -51,6 +51,7 @@ FileSourceTemplateType = Literal[
"dataverse",
"huggingface",
"iiif",
"mavedb",
"omero",
"ssh",
]
@@ -440,6 +441,22 @@ class IIIFFileSourceConfiguration(StrictModel):
manifest_url: str
class MaveDBFileSourceTemplateConfiguration(StrictModel):
type: Literal["mavedb"]
base_url: Union[str, TemplateExpansion] = "https://api.mavedb.org/api/v1"
api_key: Union[str, TemplateExpansion, None] = None
timeout: Union[float, TemplateExpansion] = 30.0
template_start: Optional[str] = None
template_end: Optional[str] = None
class MaveDBFileSourceConfiguration(StrictModel):
type: Literal["mavedb"]
base_url: str = "https://api.mavedb.org/api/v1"
api_key: Optional[str] = None
timeout: float = 30.0
class OmeroFileSourceTemplateConfiguration(StrictModel):
type: Literal["omero"]
username: Union[str, TemplateExpansion]
@@ -480,6 +497,7 @@ FileSourceTemplateConfiguration = Annotated[
DataverseFileSourceTemplateConfiguration,
HuggingFaceFileSourceTemplateConfiguration,
IIIFFileSourceTemplateConfiguration,
MaveDBFileSourceTemplateConfiguration,
OmeroFileSourceTemplateConfiguration,
SshFileSourceTemplateConfiguration,
],
@@ -506,6 +524,7 @@ FileSourceConfiguration = Annotated[
DataverseFileSourceConfiguration,
HuggingFaceFileSourceConfiguration,
IIIFFileSourceConfiguration,
MaveDBFileSourceConfiguration,
OmeroFileSourceConfiguration,
SshFileSourceConfiguration,
],
@@ -590,6 +609,7 @@ TypesToConfigurationClasses: dict[FileSourceTemplateType, type[FileSourceConfigu
"dataverse": DataverseFileSourceConfiguration,
"huggingface": HuggingFaceFileSourceConfiguration,
"iiif": IIIFFileSourceConfiguration,
"mavedb": MaveDBFileSourceConfiguration,
"omero": OmeroFileSourceConfiguration,
"ssh": SshFileSourceConfiguration,
}
+3 -3
View File
@@ -73,7 +73,9 @@ SYNTHETIC_TOOL_IDS: tuple[str, ...] = ("__DATA_FETCH__",)
def _summary_dict(summary) -> Optional[dict[str, int]]:
"""Lift JobStateSummary (a NamedTuple) to a dict, or None."""
return None if summary is None else summary._asdict()
if summary is None or summary.all_jobs == 0:
return None
return summary._asdict()
class HistoryGraphManager:
@@ -601,8 +603,6 @@ class HistoryGraphBuilder:
]
def _tr_nodes(self, tr_map: dict[int, Optional[str]]) -> list[GraphNode]:
if not tr_map:
return []
return [self._node("tool_request", tr_id, tool_id=tool_id) for tr_id, tool_id in tr_map.items()]
def _resolve_tool_names(self, nodes: list[GraphNode]) -> None:
+1
View File
@@ -4376,6 +4376,7 @@ class PageRevisionSummary(Model):
class PageRevisionDetails(PageRevisionSummary):
title: Optional[str] = None
content: Optional[str] = None
content_editor: Optional[str] = ContentEditorField
content_format: Optional[PageContentFormat] = None
+232
View File
@@ -1180,6 +1180,238 @@ def get_mcp_app(gx_app):
ops_manager = get_operations_manager(api_key, ctx)
return ops_manager.import_workflow_from_iwc(trs_id)
# ==================== Pages (notebooks and reports) ====================
@mcp.tool()
def list_pages(
api_key: str,
ctx: MCPContext,
history_id: str | None = None,
search: str | None = None,
limit: int = 100,
offset: int = 0,
show_published: bool = False,
show_shared: bool = False,
) -> dict[str, Any]:
"""List Galaxy pages (markdown documents) viewable by the user.
A page attached to a history is a "Galaxy Notebook"; a standalone page is
a "Report". Pass history_id to list only that history's notebooks.
Args:
history_id: Encoded history id. When set, returns only notebooks
attached to that history.
search: Freetext filter over title/content.
limit: Max pages to return (default 100).
offset: Pagination offset.
show_published: Also include pages published by other users (default False).
show_shared: Also include pages shared with the user (default False).
Returns:
Dict with `pages` (summaries incl. encoded id, title, history_id,
latest_revision_id), `count`, and `total_matches`.
NEXT STEPS:
- Read one: get_page(page_id)
- Create a notebook: create_page(history_id=...)
"""
with _mcp_error_handler("list_pages"):
ops_manager = get_operations_manager(api_key, ctx)
return ops_manager.list_pages(
history_id=history_id,
search=search,
limit=limit,
offset=offset,
show_published=show_published,
show_shared=show_shared,
)
@mcp.tool()
def get_page(
page_id: str,
api_key: str,
ctx: MCPContext,
include_rendered: bool = False,
) -> dict[str, Any]:
"""Get a page and the content of its latest revision.
Returns `content_editor`: the editable Galaxy-flavored markdown, with
ENCODED ids in directives (e.g. `history_dataset_id=f2db41e1fa331b3e`).
This is the form to edit and pass back to update_page.
Args:
page_id: Encoded id of the page (from list_pages / create_page).
include_rendered: When True, also return `content` -- the
embed-expanded render form (inlined dataset previews). Can be
large; omit unless you need the rendered output.
Returns:
Page details including `content_editor`, metadata, and `edit_source`
of the latest revision.
NEXT STEPS:
- Edit it: update_page(page_id, content=...)
- See history: list_page_revisions(page_id)
"""
with _mcp_error_handler("get_page"):
ops_manager = get_operations_manager(api_key, ctx)
return ops_manager.get_page(page_id, include_rendered=include_rendered)
@mcp.tool()
def create_page(
api_key: str,
ctx: MCPContext,
history_id: str | None = None,
title: str | None = None,
content: str | None = None,
annotation: str | None = None,
slug: str | None = None,
) -> dict[str, Any]:
"""Create a markdown page (Galaxy Notebook or Report).
Pass history_id to create a history-attached notebook (title auto-fills
from the history if omitted). Omit history_id to create a standalone
report -- reports REQUIRE both a title and a unique slug
(lowercase/digits/hyphens).
Content is Galaxy-flavored markdown. To embed a dataset, use a directive
with the ENCODED dataset id, e.g.
`history_dataset_display(history_dataset_id=f2db41e1fa331b3e)` or
`history_dataset_collection_display(history_dataset_collection_id=...)`.
Get encoded ids from get_history_contents / get_dataset_details.
Args:
history_id: Encoded history id to attach the page to (notebook).
title: Page title.
content: Initial markdown content.
annotation: Optional annotation attached to the page.
slug: URL slug (required for standalone reports).
Returns:
Created page details including encoded id and content_editor.
NEXT STEPS:
- Edit it: update_page(page_id, content=...)
"""
with _mcp_error_handler("create_page"):
ops_manager = get_operations_manager(api_key, ctx)
return ops_manager.create_page(
history_id=history_id,
title=title,
content=content,
annotation=annotation,
slug=slug,
)
@mcp.tool()
def update_page(
page_id: str,
api_key: str,
ctx: MCPContext,
content: str | None = None,
title: str | None = None,
) -> dict[str, Any]:
"""Update a page, creating a new revision when content changes.
Content is Galaxy-flavored markdown using ENCODED ids in directives
(e.g. `history_dataset_id=f2db41e1fa331b3e`) -- never raw integer ids or
HIDs. Get encoded ids from get_history_contents / get_dataset_details.
Edits made through this tool are recorded with edit_source="agent".
Args:
page_id: Encoded id of the page.
content: New markdown content. Omit to leave content unchanged.
title: New title. Omit to leave unchanged.
Returns:
Updated page details including content_editor.
NEXT STEPS:
- Inspect revisions: list_page_revisions(page_id)
- Roll back: revert_page_revision(page_id, revision_id)
"""
with _mcp_error_handler("update_page"):
ops_manager = get_operations_manager(api_key, ctx)
return ops_manager.update_page(page_id, content=content, title=title)
@mcp.tool()
def list_page_revisions(
page_id: str,
api_key: str,
ctx: MCPContext,
sort_desc: bool = False,
) -> dict[str, Any]:
"""List the revision history of a page.
Each revision carries an `edit_source` ("user", "agent", or "restore")
recording who made the edit.
Args:
page_id: Encoded id of the page.
sort_desc: Newest-first when True (default oldest-first).
Returns:
Dict with `revisions` (encoded id, page_id, edit_source, timestamps)
and `count`.
NEXT STEPS:
- Read a revision: get_page_revision(page_id, revision_id)
- Roll back: revert_page_revision(page_id, revision_id)
"""
with _mcp_error_handler("list_page_revisions"):
ops_manager = get_operations_manager(api_key, ctx)
return ops_manager.list_page_revisions(page_id, sort_desc=sort_desc)
@mcp.tool()
def get_page_revision(
page_id: str,
revision_id: str,
api_key: str,
ctx: MCPContext,
include_rendered: bool = False,
) -> dict[str, Any]:
"""Get the content of a single page revision.
Mirrors get_page: returns `content_editor` (the editable Galaxy-flavored
markdown with ENCODED ids in directives) -- the form to compare against
get_page or pass back to update_page.
Args:
page_id: Encoded id of the page.
revision_id: Encoded id of the revision (from list_page_revisions).
include_rendered: When True, also return `content` -- the
embed-expanded render form. Can be large; omit unless needed.
Returns:
Revision details including `content_editor`, `edit_source`, and timestamps.
"""
with _mcp_error_handler("get_page_revision"):
ops_manager = get_operations_manager(api_key, ctx)
return ops_manager.get_page_revision(page_id, revision_id, include_rendered=include_rendered)
@mcp.tool()
def revert_page_revision(
page_id: str,
revision_id: str,
api_key: str,
ctx: MCPContext,
) -> dict[str, Any]:
"""Roll a page back to an earlier revision.
Creates a NEW revision from the target revision's content, recorded with
edit_source="restore" (the history is append-only -- nothing is deleted).
Args:
page_id: Encoded id of the page.
revision_id: Encoded id of the revision to restore.
Returns:
The new restored revision's details.
"""
with _mcp_error_handler("revert_page_revision"):
ops_manager = get_operations_manager(api_key, ctx)
return ops_manager.revert_page_revision(page_id, revision_id)
mcp_app = mcp.http_app(path="/")
mcp_app.state.mcp_server = mcp
+91
View File
@@ -349,6 +349,13 @@ class TestMCPServerSmoke(IntegrationTestCase):
"search_iwc_workflows",
"get_iwc_workflow_details",
"import_workflow_from_iwc",
"list_pages",
"get_page",
"create_page",
"update_page",
"list_page_revisions",
"get_page_revision",
"revert_page_revision",
}
assert expected.issubset(tool_names), f"Missing tools: {expected - tool_names}"
@@ -563,3 +570,87 @@ class TestMCPServerSmoke(IntegrationTestCase):
data = result.data
assert "id" in data
assert data["trsID"] == trs_id
def test_mcp_page_lifecycle(self):
"""create_page -> get_page -> update_page -> list_page_revisions round-trip."""
mcp_server = self._get_mcp_server()
api_key = self._get_api_key()
populator = DatasetPopulator(self._get_interactor(api_key=api_key))
history_id = populator.new_history()
async def _flow():
async with Client(mcp_server) as client:
created = await client.call_tool(
"create_page",
{"api_key": api_key, "history_id": history_id, "title": "MCP Notebook", "content": "# Intro\n"},
)
page_id = created.data["id"]
got = await client.call_tool("get_page", {"api_key": api_key, "page_id": page_id})
await client.call_tool(
"update_page",
{"api_key": api_key, "page_id": page_id, "content": "# Intro\n\n## Methods\n"},
)
revisions = await client.call_tool("list_page_revisions", {"api_key": api_key, "page_id": page_id})
listed = await client.call_tool("list_pages", {"api_key": api_key, "history_id": history_id})
return created, got, revisions, listed
created, got, revisions, listed = self._run_async(_flow())
assert not created.is_error, created
assert created.data["history_id"] == history_id
# get_page returns the editable content and omits the rendered form by default
assert "content_editor" in got.data
assert "content" not in got.data
# update_page created a second revision tagged with edit_source="agent"
assert revisions.data["count"] == 2
assert revisions.data["revisions"][-1]["edit_source"] == "agent"
# the notebook shows up when listing pages for its history
assert created.data["id"] in {p["id"] for p in listed.data["pages"]}
def test_mcp_page_directive_and_revert(self):
"""Dataset directives survive in encoded-id space; revert restores prior content."""
mcp_server = self._get_mcp_server()
api_key = self._get_api_key()
populator = DatasetPopulator(self._get_interactor(api_key=api_key))
history_id = populator.new_history()
hda_id = populator.new_dataset(history_id)["id"]
directive = f"# Analysis\n\n```galaxy\nhistory_dataset_display(history_dataset_id={hda_id})\n```\n"
async def _flow():
async with Client(mcp_server) as client:
created = await client.call_tool(
"create_page",
{"api_key": api_key, "history_id": history_id, "title": "Directive NB", "content": directive},
)
page_id = created.data["id"]
editor = await client.call_tool("get_page", {"api_key": api_key, "page_id": page_id})
rendered = await client.call_tool(
"get_page", {"api_key": api_key, "page_id": page_id, "include_rendered": True}
)
await client.call_tool(
"update_page", {"api_key": api_key, "page_id": page_id, "content": "# Replaced\n"}
)
revisions = await client.call_tool("list_page_revisions", {"api_key": api_key, "page_id": page_id})
first_rev_id = revisions.data["revisions"][0]["id"]
old_rev = await client.call_tool(
"get_page_revision",
{"api_key": api_key, "page_id": page_id, "revision_id": first_rev_id},
)
reverted = await client.call_tool(
"revert_page_revision",
{"api_key": api_key, "page_id": page_id, "revision_id": first_rev_id},
)
return editor, rendered, old_rev, reverted
editor, rendered, old_rev, reverted = self._run_async(_flow())
# content_editor keeps the directive with the ENCODED dataset id (encoded-id space)
assert hda_id in editor.data["content_editor"]
assert "content" not in editor.data
# include_rendered exposes the expanded render form
assert "content" in rendered.data
# get_page_revision mirrors get_page: editable content_editor by default, rendered omitted
assert hda_id in old_rev.data["content_editor"]
assert "content" not in old_rev.data
# revert appends a new "restore" revision carrying the original directive content
assert reverted.data["edit_source"] == "restore"
assert hda_id in reverted.data["content_editor"]
assert "content" not in reverted.data
@@ -3,6 +3,7 @@ from unittest import mock
import pytest
from galaxy.agents.operations import AgentOperationsManager
from galaxy.schema.fields import Security
from .base import BaseTestCase
@@ -419,3 +420,190 @@ class TestAgentOperationsManagerWithMockedServices(BaseTestCase):
with pytest.raises(ValueError, match="User must be authenticated"):
agent_ops.list_user_file_sources()
class TestAgentOperationsManagerPages(BaseTestCase):
"""Page (notebook/report) operations against a mocked PagesService."""
def set_up_managers(self):
super().set_up_managers()
# DecodedDatabaseIdField on page payloads decodes via the module-global
# Security.security; point it at the same helper trans uses to encode.
Security.security = self.trans.security
self.agent_ops = AgentOperationsManager(app=self.app, trans=self.trans)
def _mock_pages_service(self, mock_service):
return mock.patch.object(
type(self.agent_ops), "pages_service", new_callable=lambda: property(lambda self: mock_service)
)
def test_list_pages_defaults_to_own_only(self):
pages = mock.MagicMock()
pages.model_dump.return_value = []
pages.root = []
mock_service = mock.MagicMock()
mock_service.index.return_value = (pages, 0)
with self._mock_pages_service(mock_service):
result = self.agent_ops.list_pages()
payload = mock_service.index.call_args.args[1]
assert payload.show_own is True
assert payload.show_published is False
assert payload.show_shared is False
assert payload.history_id is None
assert result["count"] == 0
assert result["total_matches"] == 0
def test_list_pages_history_filter(self):
encoded_history_id = self.trans.security.encode_id(42)
pages = mock.MagicMock()
pages.model_dump.return_value = []
pages.root = []
mock_service = mock.MagicMock()
mock_service.index.return_value = (pages, 5)
with self._mock_pages_service(mock_service):
result = self.agent_ops.list_pages(history_id=encoded_history_id)
payload = mock_service.index.call_args.args[1]
assert payload.history_id == 42
assert result["total_matches"] == 5
def test_get_page_drops_rendered_by_default(self):
details = mock.MagicMock()
details.model_dump.return_value = {"id": "p1", "content_editor": "# Notes", "content": "<rendered>"}
mock_service = mock.MagicMock()
mock_service.show.return_value = details
with (
self._mock_pages_service(mock_service),
mock.patch.object(self.trans.security, "decode_id", return_value=7),
):
result = self.agent_ops.get_page("encoded_page")
assert result["content_editor"] == "# Notes"
assert "content" not in result
mock_service.show.assert_called_once_with(self.trans, 7)
def test_get_page_includes_rendered_when_requested(self):
details = mock.MagicMock()
details.model_dump.return_value = {"id": "p1", "content_editor": "# Notes", "content": "<rendered>"}
mock_service = mock.MagicMock()
mock_service.show.return_value = details
with (
self._mock_pages_service(mock_service),
mock.patch.object(self.trans.security, "decode_id", return_value=7),
):
result = self.agent_ops.get_page("encoded_page", include_rendered=True)
assert result["content"] == "<rendered>"
def test_create_page_sets_markdown_format(self):
details = mock.MagicMock()
details.model_dump.return_value = {"id": "p1"}
mock_service = mock.MagicMock()
mock_service.create.return_value = details
with self._mock_pages_service(mock_service):
self.agent_ops.create_page(title="Report", content="# Hi", slug="my-report")
payload = mock_service.create.call_args.args[1]
assert payload.content_format == "markdown"
assert payload.title == "Report"
assert payload.slug == "my-report"
assert payload.history_id is None
def test_create_page_with_history_decodes_id(self):
encoded_history_id = self.trans.security.encode_id(99)
details = mock.MagicMock()
details.model_dump.return_value = {"id": "p1"}
mock_service = mock.MagicMock()
mock_service.create.return_value = details
with self._mock_pages_service(mock_service):
self.agent_ops.create_page(history_id=encoded_history_id, content="# Hi")
payload = mock_service.create.call_args.args[1]
assert payload.history_id == 99
assert payload.content_format == "markdown"
def test_update_page_sets_edit_source_agent(self):
details = mock.MagicMock()
details.model_dump.return_value = {"id": "p1"}
mock_service = mock.MagicMock()
mock_service.update.return_value = details
with (
self._mock_pages_service(mock_service),
mock.patch.object(self.trans.security, "decode_id", return_value=7),
):
self.agent_ops.update_page("encoded_page", content="# New")
call = mock_service.update.call_args
assert call.args[1] == 7
payload = call.args[2]
assert payload.edit_source == "agent"
assert payload.content == "# New"
def test_list_page_revisions(self):
revisions = mock.MagicMock()
revisions.model_dump.return_value = [{"id": "r1"}]
revisions.root = [{"id": "r1"}]
mock_service = mock.MagicMock()
mock_service.list_revisions.return_value = revisions
with (
self._mock_pages_service(mock_service),
mock.patch.object(self.trans.security, "decode_id", return_value=7),
):
result = self.agent_ops.list_page_revisions("encoded_page")
mock_service.list_revisions.assert_called_once_with(self.trans, 7, sort_desc=False)
assert result["count"] == 1
def test_get_page_revision_drops_rendered_by_default(self):
revision = mock.MagicMock()
revision.model_dump.return_value = {"id": "r1", "content_editor": "# Notes", "content": "<rendered>"}
mock_service = mock.MagicMock()
mock_service.show_revision.return_value = revision
with (
self._mock_pages_service(mock_service),
mock.patch.object(self.trans.security, "decode_id", side_effect=[7, 3]),
):
result = self.agent_ops.get_page_revision("encoded_page", "encoded_rev")
mock_service.show_revision.assert_called_once_with(self.trans, 7, 3)
assert result["content_editor"] == "# Notes"
assert "content" not in result
def test_get_page_revision_includes_rendered_when_requested(self):
revision = mock.MagicMock()
revision.model_dump.return_value = {"id": "r1", "content_editor": "# Notes", "content": "<rendered>"}
mock_service = mock.MagicMock()
mock_service.show_revision.return_value = revision
with (
self._mock_pages_service(mock_service),
mock.patch.object(self.trans.security, "decode_id", side_effect=[7, 3]),
):
result = self.agent_ops.get_page_revision("encoded_page", "encoded_rev", include_rendered=True)
assert result["content"] == "<rendered>"
def test_revert_page_revision_returns_restore(self):
revision = mock.MagicMock()
revision.model_dump.return_value = {"id": "r2", "edit_source": "restore"}
mock_service = mock.MagicMock()
mock_service.revert_revision.return_value = revision
with (
self._mock_pages_service(mock_service),
mock.patch.object(self.trans.security, "decode_id", side_effect=[7, 3]),
):
result = self.agent_ops.revert_page_revision("encoded_page", "encoded_rev")
mock_service.revert_revision.assert_called_once_with(self.trans, 7, 3)
assert result["edit_source"] == "restore"