Merge branch 'ml-jupyter-notebook' of github.com:anuprulez/galaxy into ml-jupyter-notebook

This commit is contained in:
anup kumar
2021-02-25 13:05:13 +01:00
206 changed files with 3713 additions and 1473 deletions
+1 -1
View File
@@ -165,7 +165,7 @@ doc/build
doc/schema.md
doc/source/admin/config_logging_default_yaml.rst
doc/source/dev/schema.md
client/styleguide
client/docs/dist
# webpack stats
client/webpack-stats.json
-135
View File
@@ -164,138 +164,3 @@ terminal this starts for executing Jest tests.
yarn run jest-watch Dialog
yarn run jest-watch workflow/run
### Writing a test file
Jest will try to test any file ending in "\*.test.js". Please place your test
files inside client/src folders right next to whatever files that they are
testing.
Jest has extensive documentation on the expect API, mocking, and more on the
[official docs page](https://jestjs.io/docs/en/getting-started.html), which will
be your best resource here.
```javascript
// yourtestfile.test.js
import { things } from "./yourtestfile.js";
describe("some module you wrote", () => {
let transientVariables;
let serviceInstances;
let testData;
beforeEach(() => {
// setup your code (if necessary)
});
afterEach(() => {
// teardown your code (so it doesn't ruin the next test)
});
it("should do something or other", () => {
expect(workflowNodeCount()).toBe(5);
});
});
```
### Testing ~~Suggestions~~ Obligations
#### Clearly document the intent of your test
Please remember that these tests are not _for_ you. They're for the people who
come after you. It will be a lot easier to modify, repair and upgrade your code
if they can figure out what you were originally hoping to accomplish. Try to
use as detailed 'expect' statements as possible -- overuse of 'toBeTruthy()'
for example, can hide the intent of your test.
Add a couple of comments. Use variable names that mean something. Nobody's
code is as self-documenting as they believe it to be.
#### Only test the public API that you define (carefully!)
Internal implementations come and go with library upgrades and new tech. But
the point of the unit test is to make sure your units work as designed....
which means you need to... you know... design your code to work in units.
Separate your concerns and identify the developer-facing methods and functions
you expect them to use. Test THOSE. Everything else should probably be
considered an implementation detail.
The other side of the same coin is to test *only* the unit in question. If your
component has a model that uses a service that touches Vuex, which then uses
Axios to fetch some data -- don't test all that at once. Break things apart and
mock functionality to isolate testing to units. End to end testing is a
separate thing that shouldn't be attempted using spec tests in Jest.
Assume nobody cares _how_ your code works, we just need to know that the public
API you designed _does_ work. If performance problems or new tech necessitate a
re-write, these tests become a guide for the next implementation.
#### Wrap native browser resources in a function so they can be easily mocked
If your javascript needs to talk to the window object, or navigator, etc. wrap
that in a function call so that it can be easily mocked during testing.
```javascript
// myModule.js
// ... other code
export function redirectTo(url) {
window.location = url;
}
// myModule.test.js
jest.mock("myModule", () => ({
redirectTo: (url) => {
console.log(`I would have gone to: ${url}`);
},
}));
describe("some module", () => {
// ....
it("Performs a redirect when Foo is clicked"){
expect(redirectTo.mock.calls.length).toBe(1);
}
});
```
#### Implement logic in pure functions when possible
The more of your logic that is written in deterministic functions (i.e. no
side-effects, same inputs always result in same outputs) the easier it is to
test. Just load up the functions and supply suitable test inputs.
There is almost definitely no such thing as a well-written 1000 line function.
Most of whatever happened in that thing was probably deterministic and can be
broken up into easily testable chunks.
### Specific test scenarios & examples
[Mocking an imported
dependency](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/tagService.test.js)
[Testing async
operations](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/tagService.test.js)
[Testing a Vue component for expected rendering
output](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/StatelessTags.test.js)
[Firing an event against a shallow mounted vue
component](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/StatelessTags.test.js)
### The dirty secret about testing
It's good to have tests, but testing code isn't really about testing at all,
it's actually about software design.
Maintainable software is testable software. If you can run a unit test on your
code, then that means you must have necessarily separated your code into
testable units and you will have almost definitely written better, more
modular, more easily manipulated code, and nobody will ever contemplate using
git-blame to figure out what went wrong.
Probably.
+15
View File
@@ -0,0 +1,15 @@
/**
* Component root for rendering inside the styleguide, injects vuex store and other common elements
* into each component example section.
*/
import store from "../src/store";
export default (previewComponent) => {
return {
store,
render(h) {
return h(previewComponent);
},
};
};
+142
View File
@@ -0,0 +1,142 @@
/**
* Functions for generating the sections for the style guide. Has the old style doc glob and a
* recursive tree-walker that looks through the components folder and tries to arrange found
* markdown docs in a tree.
*/
const path = require("path");
const fs = require("fs");
const glob = require("glob");
const { humanize, titleize } = require("underscore.string");
/**
* Gets the table of contents sections for documentation in the components folder.
*
* @param {string} rootPath absolute path to components folder
* @return {Object} Rootnode, section map, childMap
*/
function getDocSections(rootPath, options = {}) {
const { ignore = [], docSelector = "*.@(vue|md)" } = options;
const sections = new Map(); // absolute section directory path -> section object
const childToParent = new Map(); // absolute child path -> absolute parent path
// create root node
const rootNode = newSection(rootPath, ignore);
sections.set(rootPath, rootNode);
childToParent.set(rootPath, null);
// get all matching doc files
const selector = path.join(rootPath, "**", docSelector);
const allFiles = glob.sync(selector, { ignore });
allFiles.forEach((file) => {
const p = path.parse(file);
// intermediate dir names between this dir and root
const relPath = path.relative(rootPath, p.dir);
const midFolders = relPath.split(path.sep);
// build intermediate parent sections in the event that this is deeply-nested
while (midFolders.length) {
const sectionPath = path.join(rootPath, ...midFolders);
if (!sections.has(sectionPath)) {
// create new section
const section = newSection(sectionPath, ignore);
sections.set(sectionPath, section);
// register relationship for tree-build later
const parentPath = path.join(sectionPath, "..");
childToParent.set(sectionPath, parentPath);
}
midFolders.pop();
}
// if it's a MD file, add to parent as a subsection
const section = sections.get(p.dir);
if (section && isSubsection(section, file)) {
section.children.add({
name: titleize(humanize(p.name)),
content: file,
});
}
});
// assemble recursive section tree under rootNode
buildSectionTree(sections, childToParent);
// rootNode is the main result, returning the maps so the user has the option to manipulate the
// tree before handing it over to the styleguide configs
return { sections, childToParent, rootNode };
}
function isSubsection(section, file) {
const p = path.parse(file);
// it's not a subsection if it's not a markdown file
if (p.ext !== ".md") return false;
// it's not a subsection if it's the same file as the section content
if (file === section.content) return false;
// it's not a subsection if it's an example for an existing component
const matchingComponentPath = path.join(p.dir, `${p.name}.vue`);
const componentExists = fs.existsSync(matchingComponentPath);
if (componentExists) return false;
return true;
}
/**
* Creates a new section in the TOC from passed path.
*
* @param {string} dir absolute directory path
* @return {Object} section object
*/
function newSection(dir, ignore = []) {
const section = {
name: titleize(humanize(path.basename(dir))),
components: () => glob.sync(path.join(dir, "*.vue"), { ignore }),
children: new Set(),
sectionDepth: 1,
};
// summary doc is readme/docs/index.md
const summarySelector = path.join(dir, "@(readme|docs|index).md");
const summaryDocs = glob.sync(summarySelector, { ignore, nocase: true });
if (summaryDocs.length) {
section.content = summaryDocs[0];
}
return section;
}
/**
* Turns the pair of maps into a nested object for use in the styleguide config script. Operates on
* sections array by reference.
*
* @param {Map} sections Map of path -> section object
* @param {Map} childToParent Map of childPath -> parentPath
*/
function buildSectionTree(sections, childToParent) {
// add each child to its parent
for (const [childPath, parentPath] of childToParent) {
const child = sections.get(childPath);
const parent = sections.get(parentPath);
if (child && parent && parent.children) {
parent.children.add(child);
}
}
// convert child Sets to arrays now that de-dupe not required
for (const section of sections.values()) {
section.sections = Array.from(section.children).sort((a, b) => a.name.localeCompare(b.name));
delete section.children;
}
}
module.exports = {
// public function
getDocSections,
// exporting these in case user wants to tweak the result tree and rebuild it
buildSectionTree,
newSection,
};
@@ -0,0 +1,73 @@
Without a doubt. The most problematic part of the old client is the way every important piece of
information was hung on a global variable whose initialization is completely unregulated.
### Don't directly reference window.Galaxy in Vue components
Components take props. Please pass in any values you might need from window.Galaxy as props and
avoid referencing global Galaxy inside your components. I've even created basic providers which give
you access to the Galaxy.config, current user, and current user histories. Please use them to
retrieve your values, and bypass importing Galaxy altogether.
#### Sometimes you still need to update Vue from backbone as the legacy environment changes
There are definitely use-cases where the Backbone models update over time and we need to update some
value inside Vue. Instead of importing backbone models directly into Vue components, try building a
backbone event listener that updates some relevant Vuex store.
* [Keeping Vuex in Sync with
Galaxy](https://github.com/galaxyproject/galaxy/blob/dev/client/src/store/syncVuexToGalaxy.js)
These issues should disappear over time as the all of the old client is rebuilt in the new
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
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
several mounting functions for various components depending on where that component is intended to
fit into the existing Backbone layouts.
### Using the standard mount to pass in Galaxy variables as props
A standard mount function has been provided in src/utils. This mount function accepts a component
definition, and then allows you to start up your Vue component with our standard load-out of plugins
including localization, Vuex, and a couple other utilities. This is the preferred way to mount your
component inside the old Backbone layout until the application is fully converted.
```js static
// src/mvc/OldBackboneView.js
import { getGalaxyInstance } from "app";
import MyComponent from "components/MyComponent";
import { mountVueComponent } from "utils/mountVueComponent";
const OldBackboneView = {
someInitMethodYouMake() {
const Galaxy = getGalaxyInstance();
const mounter = mountVueComponent(MyComponent);
// pass in required props
const props = {
// Something peeled off the global galaxy
somePropVal: Galaxy.someDealie,
// ...or the current history
name: Galaxy.currentHistory.name,
// or maybe from the backbone model for this view
shoeSize: this.model.shoeSize
};
// VM is a Vue instance.
// this.$el is some jquery selection, first item is the actual DOM object
const container = this.$el[0];
const vm = mounter(props, container);
}
}
```
@@ -0,0 +1,18 @@
Did you know, jQuery is old enough to drive? It's old enough to get a driver's license. jQuery is a
tool that was built to deal with inconsistencies in browsers that NO LONGER EXIST. In a couple
years, jQuery will be voting, drinking, and capable of being tried as an adult.
If you think you need jQuery, you are mistaken. Please seek help from somebody in the wg-ui-ux
workgroup. There is nothing jQuery can provide you that isn't already part of vanilla javascript or
a standard well-tested 3rd party modern npm module.
But that's not even the main problem. jQuery injects global initializations into every page on the
site whether you want it or not. jQuery leverages an outdated initialization paradigm which is
hugely problematic when it comes to unit testing and module building.
One of our most important goals in redesigning Galaxy is the complete elimination of this library
from our source, along with all its invasive plugins.
### References
* [You Don't Need jQuery](https://github.com/nefe/You-Dont-Need-jQuery)
* [document.querySelector](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
@@ -0,0 +1,164 @@
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
information comes in as properties, any internal variables get defined in "data", changes go out as
events.
### Composition Example
```html static
<!-- Use of a renderless component with a display component -->
<DoodadProvider v-slot="{ doodad, saveDoodad }">
<DoodadEditor :doodad="doodad" @update:doodad="saveDoodad" />
</DoodadProvider>
```
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
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.
## The Renderer
```html static
<!-- DoodadEditor.vue, a simple "rendering" component -->
<template>
<AutoComplete
:options="options"
:value="doodad.category"
@select="saveCategory"
/>
</template>
<script>
export default {
props: {
doodad: { type: Object, required: true },
options: { type: Array, required: true },
},
methods: {
saveCategory(newCategory) {
this.$emit('update:doodad', { ...this.doodad, category: newCategory });
}
}
}
</script>
```
This component accepts a mandatory input object (doodad), lets the user play with a category prop,
then emits a fresh object after it's done. So what, what's the big deal? The important part to walk
away from this dumb example is the things that are NOT in this sample component.
This component doesn't save the data. This component doesn't make ajax calls, and this component
doesn't mutate its props. What it does do is to allow the user to edit some object named "doodad"
and emits a new fresh version of that doodad when it's done. (Note also that we are using the
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.
## The Provider
As the opposite of the rendering component, a provider or renderless component, is pure logic. It
should not know or care what your renderer is going to do with the data it provides. It is simply a
fancy way of configuring some data manipulation methods. This is one of the many ways of reusing
functionality available in Vue. Some others are [Mixins](https://vuejs.org/v2/guide/mixins.html),
[Provide/Inject](https://v3.vuejs.org/guide/component-provide-inject.html) and (in Vue3) [the
composition API](https://v3.vuejs.org/guide/composition-api-introduction.html).
```js static
// DoodadProvider.js
import { loadDoodad, saveDoodad } from "./someAjaxQueryModule";
export default {
data() {
return {
loaded: false,
doodad: null
}
},
methods: {
async saveDoodad(newVal) {
this.loaded = false;
const newVal = await saveDoodad(newVal);
this.doodad = newVal;
this.loaded = true;
},
async loadDoodad(newVal) {
this.loaded = false;
const newVal = await loadDoodad(newVal);
this.loaded = true;
return newVal;
}
},
async created() {
this.doodad = await loadDoodad();
},
render() {
return return this.$scopedSlots.default({
loaded: this.loaded,
doodad: this.doodad,
saveDoodad: this.saveDoodad
});
}
}
```
Here is an example of a simple possible renderless provider. The important part about this is the
render() function which simply renders [one big default
slot](https://vuejs.org/v2/guide/components-slots.html) and binds some of its own properties to that
slot for use by downstream components.
## Unit Testing a renderless component
Testing of a component like our editor is pretty straightforward and follows the standard Vue
guidelines. If you did a clean-enough job you won't even need to mock anything since all your data
dependencies should be delivered via props.
But it's not so obvious how to unit-test a renderless provider. What do you check for? There's no
mandatory markup, just one big empty slot.
```js static
// Testing a renderless component
import { shallowMount } from "@vue/test-utils";
import { getLocalVue, waitForLifecyleEvent } from "jest/helpers";
import DoodadProvider from "./DoodadProvider";
describe("A renderless component", () => {
const localVue = getLocalVue();
let wrapper;
let slotProps;
beforeEach(async () => {
wrapper = shallowMount(DoodadProvider, {
localVue,
// The mount fn allows you to hook into a slot for this very reason
scopedSlots: {
default(props) {
slotProps = props;
},
},
});
// waits for "updated" Vue lifecycle hook to fire on the renderless
// component. This is often good enough for waiting for
// an initial ajax load to finish, for example
await waitForLifecyleEvent(wrapper.vm, "updated");
})
test("someProp", () => {
const { someProp } = slotProps;
expect(someProp).toExist();
// ...more tests
})
})
```
@@ -0,0 +1,71 @@
### A component is really just a fancy function
I'm not talking about how webpack turns it into a rendering function. That's obvious.
I mean conceptually, props come in (like arguments) and events go out (like the return statements).
A component is a fancy kind of function that can keep emitting results and accept changing inputs
over time. In truth it more closely resembles an Observable, but an observable is ALSO a slightly
fancier kind of function.
If you just think of a component as thing that takes input props and emits output events you're well
on your way to using them well.
The worst components are ones that might as well just be a big single page script. That's zero
percent better than the spaghetti we're working so hard to replace. That's just repackaging all the
problems of the old imperative class-based legacy code.
### Get comfortable with events, limit your dependence on Vuex
New vue programmers are ok at handing props to components, but they rarely use events effectively
(at first). As a result they end up using a lot of global state, a million little data props and
relying on imperfect globalized tools like Vuex or other imported dependencies for every little
variable.
Vuex definitely has its uses, but not as many as you might expect given the way it is
overly-emphasized in common tutorials. It's easy to walk away from an "Intro to Vue" video with the
idea that all data must live in Vuex all the time. That's a really undesirable situation.
Although vuex is a well-organized (many would say over-organized) state machine, it is important
to remember that it is still a kind of global injection and deserves to be considered as such.
* [Should I Store This Data in
Vuex](https://markus.oberlehner.net/blog/should-i-store-this-data-in-vuex/)
* [Vuex getters are great, but don’t overuse
them](https://codeburst.io/vuex-getters-are-great-but-dont-overuse-them-9c946689b414)
Data persistence should be something that happens near the top of your component tree, not down in
the guts.
Your first thought with a component should be: "How can I offload the handling of the results of
this component to my caller?" The answer is usually going to be events. A component that simply
accepts props and emits events can be re-used in more contexts than one that relies on an external
global state to operate.
#### Read up on .sync and v-model
They're just fancy shorthands for a prop / event handler combination. They are fundamentally no
different from props and events, but the syntax is important to understand.
### Think carefully about what should really be in "data".
Most of good component design boils down to answering the following question: What do I want to put
in data, computed, and props?
Data is the place where temp data goes that is not sensible to persist in Vuex or other
application-wide global state, usually because its use is very specific to the operation of this
particular component. There really should only be a few variables in data.
#### Break down your own internal dependencies
Most components only need one or two variables in data. If you have a large amount of data
variables, it's worth taking a little time to stop and build a mini-dependency tree for yourself.
You will probably find that almost everything can be written in terms of computed transformations on
a small number of data and properties.
Defining your values using computes will lead to a lot less "oh I forgot to update that
variable"-style bugs.
If you have more than a few data variables you probably (a) aren't leveraging computeds and
properties, or (b) are trying to implement too many features for just one component. It is important
separate your concerns in components just like you do in any other kind of programming.
@@ -0,0 +1,20 @@
[Galaxy uses Jest](https://jestjs.io/) for its client-side unit testing
framework.
For testing Vue components, we use the [Vue testing
utils](https://vue-test-utils.vuejs.org/) to mount individual components in a
test bed and check them for rendered features. Please use jest-based mocking
for isolating test functionality.
### Specific test scenarios & examples
* [Mocking an imported
dependency](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/tagService.test.js)
* [Testing async
operations](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/tagService.test.js)
* [Testing a Vue component for expected rendering
output](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/StatelessTags.test.js)
* [Firing an event against a shallow mounted vue
component](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/StatelessTags.test.js)
@@ -0,0 +1,59 @@
Part of making good code is making that code easy to test.
### Implement logic in pure functions when possible
The more of your logic that is written in deterministic functions (i.e. no
side-effects, same inputs always result in same outputs) the easier it is to
test. Just load up the functions and supply suitable test inputs.
There is almost definitely no such thing as a well-written 1000 line function.
Most of whatever happened in that thing was probably deterministic and can be
broken up into easily testable chunks.
### Wrap native browser resources in a function so they can be easily mocked
If your javascript needs to talk to the window object, or navigator, etc. wrap
that in a function call so that it can be easily mocked during testing.
#### Your Module
```js static
// myModule.js
// ... other code
export function redirectTo(url) {
window.location = url;
}
export function theThingYouReallyCareAbout() {
.....
redirectTo(somePlace);
....
}
```
#### Your test file
```js static
// myModule.test.js
import { theThingYouReallyCareAbout, redirectTo } from "./myModule";
// calling jest.mock on the appropriate import path wraps the exports and gives
// access to new testing methods like mockImplementation
jest.mock("./myModule");
redirectTo.mockImplementation((url) => {
console.log(`I would have gone to: ${url}`);
});
// You are testing theThingYouReallyCareAbout, but that function needs
// to call a window.relocate, By wrapping redirectTo as a function it was easy to mock
// and avoid errors in your test, because native browser objects do not even exist
// in the unit-testing environment.
describe("theThingYouReallyCareAbout", () => {
it("Performs a redirect when Foo is clicked"){
const result = theThingYouReallyCareAbout(abc123);
expect(result).toBe(1);
}
});
```
@@ -0,0 +1,73 @@
### Clearly document the intent of your test
Please remember that these tests are not _for_ you. They're for the people who
come after you. It will be a lot easier to modify, repair and upgrade your code
if they can figure out what you were originally hoping to accomplish. Try to
use as detailed 'expect' statements as possible -- overuse of 'toBeTruthy()'
for example, can hide the intent of your test.
Add a couple of comments. Use variable names that mean something. Nobody's
code is as self-documenting as they believe it to be.
### Only test the public API that you define
Internal implementations come and go with library upgrades and new tech. But
the point of the unit test is to make sure your units work as designed....
which means you need to... you know... design your code to work in units.
Separate your concerns and identify the developer-facing methods and functions
you expect them to use. Test THOSE. Everything else should probably be
considered an implementation detail.
The other side of the same coin is to test *only* the unit in question. If your
component has a model that uses a service that touches Vuex, which then uses
Axios to fetch some data -- don't test all that at once. Break things apart and
mock functionality to isolate testing to units. End to end testing is a
separate thing that shouldn't be attempted using spec tests in Jest.
Assume nobody cares _how_ your code works, we just need to know that the public
API you designed _does_ work. If performance problems or new tech necessitate a
re-write, these tests become a guide for the next implementation.
### Writing a test file
Jest will try to test any file ending in "\*.test.js". Please place your test
files inside client/src folders right next to whatever files that they are
testing.
Jest has extensive documentation on the expect API, mocking, and more on the
[official docs page](https://jestjs.io/docs/en/getting-started.html), which will
be your best resource here.
```js static
// yourcode.test.js
import { things } from "./yourcode.js";
describe("some module you wrote", () => {
let transientVariables;
let serviceInstances;
let testData;
beforeEach(() => {
// setup your code (if necessary)
});
afterEach(() => {
// teardown your code (so it doesn't ruin the next test)
});
it("should do something or other", () => {
expect(workflowNodeCount()).toBe(5);
});
});
```
### Check out the Jest helper functions
We have created some [common helpers for common testing
scenarios](https://github.com/galaxyproject/galaxy/blob/dev/client/tests/jest/helpers.js).
+1
View File
@@ -0,0 +1 @@
# testdocs/a-file.md
@@ -0,0 +1 @@
# testdocs/another-file.md
@@ -0,0 +1,3 @@
<template>
<div>Yay!</div>
</template>
@@ -0,0 +1 @@
# ignored-file.md
@@ -0,0 +1 @@
# some-loose-file.md
@@ -0,0 +1 @@
# loose file
@@ -0,0 +1 @@
# testdocs/abc/readme.md
@@ -0,0 +1 @@
# foo
@@ -0,0 +1 @@
# another loose file
@@ -0,0 +1 @@
# Loose file
+62
View File
@@ -0,0 +1,62 @@
/**
* Tests the functions which build the sections for the styleguide.
*/
const path = require("path");
const { getDocSections } = require("../sections");
const getSectionByName = (sections, name) => sections.find((o) => o.name == name);
describe("getDocSections", () => {
const ignore = [
// ignoring a whole directory
"**/ignored/*",
// ignoring a file
"**/ignore*",
];
const testDocRoot = path.join(__dirname, "sample-docs");
const { rootNode } = getDocSections(testDocRoot, { ignore });
test("section generation", () => {
expect(rootNode.name).toEqual("Sample Docs");
expect(rootNode.sections.length).toEqual(7);
expect(rootNode.content).toBeUndefined();
});
test("nested subsection should appear even if no docs in intermediate folders", () => {
const deepSection = getSectionByName(rootNode.sections, "Nested Folders");
expect(deepSection.sections.length).toEqual(1);
});
test("subdirectory with readme should register as summary", () => {
const subsection = getSectionByName(rootNode.sections, "Has Readme");
// readme interpreted as content file and not as loose section
// one other loose file
expect(subsection.content).toContain("readme.md");
expect(subsection.sections.length).toEqual(1);
});
test("subdirectory with no readme file", () => {
const subsection = getSectionByName(rootNode.sections, "No Summary File");
// should just see 2 folders no summary
expect(subsection.content).toBeUndefined();
expect(subsection.sections.length).toEqual(2);
});
test("subdirectory with ignored file", () => {
const subsection = getSectionByName(rootNode.sections, "Contains Ommitted File");
expect(subsection.sections.length).toEqual(1);
});
test("ignored subdirectory", () => {
const ignoredSection = getSectionByName(rootNode.sections, "Ignored");
expect(ignoredSection).toBeUndefined();
});
test("should not create a section for a component example file", () => {
const section = getSectionByName(rootNode.sections, "Component Example");
expect(section.sections.length).toEqual(0);
});
});
+69
View File
@@ -0,0 +1,69 @@
### Beta History Panel Component Tree
This is not intended to be a complete listing, but a general idea of how the components are intended
to interact with each other.
```html static
<CurrentHistoryPanel>
<HistoryPanel :history="history">
<!-- for the right-hand side history we show some
optional nav elements, can be ommitted for histories
shown in multi-history view -->
<slot:nav>
<HistorySelector />
<HistoryMenu />
</slot:nav>
<!-- if main history selected -->
<History :history="history">
<!-- HCP does the heavy-lifting of mixing params, history, and
scroll position to deliver the content for the scroller -->
<HistoryContentProvider :parent="history">
<HistoryDetails />
<HistoryMessages />
<ContentOperations />
<Scroller>
<!-- HistoryContentItem is a dynamic component that becomes
either Dataset or DatasetCollection depending
on the props passed to it -->
(<HistoryContentItem />)
<Dataset />
<!-- or -->
<DatasetCollection />
</Scroller>
</HistoryContentProvider>
</History>
<!-- When a collection is selected for viewing, send in a
breadcrumbs list of collections the user has selected -->
<CurrentCollection :selected-collections="breadcrumbs">
<CollectionContentProvider :parent="selectedCollection">
<CollectionNav />
<Details />
<Scroller>
<!-- Subdataset and Subcollection are similar to the Dataset
and DatasetCollection ContentItem components, but mostly
read-only since they are part of the collection-->
(<CollectionContentItem />)
<Subdataset />
<!-- or -->
<Subcollection />
</Scroller>
</CollectionContentProvider>
</CurrentCollection>
</HistoryPanel>
</CurrentHistoryPanel>
```
@@ -61,7 +61,7 @@
<p class="text-center" v-if="is_unrestricted">
You can
<strong @click="toggleDatasetPrivacy(true)" class="make-private">
<a href="javascript:void(0)">make this dataset private</a>
<a id="make-private" href="javascript:void(0)">make this dataset private</a>
</strong>
to you.
</p>
@@ -5,7 +5,7 @@
</h4>
<b-row>
<b-col>
<div v-if="options && value">
<div :class="permission_type" v-if="options && value">
<multiselect
v-model="value"
:options="fetched_options"
@@ -109,6 +109,7 @@
@onEdit="onEdit"
@onAttributes="onAttributes"
@onLint="onLint"
@onUpgrade="onUpgrade"
/>
</div>
</div>
@@ -422,6 +423,9 @@ export default {
this.$refs.lint.refresh();
showLint();
},
onUpgrade() {
this.attemptRefactor([{ action_type: "upgrade_all_steps" }]);
},
onEdit() {
this.isCanvas = true;
},
@@ -60,6 +60,9 @@
<b-dropdown-item href="#" @click="$emit('onLayout')"
><span class="fa fa-align-left mr-1" />Auto Layout</b-dropdown-item
>
<b-dropdown-item href="#" @click="$emit('onUpgrade')"
><span class="fa fa-recycle mr-1" />Upgrade All Workflow Steps</b-dropdown-item
>
<b-dropdown-item href="#" @click="$emit('onDownload')"
><span class="fa fa-download mr-1" />Download</b-dropdown-item
>
+45 -46
View File
@@ -1,58 +1,57 @@
const path = require("path");
const glob = require("glob");
const fs = require("fs");
const merge = require("webpack-merge");
const baseConfig = require("./webpack.config.js");
const { getDocSections } = require("./docs/sections");
const buildWebpack = require("./webpack.config.js");
const webpackConfig = baseConfig();
function getWebpack() {
const cfg = buildWebpack();
const fileLoaderTest = /\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)(\?.*$|$)/;
// looks like our src plays with the webpack publicPath dynamically,
// presumably to allow for dyamic loads, but this is a problem when
// you're not outputting code to a non-standard location.
// allowing this to happen breaks the styleguide.
cfg.module.rules.push({
test: /onload\/publicPath/,
use: { loader: "ignore-loader" },
});
const fileLoaderConfigRule = { rules: [{ test: fileLoaderTest, use: ["file-loader"] }] };
return cfg;
}
webpackConfig.module = merge.smart(webpackConfig.module, fileLoaderConfigRule);
webpackConfig.output.publicPath = "";
// TODO: Fix broken module imports before attempting to view in styleguidef
const problemChildren = ["**/HistoryView.vue", "**/admin/DataManager/*", "**/LibraryFolder/*"];
if (problemChildren.length) {
console.warn("Not rendering styleguide for the following components:", problemChildren);
}
webpackConfig.resolve.modules.push(path.join(__dirname, "src/style/scss"));
function getSections() {
// Style sections
const docRootPath = path.join(__dirname, "docs/src");
const { rootNode: docRoot } = getDocSections(docRootPath, { docSelector: "*.md" });
const [design, styles] = docRoot.sections;
delete docRoot.components;
const galaxyStyleDocs = [];
glob.sync("./docs/galaxy-*.md").forEach((file) => {
const name = file.match(/galaxy-(\w+).md/)[1];
galaxyStyleDocs.push({ name: name, content: file });
});
// recursive component tree docs
const cmpPath = path.join(__dirname, "src/components");
const { rootNode: componentDocs } = getDocSections(cmpPath, { ignore: problemChildren });
const sections = [
{
name: "Galaxy styles",
sections: galaxyStyleDocs,
},
{
name: "Basic Bootstrap Styles",
content: "./docs/bootstrap.md",
},
{
name: "Components",
// Components that are directories will get their own section
sections: glob
.sync("./src/components/*")
.map((file) => {
if (fs.lstatSync(file).isDirectory()) {
return {
name: path.basename(file),
components: file + "/**/*.vue",
};
}
})
.filter((v) => v),
// ...while top level components are handled here.
components: "./src/components/*.vue",
},
];
return [design, styles, componentDocs];
}
module.exports = {
webpackConfig,
webpackConfig: getWebpack(),
title: "Galaxy Client Resources",
sections: getSections(),
getExampleFilename(componentPath) {
return componentPath.replace(/\.(vue|js)?$/, ".md");
},
require: [
"./src/style/scss/base.scss",
"./src/polyfills.js",
// "./src/bundleEntries.js"
],
tocMode: "collapse",
renderRootJsx: "./docs/root",
styleguideDir: "./docs/dist",
pagePerSection: true,
sections,
require: ["./src/style/scss/base.scss", "./src/polyfills.js", "./src/bundleEntries.js"],
vuex: "./src/store/index.js",
ignore: problemChildren,
};
+1 -1
View File
@@ -118,7 +118,7 @@ module.exports = {
rootDir: path.join(__dirname, "../../"),
// A list of paths to directories that Jest should use to search for files in
roots: ["<rootDir>/src", "<rootDir>/tests/jest/standalone/"],
roots: ["<rootDir>/src", "<rootDir>/tests/jest/standalone/", "<rootDir>/docs/"],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
+2 -2
View File
@@ -53,7 +53,7 @@ function addIframe() {
fetch("/training-material/")
.then((response) => {
if (!response.ok) {
url = "https://training.galaxyproject.org/training-material/";
url = "https://training.galaxyproject.org/training-material/?utm_source=webhook&utm_medium=noproxy&utm_campaign=gxy";
message = `
<span>
<a href="https://docs.galaxyproject.org/en/master/admin/special_topics/gtn.html">Click to run</a> unavailable.
@@ -70,7 +70,7 @@ function addIframe() {
onloadscroll = storedLocation.split(" ")[0];
url = storedLocation.split(" ")[1];
} else {
url = "/training-material/";
url = "/training-material/?utm_source=webhook&utm_medium=proxy&utm_campaign=gxy";
}
message = "";
}
+69 -47
View File
@@ -4,13 +4,18 @@ import sys
import time
from typing import Any
from sqlalchemy.orm.scoping import (
scoped_session,
)
import galaxy.model
import galaxy.model.security
import galaxy.queues
import galaxy.security
from galaxy import config, jobs
from galaxy import auth, config, jobs
from galaxy.config_watchers import ConfigWatchers
from galaxy.containers import build_container_interfaces
from galaxy.datatypes.registry import Registry
from galaxy.files import ConfiguredFileSources
from galaxy.job_metrics import JobMetrics
from galaxy.managers.api_keys import ApiKeyManager
@@ -19,25 +24,30 @@ from galaxy.managers.folders import FolderManager
from galaxy.managers.hdas import HDAManager
from galaxy.managers.histories import HistoryManager
from galaxy.managers.interactivetool import InteractiveToolManager
from galaxy.managers.jobs import JobSearch
from galaxy.managers.libraries import LibraryManager
from galaxy.managers.roles import RoleManager
from galaxy.managers.session import GalaxySessionManager
from galaxy.managers.tools import DynamicToolManager
from galaxy.managers.users import UserManager
from galaxy.managers.workflows import (
WorkflowContentsManager,
WorkflowsManager,
)
from galaxy.model.base import SharedModelMapping
from galaxy.model.database_heartbeat import DatabaseHeartbeat
from galaxy.model.mapping import GalaxyModelMapping
from galaxy.model.tags import GalaxyTagHandler
from galaxy.queue_worker import (
GalaxyQueueWorker,
send_local_control_task,
)
from galaxy.quota import get_quota_agent
from galaxy.quota import get_quota_agent, QuotaAgent
from galaxy.security.idencoding import IdEncodingHelper
from galaxy.tool_shed.galaxy_install.installed_repository_manager import InstalledRepositoryManager
from galaxy.tool_shed.galaxy_install.update_repository_manager import UpdateRepositoryManager
from galaxy.tool_util.deps.views import DependencyResolversView
from galaxy.tool_util.verify import test_data
from galaxy.tool_util.verify.test_data import TestDataResolver
from galaxy.tools.cache import (
ToolCache,
ToolShedRepositoryCache
@@ -45,7 +55,7 @@ from galaxy.tools.cache import (
from galaxy.tools.data_manager.manager import DataManagers
from galaxy.tools.error_reports import ErrorReports
from galaxy.tools.special_tools import load_lib_tools
from galaxy.tours import build_tours_registry
from galaxy.tours import build_tours_registry, ToursRegistry
from galaxy.util import (
ExecutionTimer,
heartbeat,
@@ -56,19 +66,23 @@ from galaxy.visualization.genomes import Genomes
from galaxy.visualization.plugins.registry import VisualizationsRegistry
from galaxy.web import url_for
from galaxy.web.proxy import ProxyManager
from galaxy.web_stack import application_stack_instance
from galaxy.web_stack import application_stack_instance, ApplicationStack
from galaxy.webhooks import WebhooksRegistry
from galaxy.workflow.trs_proxy import TrsProxy
from .structured_app import StructuredApp
from .di import Container
from .structured_app import BasicApp, StructuredApp
log = logging.getLogger(__name__)
app = None
class UniverseApplication(StructuredApp, config.ConfiguresGalaxyMixin):
class UniverseApplication(StructuredApp, config.ConfiguresGalaxyMixin, Container):
"""Encapsulates the state of a Universe application"""
def __init__(self, **kwargs) -> None:
super().__init__()
self._register_singleton(BasicApp, self)
self._register_singleton(StructuredApp, self)
if not log.handlers:
# Paste didn't handle it, so we need a temporary basic log
# configured. The handler added here gets dumped and replaced with
@@ -80,19 +94,19 @@ class UniverseApplication(StructuredApp, config.ConfiguresGalaxyMixin):
startup_timer = ExecutionTimer()
self.new_installation = False
# Read config file and check for errors
self.config: Any = config.Configuration(**kwargs)
self.config: Any = self._register_singleton(config.Configuration, config.Configuration(**kwargs))
self.config.check()
config.configure_logging(self.config)
self.execution_timer_factory = ExecutionTimerFactory(self.config)
self.execution_timer_factory = self._register_singleton(ExecutionTimerFactory, ExecutionTimerFactory(self.config))
self.configure_fluent_log()
# A lot of postfork initialization depends on the server name, ensure it is set immediately after forking before other postfork functions
self.application_stack = application_stack_instance(app=self)
self.application_stack = self._register_singleton(ApplicationStack, application_stack_instance(app=self))
self.application_stack.register_postfork_function(self.application_stack.set_postfork_server_name, self)
self.config.reload_sanitize_allowlist(explicit='sanitize_allowlist_file' in kwargs)
self.amqp_internal_connection_obj = galaxy.queues.connection_from_config(self.config)
# queue_worker *can* be initialized with a queue, but here we don't
# want to and we'll allow postfork to bind and start it.
self.queue_worker = GalaxyQueueWorker(self)
self.queue_worker = self._register_singleton(GalaxyQueueWorker, GalaxyQueueWorker(self))
self._configure_tool_shed_registry()
self._configure_object_store(fsmon=True)
@@ -105,23 +119,30 @@ class UniverseApplication(StructuredApp, config.ConfiguresGalaxyMixin):
# Security helper
self._configure_security()
self._register_singleton(IdEncodingHelper, self.security)
self._register_singleton(SharedModelMapping, self.model)
self._register_singleton(GalaxyModelMapping, self.model)
self._register_singleton(scoped_session, self.model.context)
# Tag handler
self.tag_handler = GalaxyTagHandler(self.model.context)
self.dataset_collections_service = DatasetCollectionManager(self)
self.history_manager = HistoryManager(self)
self.hda_manager = HDAManager(self)
self.workflow_manager = WorkflowsManager(self)
self.workflow_contents_manager = WorkflowContentsManager(self)
self.dependency_resolvers_view = DependencyResolversView(self)
self.test_data_resolver = test_data.TestDataResolver(file_dirs=self.config.tool_test_data_directories)
self.library_folder_manager = FolderManager()
self.library_manager = LibraryManager()
self.role_manager = RoleManager(self)
self.dynamic_tool_manager = DynamicToolManager(self)
self.api_keys_manager = ApiKeyManager(app)
self.tag_handler = self._register_singleton(GalaxyTagHandler)
self.user_manager = self._register_singleton(UserManager)
self._register_singleton(GalaxySessionManager)
self.hda_manager = self._register_singleton(HDAManager)
self.history_manager = self._register_singleton(HistoryManager)
self.job_search = self._register_singleton(JobSearch)
self.dataset_collections_service = self._register_singleton(DatasetCollectionManager)
self.workflow_manager = self._register_singleton(WorkflowsManager)
self.workflow_contents_manager = self._register_singleton(WorkflowContentsManager)
self.dependency_resolvers_view = self._register_singleton(DependencyResolversView, DependencyResolversView(self))
self.test_data_resolver = self._register_singleton(TestDataResolver, TestDataResolver(file_dirs=self.config.tool_test_data_directories))
self.library_folder_manager = self._register_singleton(FolderManager)
self.library_manager = self._register_singleton(LibraryManager)
self.role_manager = self._register_singleton(RoleManager)
self.dynamic_tool_manager = self._register_singleton(DynamicToolManager)
self.api_keys_manager = self._register_singleton(ApiKeyManager)
# ConfiguredFileSources
self.file_sources = ConfiguredFileSources.from_app_config(self.config)
self.file_sources = self._register_singleton(ConfiguredFileSources, ConfiguredFileSources.from_app_config(self.config))
# Tool Data Tables
self._configure_tool_data_tables(from_shed_config=False)
@@ -129,36 +150,37 @@ class UniverseApplication(StructuredApp, config.ConfiguresGalaxyMixin):
self._configure_genome_builds(data_table_name="__dbkeys__", load_old_style=True)
# Genomes
self.genomes = Genomes(self)
self.genomes = self._register_singleton(Genomes)
# Data providers registry.
self.data_provider_registry = DataProviderRegistry()
self.data_provider_registry = self._register_singleton(DataProviderRegistry)
# Initialize job metrics manager, needs to be in place before
# config so per-destination modifications can be made.
self.job_metrics = JobMetrics(self.config.job_metrics_config_file, app=self)
self.job_metrics = self._register_singleton(JobMetrics, JobMetrics(self.config.job_metrics_config_file, app=self))
# Initialize error report plugins.
self.error_reports = ErrorReports(self.config.error_report_file, app=self)
self.error_reports = self._register_singleton(ErrorReports, ErrorReports(self.config.error_report_file, app=self))
# Initialize the job management configuration
self.job_config = jobs.JobConfiguration(self)
self.job_config = self._register_singleton(jobs.JobConfiguration)
# Setup a Tool Cache
self.tool_cache = ToolCache()
self.tool_shed_repository_cache = ToolShedRepositoryCache(self)
self.tool_cache = self._register_singleton(ToolCache)
self.tool_shed_repository_cache = self._register_singleton(ToolShedRepositoryCache)
# Watch various config files for immediate reload
self.watchers = ConfigWatchers(self)
self.watchers = self._register_singleton(ConfigWatchers)
self._configure_tool_config_files()
self.installed_repository_manager = InstalledRepositoryManager(self)
self.installed_repository_manager = self._register_singleton(InstalledRepositoryManager, InstalledRepositoryManager(self))
self._configure_datatypes_registry(self.installed_repository_manager)
self._register_singleton(Registry, self.datatypes_registry)
galaxy.model.set_datatypes_registry(self.datatypes_registry)
self._configure_toolbox()
# Load Data Manager
self.data_managers = DataManagers(self)
self.data_managers = self._register_singleton(DataManagers)
# Load the update repository manager.
self.update_repository_manager = UpdateRepositoryManager(self)
self.update_repository_manager = self._register_singleton(UpdateRepositoryManager, UpdateRepositoryManager(self))
# Load proprietary datatype converters and display applications.
self.installed_repository_manager.load_proprietary_converters_and_display_applications()
# Load datatype display applications defined in local datatypes_conf.xml
@@ -171,26 +193,26 @@ class UniverseApplication(StructuredApp, config.ConfiguresGalaxyMixin):
load_lib_tools(self.toolbox)
self.toolbox.persist_cache(register_postfork=True)
# visualizations registry: associates resources with visualizations, controls how to render
self.visualizations_registry = VisualizationsRegistry(
self.visualizations_registry = self._register_singleton(VisualizationsRegistry, VisualizationsRegistry(
self,
directories_setting=self.config.visualization_plugins_directory,
template_cache_dir=self.config.template_cache_path)
template_cache_dir=self.config.template_cache_path))
# Tours registry
self.tour_registry = build_tours_registry(self.config.tour_config_dir)
tour_registry = build_tours_registry(self.config.tour_config_dir)
self.tour_registry = tour_registry
self[ToursRegistry] = tour_registry # type: ignore
# Webhooks registry
self.webhooks_registry = WebhooksRegistry(self.config.webhooks_dir)
self.webhooks_registry = self._register_singleton(WebhooksRegistry, WebhooksRegistry(self.config.webhooks_dir))
# Load security policy.
self.security_agent = self.model.security_agent
self.host_security_agent = galaxy.model.security.HostAgent(
model=self.security_agent.model,
permitted_actions=self.security_agent.permitted_actions)
# Load quota management.
self.quota_agent = get_quota_agent(self.config, self.model)
self.quota_agent = self._register_singleton(QuotaAgent, get_quota_agent(self.config, self.model))
# Heartbeat for thread profiling
self.heartbeat = None
from galaxy import auth
self.auth_manager = auth.AuthManager(self)
self.user_manager = UserManager(self)
self.auth_manager = self._register_singleton(auth.AuthManager, auth.AuthManager(self.config))
# Start the heartbeat process if configured and available (wait until
# postfork if using uWSGI)
if self.config.use_heartbeat:
@@ -221,7 +243,7 @@ class UniverseApplication(StructuredApp, config.ConfiguresGalaxyMixin):
# Start the job manager
from galaxy.jobs import manager
self.job_manager = manager.JobManager(self)
self.job_manager = self._register_singleton(manager.JobManager)
self.application_stack.register_postfork_function(self.job_manager.start)
self.proxy_manager = ProxyManager(self.config)
@@ -229,7 +251,7 @@ class UniverseApplication(StructuredApp, config.ConfiguresGalaxyMixin):
# Must be initialized after job_config.
self.workflow_scheduling_manager = scheduling_manager.WorkflowSchedulingManager(self)
self.trs_proxy = TrsProxy(self.config)
self.trs_proxy = self._register_singleton(TrsProxy, TrsProxy(self.config))
# Must be initialized after any component that might make use of stack messaging is configured. Alternatively if
# it becomes more commonly needed we could create a prefork function registration method like we do with
# postfork functions.
@@ -341,7 +363,7 @@ class UniverseApplication(StructuredApp, config.ConfiguresGalaxyMixin):
self.trace_logger = None
@property
def is_job_handler(self):
def is_job_handler(self) -> bool:
return (self.config.track_jobs_in_database and self.job_config.is_handler) or not self.config.track_jobs_in_database
+3 -4
View File
@@ -12,10 +12,9 @@ log = logging.getLogger(__name__)
class AuthManager:
def __init__(self, app):
self.__app = app
self.redact_username_in_logs = app.config.redact_username_in_logs
self.authenticators = get_authenticators(app.config.auth_config_file, app.config.is_set('auth_config_file'))
def __init__(self, config):
self.redact_username_in_logs = config.redact_username_in_logs
self.authenticators = get_authenticators(config.auth_config_file, config.is_set('auth_config_file'))
def check_registration_allowed(self, email, username, password):
"""Checks if the provided email/username is allowed to register."""
+1 -1
View File
@@ -816,7 +816,7 @@ class GalaxyAppConfiguration(BaseAppConfiguration, CommonConfigurationMixin):
# InteractiveTools propagator mapping file
self.interactivetools_map = self._in_root_dir(kwargs.get("interactivetools_map", self._in_data_dir("interactivetools_map.sqlite")))
self.interactivetools_prefix = kwargs.get("interactivetools_prefix", "interactivetool")
self.interactivetools_proxy_host = kwargs.get("interactivetool_proxy_host", None)
self.interactivetools_proxy_host = kwargs.get("interactivetools_proxy_host", None)
self.containers_conf = parse_containers_config(self.containers_config_file)
+2 -1
View File
@@ -2,6 +2,7 @@ import logging
from os.path import dirname
from galaxy.queue_worker import job_rule_modules
from galaxy.structured_app import StructuredApp
from galaxy.tools.toolbox.watcher import (
get_tool_conf_watcher,
get_tool_watcher,
@@ -14,7 +15,7 @@ log = logging.getLogger(__name__)
class ConfigWatchers:
"""Contains ToolConfWatcher, ToolWatcher and ToolDataWatcher objects."""
def __init__(self, app):
def __init__(self, app: StructuredApp):
self.app = app
self.active = False
# ToolConfWatcher objects will watch the tool_cache if the tool_cache is passed into get_tool_conf_watcher.
+85 -71
View File
@@ -477,9 +477,9 @@ class Bed(Interval):
if not get_headers(file_prefix, '\t', comment_designator='#', count=1):
return False
try:
headers = iter_headers(file_prefix, '\t', comment_designator='#')
for hdr in headers:
if hdr[0] == '':
found_valid_lines = False
for hdr in iter_headers(file_prefix, '\t', comment_designator='#'):
if not hdr or hdr == ['']:
continue
if len(hdr) < 3 or len(hdr) > 12:
return False
@@ -542,7 +542,8 @@ class Bed(Interval):
return False
if len(block_sizes) != block_count or len(block_starts) != block_count:
return False
return True
found_valid_lines = True
return found_valid_lines
except Exception:
return False
@@ -818,28 +819,33 @@ class Gff(Tabular, _RemoteCallMixin):
if len(get_headers(file_prefix, '\t', count=2)) < 2:
return False
try:
headers = iter_headers(file_prefix, '\t')
for hdr in headers:
if hdr and hdr[0].startswith('##gff-version') and hdr[0].find('2') < 0:
found_valid_lines = False
for hdr in iter_headers(file_prefix, '\t'):
if not hdr or hdr == ['']:
continue
if hdr[0].startswith('##gff-version') and hdr[0].find('2') < 0:
return False
if hdr and hdr[0] and not hdr[0].startswith('#'):
if len(hdr) != 9:
return False
# The gff-version header comment may have been stripped, so inspect the data
if hdr[0].startswith('#'):
continue
if len(hdr) != 9:
return False
try:
int(hdr[3])
int(hdr[4])
except Exception:
return False
if hdr[5] != '.':
try:
int(hdr[3])
int(hdr[4])
float(hdr[5])
except Exception:
return False
if hdr[5] != '.':
try:
float(hdr[5])
except Exception:
return False
if hdr[6] not in data.valid_strand:
return False
if hdr[7] not in self.valid_gff_frame:
return False
return True
if hdr[6] not in data.valid_strand:
return False
if hdr[7] not in self.valid_gff_frame:
return False
found_valid_lines = True
return found_valid_lines
except Exception:
return False
@@ -953,37 +959,41 @@ class Gff3(Gff):
if len(get_headers(file_prefix, '\t', count=2)) < 2:
return False
try:
headers = iter_headers(file_prefix, '\t')
for hdr in headers:
if hdr and hdr[0].startswith('##gff-version') and hdr[0].find('3') >= 0:
found_valid_lines = False
for hdr in iter_headers(file_prefix, '\t'):
if not hdr or hdr == ['']:
continue
if hdr[0].startswith('##gff-version') and hdr[0].find('3') >= 0:
return True
elif hdr and hdr[0].startswith('##gff-version') and hdr[0].find('3') < 0:
elif hdr[0].startswith('##gff-version') and hdr[0].find('3') < 0:
return False
# Header comments may have been stripped, so inspect the data
if hdr and hdr[0] and not hdr[0].startswith('#'):
if len(hdr) != 9:
# The gff-version header comment may have been stripped, so inspect the data
if hdr[0].startswith('#'):
continue
if len(hdr) != 9:
return False
try:
int(hdr[3])
except Exception:
if hdr[3] != '.':
return False
try:
int(hdr[4])
except Exception:
if hdr[4] != '.':
return False
if hdr[5] != '.':
try:
int(hdr[3])
float(hdr[5])
except Exception:
if hdr[3] != '.':
return False
try:
int(hdr[4])
except Exception:
if hdr[4] != '.':
return False
if hdr[5] != '.':
try:
float(hdr[5])
except Exception:
return False
if hdr[6] not in self.valid_gff3_strand:
return False
if hdr[7] not in self.valid_gff3_phase:
return False
parse_gff3_attributes(hdr[8])
return True
if hdr[6] not in self.valid_gff3_strand:
return False
if hdr[7] not in self.valid_gff3_phase:
return False
parse_gff3_attributes(hdr[8])
found_valid_lines = True
return found_valid_lines
except Exception:
return False
@@ -1031,34 +1041,38 @@ class Gtf(Gff):
if len(get_headers(file_prefix, '\t', count=2)) < 2:
return False
try:
headers = iter_headers(file_prefix, '\t')
for hdr in headers:
if hdr and hdr[0].startswith('##gff-version') and hdr[0].find('2') < 0:
found_valid_lines = False
for hdr in iter_headers(file_prefix, '\t'):
if not hdr or hdr == ['']:
continue
if hdr[0].startswith('##gff-version') and hdr[0].find('2') < 0:
return False
if hdr and hdr[0] and not hdr[0].startswith('#'):
if len(hdr) != 9:
return False
# The gff-version header comment may have been stripped, so inspect the data
if hdr[0].startswith('#'):
continue
if len(hdr) != 9:
return False
try:
int(hdr[3])
int(hdr[4])
except Exception:
return False
if hdr[5] != '.':
try:
int(hdr[3])
int(hdr[4])
float(hdr[5])
except Exception:
return False
if hdr[5] != '.':
try:
float(hdr[5])
except Exception:
return False
if hdr[6] not in data.valid_strand:
return False
if hdr[7] not in self.valid_gff_frame:
return False
# Check attributes for gene_id (transcript_id is also mandatory
# but not for genes)
attributes = parse_gff_attributes(hdr[8])
if 'gene_id' not in attributes:
return False
return True
if hdr[6] not in data.valid_strand:
return False
if hdr[7] not in self.valid_gff_frame:
return False
# Check attributes for gene_id (transcript_id is also mandatory
# but not for genes)
attributes = parse_gff_attributes(hdr[8])
if 'gene_id' not in attributes:
return False
found_valid_lines = True
return found_valid_lines
except Exception:
return False
+15 -14
View File
@@ -235,24 +235,22 @@ def is_column_based(fname_or_file_prefix, sep='\t', skip=0):
return False
try:
headers = get_headers(fname_or_file_prefix, sep)
headers = get_headers(fname_or_file_prefix, sep, comment_designator='#')[skip:]
except UnicodeDecodeError:
return False
count = 0
if not headers:
return False
for hdr in headers[skip:]:
if hdr and hdr[0] and not hdr[0].startswith('#'):
if len(hdr) > 1:
for hdr in headers:
if hdr and hdr != ['']:
if count:
if len(hdr) != count:
return False
else:
count = len(hdr)
break
if count < 2:
return False
for hdr in headers[skip:]:
if hdr and hdr[0] and not hdr[0].startswith('#'):
if len(hdr) != count:
return False
return True
if count < 2:
return False
return count >= 2
def guess_ext(fname, sniff_order, is_binary=False):
@@ -303,13 +301,13 @@ def guess_ext(fname, sniff_order, is_binary=False):
>>> guess_ext(fname, sniff_order)
'gff3'
>>> fname = get_test_fname('2.txt')
>>> guess_ext(fname, sniff_order) # 2.txt
>>> guess_ext(fname, sniff_order)
'txt'
>>> fname = get_test_fname('2.tabular')
>>> guess_ext(fname, sniff_order)
'tabular'
>>> fname = get_test_fname('3.txt')
>>> guess_ext(fname, sniff_order) # 3.txt
>>> guess_ext(fname, sniff_order)
'txt'
>>> fname = get_test_fname('test_tab1.tabular')
>>> guess_ext(fname, sniff_order)
@@ -454,6 +452,9 @@ def guess_ext(fname, sniff_order, is_binary=False):
>>> fname = get_test_fname('1imzml')
>>> guess_ext(fname, sniff_order) # This test case is ensuring doesn't throw exception, actual value could change if non-utf encoding handling improves.
'data'
>>> fname = get_test_fname('too_many_comments_gff3.tabular')
>>> guess_ext(fname, sniff_order) # It's a VCF but is sniffed as tabular because of the limit on the number of header lines we read
'tabular'
"""
file_prefix = FilePrefix(fname)
file_ext = run_sniffers_raw(file_prefix, sniff_order, is_binary)
+6 -7
View File
@@ -744,13 +744,12 @@ class BaseVcf(Tabular):
def set_meta(self, dataset, **kwd):
super().set_meta(dataset, **kwd)
source = open(dataset.file_name)
# Skip comments.
line = None
for line in source:
if not line.startswith('##'):
break
with compression_utils.get_fileobj(dataset.file_name) as fh:
# Skip comments.
for line in fh:
if not line.startswith('##'):
break
if line and line.startswith('#'):
# Found header line, get sample names.
@@ -816,7 +815,7 @@ class VcfGz(BaseVcf, binary.Binary):
return binascii.hexlify(last28) == b'1f8b08040000000000ff0600424302001b0003000000000000000000'
def set_meta(self, dataset, **kwd):
super(BaseVcf, self).set_meta(dataset, **kwd)
super().set_meta(dataset, **kwd)
""" Creates the index for the VCF file. """
# These metadata values are not accessible by users, always overwrite
index_file = dataset.metadata.tabix_index
@@ -0,0 +1,65 @@
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
ctgA est match 5410 7503 . - . ID=EST:agt830.3;Target=agt830.3+1+595
ctgA est HSP 7000 7503 . - . Parent=EST:agt830.3;Target=agt830.3+1+504
ctgA est HSP 5410 5500 . ? . Parent=EST:agt830.3;Target=agt830.3+505+595
+8
View File
@@ -219,6 +219,14 @@ class ConditionalDependencies:
def check_fs_webdavfs(self):
return 'webdav' in self.file_sources
def check_fs_s3fs(self):
# pyfilesystem plugin access to s3
return 's3' in self.file_sources
def check_s3fs(self):
# use s3fs directly (skipping pyfilesystem) for direct access to more options
return 's3fs' in self.file_sources
def check_watchdog(self):
install_set = {'auto', 'True', 'true', 'polling'}
return (self.config['watch_tools'] in install_set
@@ -13,8 +13,11 @@ python-ldap==3.2.0
python-pam
galaxycloudrunner
fs.webdavfs
fs.dropboxfs
# For file sources plugins
fs.webdavfs # type: webdav
fs.dropboxfs # type: dropbox
fs-s3fs # type: s3
s3fs # type: s3fs
# Chronos client
chronos-python==1.2.1
@@ -38,6 +38,7 @@ cmd2==1.4.0; python_version >= "3.6"
colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6") or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.5.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6")
coloredlogs==15.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4" or python_version >= "3.6" and python_version < "4" and python_full_version >= "3.5.0"
commonmark==0.9.1
contextvars==2.4; python_version >= "3.6" and python_version < "3.7"
coverage==5.3.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4"
cryptography==3.3.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
cwltool==3.0.20201109103151; python_version >= "3.6" and python_version < "4"
@@ -68,6 +69,7 @@ gxformat2==0.15.0
h11==0.12.0; python_version >= "3.6"
h5py==3.1.0; python_version >= "3.6"
httplib2==0.18.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0"
httpx==0.16.1
humanfriendly==9.1; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4" or python_version >= "3.6" and python_version < "4" and python_full_version >= "3.5.0"
idna==2.10; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4"
imagesize==1.2.0; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.5"
@@ -83,6 +85,7 @@ jsonpatch==1.28; python_version >= "3.6" and python_full_version < "3.0.0" or py
jsonpointer==2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
keystoneauth1==4.3.0; python_version >= "3.6"
kombu==5.0.2; python_version >= "3.6"
lagom==1.0.0; python_version >= "3.6" and python_version < "4.0"
lockfile==0.12.2; python_version >= "3.6" and python_version < "4"
lxml==4.6.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
mako==1.1.4; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0")
@@ -148,6 +151,7 @@ pyparsing==2.4.7; (python_version >= "2.6" and python_full_version < "3.0.0") or
pyperclip==1.8.1; python_version >= "3.6"
pyreadline==2.1; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4" and sys_platform == "win32" or python_version >= "3.6" and python_version < "4" and python_full_version >= "3.5.0" and sys_platform == "win32"
pysam==0.16.0.1
pytest-asyncio==0.14.0
pytest-cov==2.11.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
pytest-html==3.1.1; python_version >= "3.6"
pytest-metadata==1.11.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
@@ -208,6 +212,7 @@ sqlalchemy==1.3.22; (python_version >= "2.7" and python_full_version < "3.0.0")
sqlitedict==1.7.0
sqlparse==0.4.1; python_version >= "3.5"
starlette==0.13.6; python_version >= "3.6" and python_version < "4.0"
starlette-context==0.3.1; python_version >= "3.7"
stevedore==3.3.0; python_version >= "3.6"
svgwrite==1.4.1; python_version >= "3.6"
tempita==0.5.2
@@ -1,4 +1,4 @@
flake8
flake8-bugbear
flake8-import-order
mypy
mypy==0.790
@@ -35,6 +35,7 @@ cloudbridge==2.1.0
cmd2==1.4.0; python_version >= "3.6"
colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
coloredlogs==15.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4" or python_version >= "3.6" and python_version < "4" and python_full_version >= "3.5.0"
contextvars==2.4; python_version >= "3.6" and python_version < "3.7"
cryptography==3.3.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
cwltool==3.0.20201109103151; python_version >= "3.6" and python_version < "4"
dataclasses==0.8; python_version >= "3.6" and python_version < "3.7"
@@ -74,6 +75,7 @@ jsonpatch==1.28; python_version >= "3.6" and python_full_version < "3.0.0" or py
jsonpointer==2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
keystoneauth1==4.3.0; python_version >= "3.6"
kombu==5.0.2; python_version >= "3.6"
lagom==1.0.0; python_version >= "3.6" and python_version < "4.0"
lockfile==0.12.2; python_version >= "3.6" and python_version < "4"
lxml==4.6.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
mako==1.1.4; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0")
@@ -168,6 +170,7 @@ sqlalchemy==1.3.22; (python_version >= "2.7" and python_full_version < "3.0.0")
sqlitedict==1.7.0
sqlparse==0.4.1; python_version >= "3.5"
starlette==0.13.6; python_version >= "3.6" and python_version < "4.0"
starlette-context==0.3.1; python_version >= "3.7"
stevedore==3.3.0; python_version >= "3.6"
svgwrite==1.4.1; python_version >= "3.6"
tempita==0.5.2
+37
View File
@@ -0,0 +1,37 @@
"""Dependency injection framework for Galaxy-type apps."""
from typing import Optional, Type, TypeVar
from lagom import Container as LagomContainer
from lagom.exceptions import UnresolvableType
T = TypeVar("T")
class Container(LagomContainer):
"""Abstraction around lagom to provide a dependency injection context.
Abstractions used by Galaxy should come through this interface so we can swap
out the backend as needed. For instance https://punq.readthedocs.io/en/latest/
containers look very nice and would allow us to also inject by name (e.g. for
config variables for instance).
"""
def _register_singleton(self, dep_type: Type[T], instance: Optional[T] = None) -> T:
if instance is None:
# create an instance from the context and register it as a singleton
instance = self[dep_type]
self[dep_type] = instance
return self[dep_type]
def resolve_or_none(self, dep_type: Type[T]) -> Optional[T]:
"""Resolve the dependent type or just return None.
If resolution is impossible assume caller has a backup plan for
constructing the desired object. Used to construct controllers that
may or may not be resolvable (some have upgraded but legacy framework still
works).
"""
try:
return self[dep_type]
except UnresolvableType:
return None
+83
View File
@@ -0,0 +1,83 @@
import functools
import os
try:
import s3fs
except ImportError:
s3fs = None
from ..sources import BaseFilesSource
DEFAULT_ENFORCE_SYMLINK_SECURITY = True
DEFAULT_DELETE_ON_REALIZE = False
class S3FsFilesSource(BaseFilesSource):
plugin_type = 's3fs'
def __init__(self, **kwd):
if s3fs is None:
raise Exception("Package s3fs unavailable but required for this file source plugin.")
props = self._parse_common_config_opts(kwd)
self._bucket = props.pop("bucket")
assert self._bucket
self._props = props
def list(self, path="/", recursive=True, user_context=None):
fs = self._open_fs(user_context=user_context)
if recursive:
res = []
bucket_path = self._bucket_path(path)
for p, dirs, files in fs.walk(bucket_path, detail=True):
to_dict = functools.partial(self._resource_info_to_dict, p)
res.extend(map(to_dict, dirs.values()))
res.extend(map(to_dict, files.values()))
return res
else:
bucket_path = self._bucket_path(path)
res = fs.ls(bucket_path, detail=True)
to_dict = functools.partial(self._resource_info_to_dict, path)
return list(map(to_dict, res))
def realize_to(self, source_path, native_path, user_context=None):
bucket_path = self._bucket_path(source_path)
self._open_fs(user_context=user_context).download(bucket_path, native_path)
def _write_from(self, target_path, native_path, user_context=None):
raise NotImplementedError()
def _bucket_path(self, path):
if not path.startswith("/"):
path = f"/{path}"
return f"{self._bucket}{path}"
def _open_fs(self, user_context=None):
fs = s3fs.S3FileSystem(**self._props)
return fs
def _resource_info_to_dict(self, dir_path, resource_info):
name = os.path.basename(resource_info["name"])
path = os.path.join(dir_path, name)
uri = self.uri_from_path(path)
if resource_info["type"] == "directory":
return {"class": "Directory", "name": name, "uri": uri, "path": path}
else:
return {
"class": "File",
"name": name,
"size": resource_info["size"],
# should this be mtime...
"ctime": self.to_dict_time(resource_info["LastModified"]),
"uri": uri,
"path": path,
}
def _serialization_props(self, user_context=None):
effective_props = {}
for key, val in self._props.items():
effective_props[key] = self._evaluate_prop(val, user_context=user_context)
effective_props["bucket"] = self._bucket
return effective_props
__all__ = ('S3FsFilesSource',)
+10 -2
View File
@@ -18,6 +18,7 @@ from abc import (
abstractmethod,
)
from json import loads
from typing import Any, Dict, List
import packaging.version
import yaml
@@ -56,6 +57,7 @@ from galaxy.jobs.runners import BaseJobRunner, JobState
from galaxy.metadata import get_metadata_compute_strategy
from galaxy.model import store
from galaxy.objectstore import ObjectStorePopulator
from galaxy.structured_app import StructuredApp
from galaxy.tool_util.deps import requirements
from galaxy.tool_util.output_checker import (
check_output,
@@ -277,6 +279,13 @@ class JobConfiguration(ConfiguresHandlers):
These features are configured in the job configuration, by default, ``job_conf.xml``
"""
runner_plugins: List[dict]
handlers: dict
handler_runner_plugins: Dict[str, str]
tools: Dict[str, list]
resource_groups: Dict[str, list]
destinations: Dict[str, tuple]
resource_parameters: Dict[str, Any]
DEFAULT_BASE_HANDLER_POOLS = ('job-handlers',)
DEFAULT_NWORKERS = 4
@@ -292,7 +301,7 @@ class JobConfiguration(ConfiguresHandlers):
<when value="yes"/>
</conditional>"""
def __init__(self, app):
def __init__(self, app: StructuredApp):
"""Parse the job configuration XML.
"""
self.app = app
@@ -306,7 +315,6 @@ class JobConfiguration(ConfiguresHandlers):
self.handler_max_grab = None
self.handler_ready_window_size = None
self.destinations = {}
self.destination_tags = {}
self.default_destination_id = None
self.tools = {}
self.resource_groups = {}
+10 -1
View File
@@ -40,7 +40,16 @@ JOB_WAIT, JOB_ERROR, JOB_INPUT_ERROR, JOB_INPUT_DELETED, JOB_READY, JOB_DELETED,
DEFAULT_JOB_PUT_FAILURE_MESSAGE = 'Unable to run job due to a misconfiguration of the Galaxy job running system. Please contact a site administrator.'
class JobHandler:
class JobHandlerI:
def start(self):
pass
def shutdown(self):
pass
class JobHandler(JobHandlerI):
"""
Handle the preparation, running, tracking, and finishing of jobs
"""
+5 -2
View File
@@ -9,6 +9,7 @@ from sqlalchemy.sql.expression import null
from galaxy.exceptions import HandlerAssignmentError, ToolExecutionError
from galaxy.jobs import handler, NoopQueue
from galaxy.model import Job
from galaxy.structured_app import StructuredApp
from galaxy.web_stack.message import JobHandlerMessage
log = logging.getLogger(__name__)
@@ -18,7 +19,9 @@ class JobManager:
"""
Highest level interface to job management.
"""
def __init__(self, app):
job_handler: handler.JobHandlerI
def __init__(self, app: StructuredApp):
self.app = app
self.job_lock = False
if self.app.is_job_handler:
@@ -110,7 +113,7 @@ class NoopManager:
pass
class NoopHandler:
class NoopHandler(handler.JobHandlerI):
"""
Implements the JobHandler interface but does nothing
"""
+1
View File
@@ -153,6 +153,7 @@ class SlurmJobRunner(DRMAAJobRunner):
self.work_queue.put((self.fail_job, ajs))
return
if drmaa_state == self.drmaa_job_states.DONE:
ajs.job_wrapper.reclaim_ownership()
with open(ajs.error_file) as rfh:
_remove_spurious_top_lines(rfh, ajs)
with open(ajs.error_file, 'r+') as f:
+6 -3
View File
@@ -1,9 +1,12 @@
from galaxy.structured_app import BasicApp
class ApiKeyManager:
def __init__(self, app):
def __init__(self, app: BasicApp):
self.app = app
def create_api_key(self, user):
def create_api_key(self, user) -> str:
guid = self.app.security.get_new_guid()
new_key = self.app.model.APIKeys()
new_key.user_id = user.id
@@ -13,7 +16,7 @@ class ApiKeyManager:
sa_session.flush()
return guid
def get_or_create_api_key(self, user):
def get_or_create_api_key(self, user) -> 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.
+19 -12
View File
@@ -28,14 +28,16 @@ attribute change to a model object.
import datetime
import logging
import re
from typing import Type
from typing import Callable, Dict, List, Optional, Set, Type
import routes
import sqlalchemy
from sqlalchemy.orm.scoping import scoped_session
from galaxy import exceptions
from galaxy import model
from galaxy.model import tool_shed_install
from galaxy.structured_app import BasicApp, StructuredApp
from galaxy.util import namedtuple
log = logging.getLogger(__name__)
@@ -158,11 +160,12 @@ class ModelManager:
"""
model_class: type = object
foreign_key_name: str
app: BasicApp
def __init__(self, app):
def __init__(self, app: BasicApp):
self.app = app
def session(self):
def session(self) -> scoped_session:
return self.app.model.context
def _session_setattr(self, item, attr, val, fn=None, flush=True):
@@ -485,7 +488,7 @@ class HasAModelManager:
# examples where this doesn't really work are ConfigurationSerializer (no manager)
# and contents (2 managers)
def __init__(self, app, manager=None, **kwargs):
def __init__(self, app: StructuredApp, manager=None, **kwargs):
self._manager = manager
@property
@@ -494,7 +497,7 @@ class HasAModelManager:
# PRECONDITION: assumes self.app is assigned elsewhere
if not self._manager:
# TODO: pass this serializer to it
self._manager = self.model_manager_class(self.app)
self._manager = self.app[self.model_manager_class]
# this will error for unset model_manager_class'es
return self._manager
@@ -536,8 +539,10 @@ class ModelSerializer(HasAModelManager):
"""
#: 'service' to use for getting urls - use class var to allow overriding when testing
url_for = staticmethod(routes.url_for)
default_view: Optional[str]
views: Dict[str, List[str]]
def __init__(self, app, **kwargs):
def __init__(self, app: StructuredApp, **kwargs):
"""
Set up serializer map, any additional serializable keys, and views here.
"""
@@ -548,9 +553,9 @@ class ModelSerializer(HasAModelManager):
# this allows us to: 'mention' the key without adding the default serializer
# TODO: we may want to eventually error if a key is requested
# that is in neither serializable_keyset or serializers
self.serializable_keyset = set()
self.serializable_keyset: Set[str] = set()
# a map of dictionary keys to the functions (often lambdas) that create the values for those keys
self.serializers = {}
self.serializers: Dict[str, Callable] = {}
# add subclass serializers defined there
self.add_serializers()
# update the keyset by the serializers (removing the responsibility from subclasses)
@@ -708,15 +713,15 @@ class ModelDeserializer(HasAModelManager):
"""
# TODO:?? a larger question is: which should be first? Deserialize then validate - or - validate then deserialize?
def __init__(self, app, validator=None, **kwargs):
def __init__(self, app: StructuredApp, validator=None, **kwargs):
"""
Set up deserializers and validator.
"""
super().__init__(app, **kwargs)
self.app = app
self.deserializers = {}
self.deserializable_keyset = set()
self.deserializers: Dict[str, Callable] = {}
self.deserializable_keyset: Set[str] = set()
self.add_deserializers()
# a sub object that can validate incoming values
self.validate = validator or ModelValidator(self.app)
@@ -896,8 +901,10 @@ class ModelFilterParser(HasAModelManager):
model_class: type
parsed_filter = parsed_filter
orm_filter_parsers: Dict[str, Dict]
fn_filter_parsers: Dict[str, Dict]
def __init__(self, app, **kwargs):
def __init__(self, app: StructuredApp, **kwargs):
"""
Set up serializer map, any additional serializable keys, and views here.
"""
+3 -1
View File
@@ -5,12 +5,14 @@ import requests
from beaker.cache import CacheManager
from beaker.util import parse_cache_config_options
from galaxy.structured_app import BasicApp
log = logging.getLogger(__name__)
class CitationsManager:
def __init__(self, app):
def __init__(self, app: BasicApp) -> None:
self.app = app
self.doi_cache = DoiCache(app.config)
-3
View File
@@ -50,9 +50,6 @@ class CloudManager(sharable.SharableModelManager):
# setting this property.
model_class = model.History
def __init__(self, app, *args, **kwargs):
super().__init__(app, *args, **kwargs)
@staticmethod
def configure_provider(provider, credentials):
"""
+24 -15
View File
@@ -8,20 +8,21 @@ from galaxy.exceptions import (
MessageException,
RequestParameterInvalidException
)
from galaxy.managers import (
hdas,
histories,
lddas,
)
from galaxy.managers.collections_util import validate_input_element_identifiers
from galaxy.model import tags
from galaxy.model.dataset_collections import builder
from galaxy.model.dataset_collections.matching import MatchingCollections
from galaxy.model.dataset_collections.registry import DATASET_COLLECTION_TYPES_REGISTRY
from galaxy.model.dataset_collections.type_description import COLLECTION_TYPE_DESCRIPTION_FACTORY
from galaxy.model.mapping import GalaxyModelMapping
from galaxy.model.tags import GalaxyTagHandler
from galaxy.security.idencoding import IdEncodingHelper
from galaxy.util import (
validation
)
from .hdas import HDAManager
from .histories import HistoryManager
from .lddas import LDDAManager
log = logging.getLogger(__name__)
@@ -36,16 +37,24 @@ class DatasetCollectionManager:
"""
ELEMENTS_UNINITIALIZED = object()
def __init__(self, app):
def __init__(
self,
model: GalaxyModelMapping,
security: IdEncodingHelper,
hda_manager: HDAManager,
history_manager: HistoryManager,
tag_handler: GalaxyTagHandler,
ldda_manager: LDDAManager,
):
self.type_registry = DATASET_COLLECTION_TYPES_REGISTRY
self.collection_type_descriptions = COLLECTION_TYPE_DESCRIPTION_FACTORY
self.model = app.model
self.security = app.security
self.model = model
self.security = security
self.hda_manager = hdas.HDAManager(app)
self.history_manager = histories.HistoryManager(app)
self.tag_handler = tags.GalaxyTagHandler(app.model.context)
self.ldda_manager = lddas.LDDAManager(app)
self.hda_manager = hda_manager
self.history_manager = history_manager
self.tag_handler = tag_handler
self.ldda_manager = ldda_manager
def precreate_dataset_collection_instance(self, trans, parent, name, structure, implicit_inputs=None, implicit_output_name=None, tags=None, completed_collection=None):
# TODO: prebuild all required HIDs and send them in so no need to flush in between.
@@ -136,7 +145,7 @@ class DatasetCollectionManager:
def _create_instance_for_collection(self, trans, parent, name, dataset_collection, implicit_output_name=None, implicit_inputs=None, tags=None, set_hid=True, flush=True):
if isinstance(parent, model.History):
dataset_collection_instance = self.model.HistoryDatasetCollectionAssociation(
dataset_collection_instance = model.HistoryDatasetCollectionAssociation(
collection=dataset_collection,
name=name,
)
@@ -153,7 +162,7 @@ class DatasetCollectionManager:
parent.add_dataset_collection(dataset_collection_instance)
elif isinstance(parent, model.LibraryFolder):
dataset_collection_instance = self.model.LibraryDatasetCollectionAssociation(
dataset_collection_instance = model.LibraryDatasetCollectionAssociation(
collection=dataset_collection,
folder=parent,
name=name,
+5 -4
View File
@@ -18,6 +18,7 @@ from galaxy.managers import (
secured,
users
)
from galaxy.structured_app import StructuredApp
from galaxy.util.checkers import check_binary
log = logging.getLogger(__name__)
@@ -32,7 +33,7 @@ class DatasetManager(base.ModelManager, secured.AccessibleManagerMixin, deletabl
# TODO:?? get + error_if_uploading is common pattern, should upload check be worked into access/owed?
def __init__(self, app):
def __init__(self, app: StructuredApp):
super().__init__(app)
self.permissions = DatasetRBACPermissions(app)
# needed for admin test
@@ -142,11 +143,11 @@ class DatasetRBACPermissions:
class DatasetSerializer(base.ModelSerializer, deletable.PurgableSerializerMixin):
model_manager_class = DatasetManager
def __init__(self, app):
def __init__(self, app: StructuredApp, user_manager: users.UserManager):
super().__init__(app)
self.dataset_manager = self.manager
# needed for admin test
self.user_manager = users.UserManager(app)
self.user_manager = user_manager
self.default_view = 'summary'
self.add_view('summary', [
@@ -438,7 +439,7 @@ class _UnflattenedMetadataDatasetAssociationSerializer(base.ModelSerializer,
deletable.PurgableSerializerMixin):
def __init__(self, app):
self.dataset_serializer = DatasetSerializer(app)
self.dataset_serializer = app[DatasetSerializer]
super().__init__(app)
def add_serializers(self):
+2 -1
View File
@@ -5,6 +5,7 @@ from typing import (
)
from galaxy.datatypes.registry import Registry
from galaxy.structured_app import StructuredApp
log = logging.getLogger(__name__)
@@ -12,7 +13,7 @@ log = logging.getLogger(__name__)
class DisplayApplicationsManager:
"""Interface/service object for sharing logic between controllers."""
def __init__(self, app):
def __init__(self, app: StructuredApp):
self._app = app
@property
+113
View File
@@ -0,0 +1,113 @@
import logging
from typing import (
Any,
Dict,
List,
Optional,
)
from galaxy import model
from galaxy.app import StructuredApp
from galaxy.exceptions import (
ObjectNotFound,
)
from galaxy.managers.base import decode_id
from galaxy.managers.context import ProvidesAppContext
from galaxy.schema.fields import EncodedDatabaseIdField
from galaxy.web import url_for
log = logging.getLogger(__name__)
class GroupRolesManager:
"""Interface/service object shared by controllers for interacting with group roles."""
def __init__(self, app: StructuredApp) -> None:
self._app = app
def index(self, trans: ProvidesAppContext, group_id: EncodedDatabaseIdField) -> List[Dict[str, Any]]:
"""
Returns a collection roles associated with the given group.
"""
group = self._get_group(trans, group_id)
rval = []
for gra in group.roles:
group_role = self._serialize_group_role(group_id, gra.role)
rval.append(group_role)
return rval
def show(self, trans: ProvidesAppContext, id: EncodedDatabaseIdField, group_id: EncodedDatabaseIdField) -> Dict[str, Any]:
"""
Returns information about a group role.
"""
role_id = id
group = self._get_group(trans, group_id)
role = self._get_role(trans, role_id)
group_role = self._get_group_role(trans, group, role)
if group_role is None:
raise ObjectNotFound(f"Role {role.name} not in group {group.name}")
return self._serialize_group_role(group_id, role)
def update(self, trans: ProvidesAppContext, id: EncodedDatabaseIdField, group_id: EncodedDatabaseIdField):
"""
Adds a role to a group if it is not already associated.
"""
role_id = id
group = self._get_group(trans, group_id)
role = self._get_role(trans, role_id)
group_role = self._get_group_role(trans, group, role)
if group_role is None:
self._add_role_to_group(trans, group, role)
return self._serialize_group_role(group_id, role)
def delete(self, trans: ProvidesAppContext, id: EncodedDatabaseIdField, group_id: EncodedDatabaseIdField):
"""
Removes a role from a group.
"""
role_id = id
group = self._get_group(trans, group_id)
role = self._get_role(trans, role_id)
group_role = self._get_group_role(trans, group, role)
if group_role is None:
raise ObjectNotFound(f"Role {role.name} not in group {group.name}")
self._remove_role_from_group(trans, group_role)
return self._serialize_group_role(group_id, role)
def _get_group(self, trans: ProvidesAppContext, encoded_group_id: EncodedDatabaseIdField) -> Any:
decoded_group_id = decode_id(self._app, encoded_group_id)
group = trans.sa_session.query(model.Group).get(decoded_group_id)
if group is None:
raise ObjectNotFound(f"Group with id {encoded_group_id} was not found.")
return group
def _get_role(self, trans: ProvidesAppContext, encoded_role_id: EncodedDatabaseIdField) -> model.Role:
decoded_role_id = decode_id(self._app, encoded_role_id)
role = trans.sa_session.query(model.Role).get(decoded_role_id)
if role is None:
raise ObjectNotFound(f"Role with id {encoded_role_id} was not found.")
return role
def _get_group_role(self, trans: ProvidesAppContext, group: model.Group, role: model.Role) -> Optional[model.GroupRoleAssociation]:
return trans.sa_session.query(model.GroupRoleAssociation).filter(
model.GroupRoleAssociation.group == group,
model.GroupRoleAssociation.role == role
).one_or_none()
def _add_role_to_group(self, trans: ProvidesAppContext, group: model.Group, role: model.Role):
gra = model.GroupRoleAssociation(group, role)
trans.sa_session.add(gra)
trans.sa_session.flush()
def _remove_role_from_group(self, trans: ProvidesAppContext, group_role: model.GroupRoleAssociation):
trans.sa_session.delete(group_role)
trans.sa_session.flush()
def _serialize_group_role(self, encoded_group_id: EncodedDatabaseIdField, role: model.Role):
encoded_role_id = self._app.security.encode_id(role.id)
return {
"id": encoded_role_id,
"name": role.name,
"url": url_for('group_role', group_id=encoded_group_id, id=encoded_role_id)
}
+113
View File
@@ -0,0 +1,113 @@
import logging
from typing import (
Any,
Dict,
List,
Optional,
)
from galaxy import model
from galaxy.app import StructuredApp
from galaxy.exceptions import (
ObjectNotFound,
)
from galaxy.managers.base import decode_id
from galaxy.managers.context import ProvidesAppContext
from galaxy.schema.fields import EncodedDatabaseIdField
from galaxy.web import url_for
log = logging.getLogger(__name__)
class GroupUsersManager:
"""Interface/service object shared by controllers for interacting with group users."""
def __init__(self, app: StructuredApp) -> None:
self._app = app
def index(self, trans: ProvidesAppContext, group_id: EncodedDatabaseIdField) -> List[Dict[str, Any]]:
"""
Returns a collection (list) with some information about users associated with the given group.
"""
group = self._get_group(trans, group_id)
rval = []
for uga in group.users:
group_user = self._serialize_group_user(group_id, uga.user)
rval.append(group_user)
return rval
def show(self, trans: ProvidesAppContext, id: EncodedDatabaseIdField, group_id: EncodedDatabaseIdField) -> Dict[str, Any]:
"""
Returns information about a group user.
"""
user_id = id
group = self._get_group(trans, group_id)
user = self._get_user(trans, user_id)
group_user = self._get_group_user(trans, group, user)
if group_user is None:
raise ObjectNotFound(f"User {user.email} not in group {group.name}")
return self._serialize_group_user(group_id, user)
def update(self, trans: ProvidesAppContext, id: EncodedDatabaseIdField, group_id: EncodedDatabaseIdField):
"""
Adds a user to a group.
"""
user_id = id
group = self._get_group(trans, group_id)
user = self._get_user(trans, user_id)
group_user = self._get_group_user(trans, group, user)
if group_user is None:
self._add_user_to_group(trans, group, user)
return self._serialize_group_user(group_id, user)
def delete(self, trans: ProvidesAppContext, id: EncodedDatabaseIdField, group_id: EncodedDatabaseIdField):
"""
Removes a user from a group.
"""
user_id = id
group = self._get_group(trans, group_id)
user = self._get_user(trans, user_id)
group_user = self._get_group_user(trans, group, user)
if group_user is None:
raise ObjectNotFound(f"User {user.email} not in group {group.name}")
self._remove_user_from_group(trans, group_user)
return self._serialize_group_user(group_id, user)
def _get_group(self, trans: ProvidesAppContext, encoded_group_id: EncodedDatabaseIdField) -> Any:
decoded_group_id = decode_id(self._app, encoded_group_id)
group = trans.sa_session.query(model.Group).get(decoded_group_id)
if group is None:
raise ObjectNotFound(f"Group with id {encoded_group_id} was not found.")
return group
def _get_user(self, trans: ProvidesAppContext, encoded_user_id: EncodedDatabaseIdField) -> model.User:
decoded_user_id = decode_id(self._app, encoded_user_id)
user = trans.sa_session.query(model.User).get(decoded_user_id)
if user is None:
raise ObjectNotFound(f"User with id {encoded_user_id} was not found.")
return user
def _get_group_user(self, trans: ProvidesAppContext, group: model.Group, user: model.User) -> Optional[model.UserGroupAssociation]:
return trans.sa_session.query(model.UserGroupAssociation).filter(
model.UserGroupAssociation.user == user,
model.UserGroupAssociation.group == group
).one_or_none()
def _add_user_to_group(self, trans: ProvidesAppContext, group: model.Group, user: model.User):
gra = model.UserGroupAssociation(user, group)
trans.sa_session.add(gra)
trans.sa_session.flush()
def _remove_user_from_group(self, trans: ProvidesAppContext, group_user: model.UserGroupAssociation):
trans.sa_session.delete(group_user)
trans.sa_session.flush()
def _serialize_group_user(self, encoded_group_id: EncodedDatabaseIdField, user: model.User):
encoded_user_id = self._app.security.encode_id(user.id)
return {
"id": encoded_user_id,
"email": user.email,
"url": url_for('group_user', group_id=encoded_group_id, id=encoded_user_id)
}
+116
View File
@@ -0,0 +1,116 @@
from typing import (
Any,
Dict,
List,
)
from sqlalchemy import false
from galaxy import model
from galaxy.app import StructuredApp
from galaxy.exceptions import (
Conflict,
ObjectAttributeMissingException,
ObjectNotFound,
)
from galaxy.managers.base import decode_id
from galaxy.managers.context import ProvidesAppContext
from galaxy.schema.fields import EncodedDatabaseIdField
from galaxy.web import url_for
class GroupsManager:
"""Interface/service object shared by controllers for interacting with groups."""
def __init__(self, app: StructuredApp) -> None:
self._app = app
def index(self, trans: ProvidesAppContext):
"""
Displays a collection (list) of groups.
"""
rval = []
for group in trans.sa_session.query(model.Group).filter(model.Group.table.c.deleted == false()):
item = group.to_dict(value_mapper={'id': trans.security.encode_id})
encoded_id = trans.security.encode_id(group.id)
item['url'] = url_for('group', id=encoded_id)
rval.append(item)
return rval
def create(self, trans: ProvidesAppContext, payload: Dict[str, Any]):
"""
Creates a new group.
"""
name = payload.get('name', None)
if name is None:
raise ObjectAttributeMissingException("Missing required name")
self._check_duplicated_group_name(trans, name)
group = model.Group(name=name)
trans.sa_session.add(group)
encoded_user_ids = payload.get('user_ids', [])
users = self._get_users_by_encoded_ids(trans, encoded_user_ids)
encoded_role_ids = payload.get('role_ids', [])
roles = self._get_roles_by_encoded_ids(trans, encoded_role_ids)
trans.app.security_agent.set_entity_group_associations(groups=[group], roles=roles, users=users)
trans.sa_session.flush()
encoded_id = trans.security.encode_id(group.id)
item = group.to_dict(view='element', value_mapper={'id': trans.security.encode_id})
item['url'] = url_for('group', id=encoded_id)
return [item]
def show(self, trans: ProvidesAppContext, encoded_id: EncodedDatabaseIdField):
"""
Displays information about a group.
"""
group = self._get_group(trans, encoded_id)
item = group.to_dict(view='element', value_mapper={'id': trans.security.encode_id})
item['url'] = url_for('group', id=encoded_id)
item['users_url'] = url_for('group_users', group_id=encoded_id)
item['roles_url'] = url_for('group_roles', group_id=encoded_id)
return item
def update(self, trans: ProvidesAppContext, encoded_id: EncodedDatabaseIdField, payload: Dict[str, Any]):
"""
Modifies a group.
"""
group = self._get_group(trans, encoded_id)
name = payload.get('name', None)
if name:
self._check_duplicated_group_name(trans, name)
group.name = name
trans.sa_session.add(group)
encoded_user_ids = payload.get('user_ids', [])
users = self._get_users_by_encoded_ids(trans, encoded_user_ids)
encoded_role_ids = payload.get('role_ids', [])
roles = self._get_roles_by_encoded_ids(trans, encoded_role_ids)
trans.app.security_agent.set_entity_group_associations(groups=[group], roles=roles, users=users, delete_existing_assocs=False)
trans.sa_session.flush()
def _decode_id(self, encoded_id: EncodedDatabaseIdField) -> int:
return decode_id(self._app, encoded_id)
def _decode_ids(self, encoded_ids: List[EncodedDatabaseIdField]) -> List[int]:
return [self._decode_id(encoded_id) for encoded_id in encoded_ids]
def _check_duplicated_group_name(self, trans: ProvidesAppContext, group_name: str) -> None:
if trans.sa_session.query(model.Group).filter(model.Group.table.c.name == group_name).first():
raise Conflict(f"A group with name '{group_name}' already exists")
def _get_group(self, trans: ProvidesAppContext, encoded_id: EncodedDatabaseIdField) -> model.Group:
decoded_group_id = self._decode_id(encoded_id)
group = trans.sa_session.query(model.Group).get(decoded_group_id)
if group is None:
raise ObjectNotFound(f"Group with id {encoded_id} was not found.")
return group
def _get_users_by_encoded_ids(self, trans: ProvidesAppContext, encoded_user_ids: List[EncodedDatabaseIdField]) -> List[model.User]:
decoded_user_ids = self._decode_ids(encoded_user_ids)
users = trans.sa_session.query(model.User).filter(model.User.table.c.id.in_(decoded_user_ids)).all()
return users
def _get_roles_by_encoded_ids(self, trans: ProvidesAppContext, encoded_role_ids: List[EncodedDatabaseIdField]) -> List[model.Role]:
decoded_role_ids = self._decode_ids(encoded_role_ids)
roles = trans.sa_session.query(model.Role).filter(model.Role.table.c.id.in_(decoded_role_ids)).all()
return roles
+6 -5
View File
@@ -18,8 +18,9 @@ from galaxy.managers import (
datasets,
secured,
taggable,
users
users,
)
from galaxy.structured_app import StructuredApp
log = logging.getLogger(__name__)
@@ -40,12 +41,12 @@ class HDAManager(datasets.DatasetAssociationManager,
# TODO: move what makes sense into DatasetManager
# TODO: which of these are common with LDDAs and can be pushed down into DatasetAssociationManager?
def __init__(self, app):
def __init__(self, app: StructuredApp, user_manager: users.UserManager):
"""
Set up and initialize other managers needed by hdas.
"""
super().__init__(app)
self.user_manager = users.UserManager(app)
self.user_manager = user_manager
def get_owned_ids(self, object_ids, history=None):
"""Get owned IDs.
@@ -247,7 +248,7 @@ class HDASerializer( # datasets._UnflattenedMetadataDatasetAssociationSerialize
annotatable.AnnotatableSerializerMixin):
model_manager_class = HDAManager
def __init__(self, app):
def __init__(self, app: StructuredApp):
super().__init__(app)
self.hda_manager = self.manager
@@ -506,7 +507,7 @@ class HDADeserializer(datasets.DatasetAssociationDeserializer,
"""
model_manager_class = HDAManager
def __init__(self, app):
def __init__(self, app: StructuredApp):
super().__init__(app)
self.hda_manager = self.manager
+5 -10
View File
@@ -16,6 +16,7 @@ from galaxy.managers import (
taggable
)
from galaxy.managers.collections_util import get_hda_and_element_identifiers
from galaxy.structured_app import StructuredApp
from galaxy.util.zipstream import ZipstreamWrapper
@@ -55,12 +56,6 @@ class HDCAManager(
tag_assoc = model.HistoryDatasetCollectionTagAssociation
annotation_assoc = model.HistoryDatasetCollectionAssociationAnnotationAssociation
def __init__(self, app):
"""
Set up and initialize other managers needed by hdcas.
"""
super().__init__(app)
def map_datasets(self, content, fn, *parents):
"""
Iterate over the datasets of a given collection, recursing into collections, and
@@ -92,7 +87,7 @@ class DCESerializer(base.ModelSerializer):
Serializer for DatasetCollectionElements.
"""
def __init__(self, app):
def __init__(self, app: StructuredApp):
super().__init__(app)
self.hda_serializer = hdas.HDASerializer(app)
self.dc_serializer = DCSerializer(app, dce_serializer=self)
@@ -126,7 +121,7 @@ class DCSerializer(base.ModelSerializer):
Serializer for DatasetCollections.
"""
def __init__(self, app, dce_serializer=None):
def __init__(self, app: StructuredApp, dce_serializer=None):
super().__init__(app)
self.dce_serializer = dce_serializer or DCESerializer(app)
@@ -165,7 +160,7 @@ class DCASerializer(base.ModelSerializer):
Base (abstract) Serializer class for HDCAs and LDCAs.
"""
def __init__(self, app, dce_serializer=None):
def __init__(self, app: StructuredApp, dce_serializer=None):
super().__init__(app)
self.dce_serializer = dce_serializer or DCESerializer(app)
@@ -220,7 +215,7 @@ class HDCASerializer(
Serializer for HistoryDatasetCollectionAssociations.
"""
def __init__(self, app):
def __init__(self, app: StructuredApp):
super().__init__(app)
self.hdca_manager = HDCAManager(app)
+13 -12
View File
@@ -21,6 +21,7 @@ from galaxy.managers import (
history_contents,
sharable
)
from galaxy.structured_app import StructuredApp
log = logging.getLogger(__name__)
@@ -37,11 +38,11 @@ class HistoryManager(sharable.SharableModelManager, deletable.PurgableManagerMix
# TODO: incorporate imp/exp (or alias to)
def __init__(self, app, *args, **kwargs):
super().__init__(app, *args, **kwargs)
self.hda_manager = hdas.HDAManager(app)
self.contents_manager = history_contents.HistoryContentsManager(app)
self.contents_filters = history_contents.HistoryContentsFilters(app)
def __init__(self, app: StructuredApp, hda_manager: hdas.HDAManager, contents_manager: history_contents.HistoryContentsManager, contents_filters: history_contents.HistoryContentsFilters):
super().__init__(app)
self.hda_manager = hda_manager
self.contents_manager = contents_manager
self.contents_filters = contents_filters
def copy(self, history, user, **kwargs):
"""
@@ -171,7 +172,7 @@ class HistoryManager(sharable.SharableModelManager, deletable.PurgableManagerMix
class HistoryExportView:
def __init__(self, app):
def __init__(self, app: StructuredApp):
self.app = app
def get_exports(self, trans, history_id):
@@ -223,13 +224,13 @@ class HistorySerializer(sharable.SharableModelSerializer, deletable.PurgableSeri
model_manager_class = HistoryManager
SINGLE_CHAR_ABBR = 'h'
def __init__(self, app, **kwargs):
super().__init__(app, **kwargs)
def __init__(self, app: StructuredApp, hda_manager: hdas.HDAManager, hda_serializer: hdas.HDASerializer, history_contents_serializer: history_contents.HistoryContentsSerializer):
super().__init__(app)
self.history_manager = self.manager
self.hda_manager = hdas.HDAManager(app)
self.hda_serializer = hdas.HDASerializer(app)
self.history_contents_serializer = history_contents.HistoryContentsSerializer(app)
self.hda_manager = hda_manager
self.hda_serializer = hda_serializer
self.history_contents_serializer = history_contents_serializer
self.default_view = 'summary'
self.add_view('summary', [
@@ -442,7 +443,7 @@ class HistoryDeserializer(sharable.SharableModelDeserializer, deletable.Purgable
"""
model_manager_class = HistoryManager
def __init__(self, app):
def __init__(self, app: StructuredApp):
super().__init__(app)
self.history_manager = self.manager
+5 -4
View File
@@ -32,6 +32,7 @@ from galaxy.managers import (
taggable,
tools
)
from galaxy.structured_app import StructuredApp
log = logging.getLogger(__name__)
@@ -72,10 +73,10 @@ class HistoryContentsManager(containers.ContainerManagerMixin):
)
default_order_by = 'hid'
def __init__(self, app):
def __init__(self, app: StructuredApp):
self.app = app
self.contained_manager = self.contained_class_manager_class(app)
self.subcontainer_manager = self.subcontainer_class_manager_class(app)
self.contained_manager = app[self.contained_class_manager_class]
self.subcontainer_manager = app[self.subcontainer_class_manager_class]
# ---- interface
def contained(self, container, filters=None, limit=None, offset=None, order_by=None, **kwargs):
@@ -426,7 +427,7 @@ class HistoryContentsSerializer(base.ModelSerializer, deletable.PurgableSerializ
"""
model_manager_class = HistoryContentsManager
def __init__(self, app, **kwargs):
def __init__(self, app: StructuredApp, **kwargs):
super().__init__(app, **kwargs)
self.default_view = 'summary'
+13 -6
View File
@@ -21,6 +21,7 @@ from galaxy.managers.collections import DatasetCollectionManager
from galaxy.managers.datasets import DatasetManager
from galaxy.managers.hdas import HDAManager
from galaxy.managers.lddas import LDDAManager
from galaxy.structured_app import StructuredApp
from galaxy.util import (
defaultdict,
ExecutionTimer,
@@ -55,11 +56,11 @@ def get_path_key(path_tuple):
class JobManager:
def __init__(self, app):
def __init__(self, app: StructuredApp):
self.app = app
self.dataset_manager = DatasetManager(app)
def job_lock(self):
def job_lock(self) -> JobLock:
return JobLock(active=self.app.job_manager.job_lock)
def update_job_lock(self, job_lock: JobLock):
@@ -93,12 +94,18 @@ class JobManager:
class JobSearch:
"""Search for jobs using tool inputs or other jobs"""
def __init__(self, app):
def __init__(
self,
app: StructuredApp,
hda_manager: HDAManager,
dataset_collection_manager: DatasetCollectionManager,
ldda_manager: LDDAManager
):
self.app = app
self.sa_session = app.model.context
self.hda_manager = HDAManager(app)
self.dataset_collection_manager = DatasetCollectionManager(app)
self.ldda_manager = LDDAManager(app)
self.hda_manager = hda_manager
self.dataset_collection_manager = dataset_collection_manager
self.ldda_manager = ldda_manager
self.decode_id = self.app.security.decode_id
def by_tool_input(self, trans, tool_id, tool_version, param=None, param_dump=None, job_state='ok'):
+2 -1
View File
@@ -3,6 +3,7 @@ import logging
from galaxy import model, util
from galaxy.managers import base as manager_base
from galaxy.managers.datasets import DatasetAssociationManager
from galaxy.structured_app import StructuredApp
log = logging.getLogger(__name__)
@@ -13,7 +14,7 @@ class LDDAManager(DatasetAssociationManager):
"""
model_class = model.LibraryDatasetDatasetAssociation
def __init__(self, app):
def __init__(self, app: StructuredApp):
"""
Set up and initialize other managers needed by lddas.
"""
+9 -8
View File
@@ -23,7 +23,6 @@ from galaxy.managers import (
)
from galaxy.managers.context import ProvidesAppContext
from galaxy.schema.fields import EncodedDatabaseIdField
from galaxy.structured_app import StructuredApp
from galaxy.util import (
pretty_print_time_interval,
unicodify,
@@ -38,9 +37,6 @@ class LibraryManager:
Interface/service object for interacting with libraries.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def get(self, trans, decoded_library_id, check_accessible=True):
"""
Get the library from the DB.
@@ -329,10 +325,15 @@ class LibrariesManager:
Interface/service object for sharing logic between controllers.
"""
def __init__(self, app: StructuredApp):
self.folder_manager = folders.FolderManager()
self.library_manager = LibraryManager()
self.role_manager = roles.RoleManager(app)
def __init__(
self,
folder_manager: folders.FolderManager,
library_manager: LibraryManager,
role_manager: roles.RoleManager,
):
self.folder_manager = folder_manager
self.library_manager = library_manager
self.role_manager = role_manager
def index(self, trans: ProvidesAppContext, deleted: Optional[bool] = False) -> List[Any]:
"""Returns a list of summary data for all libraries.
+17 -17
View File
@@ -11,6 +11,7 @@ from enum import Enum
from html.entities import name2codepoint
from html.parser import HTMLParser
from typing import (
Callable,
List,
Optional,
)
@@ -22,15 +23,15 @@ from pydantic import (
from galaxy import exceptions, model
from galaxy.managers import base, sharable
from galaxy.managers.hdas import HDAManager
from galaxy.managers.context import ProvidesHistoryContext
from galaxy.managers.markdown_util import (
internal_galaxy_markdown_to_pdf,
ready_galaxy_markdown_for_export,
ready_galaxy_markdown_for_import,
)
from galaxy.managers.workflows import WorkflowsManager
from galaxy.model.item_attrs import UsesAnnotations
from galaxy.schema.fields import EncodedDatabaseIdField
from galaxy.structured_app import StructuredApp
from galaxy.util import unicodify
from galaxy.util.sanitize_html import sanitize_html
@@ -179,7 +180,7 @@ class PagesManager:
Common interface/service logic for interactions with pages.
"""
def __init__(self, app):
def __init__(self, app: StructuredApp):
self.manager = PageManager(app)
def index(self, trans, deleted: bool = False) -> PageSummaryList:
@@ -282,11 +283,11 @@ class PageManager(sharable.SharableModelManager, UsesAnnotations):
annotation_assoc = model.PageAnnotationAssociation
rating_assoc = model.PageRatingAssociation
def __init__(self, app, *args, **kwargs):
def __init__(self, app: StructuredApp):
"""
"""
super().__init__(app, *args, **kwargs)
self.workflow_manager = WorkflowsManager(app)
super().__init__(app)
self.workflow_manager = app.workflow_manager
def create(self, trans, payload):
user = trans.get_user()
@@ -379,7 +380,6 @@ class PageManager(sharable.SharableModelManager, UsesAnnotations):
content = ready_galaxy_markdown_for_import(trans, content)
else:
raise exceptions.RequestParameterInvalidException("content_format [%s] must be either html or markdown" % content_format)
return content
def rewrite_content_for_export(self, trans, as_dict):
@@ -406,7 +406,7 @@ class PageSerializer(sharable.SharableModelSerializer):
model_manager_class = PageManager
SINGLE_CHAR_ABBR = 'p'
def __init__(self, app):
def __init__(self, app: StructuredApp):
super().__init__(app)
self.page_manager = PageManager(app)
@@ -427,7 +427,7 @@ class PageDeserializer(sharable.SharableModelDeserializer):
"""
model_manager_class = PageManager
def __init__(self, app):
def __init__(self, app: StructuredApp):
super().__init__(app)
self.page_manager = self.manager
@@ -450,7 +450,7 @@ class PageContentProcessor(HTMLParser):
'source', 'track', 'wbr'
}
def __init__(self, trans, render_embed_html_fn):
def __init__(self, trans, render_embed_html_fn: Callable):
HTMLParser.__init__(self)
self.trans = trans
self.ignore_content = False
@@ -627,28 +627,28 @@ PAGE_CLASS_MAPPING = {
}
def placeholderRenderForEdit(trans, item_class, item_id):
def placeholderRenderForEdit(trans: ProvidesHistoryContext, item_class, item_id):
return placeholderRenderForSave(trans, item_class, item_id, encode=True)
def placeholderRenderForSave(trans, item_class, item_id, encode=False):
def placeholderRenderForSave(trans: ProvidesHistoryContext, item_class, item_id, encode=False):
encoded_item_id, decoded_item_id = get_page_identifiers(item_id, trans.app)
item_name = ''
if item_class == 'History':
history = trans.sa_session.query(trans.model.History).get(decoded_item_id)
history = trans.sa_session.query(model.History).get(decoded_item_id)
history = base.security_check(trans, history, False, True)
item_name = history.name
elif item_class == 'HistoryDatasetAssociation':
hda = trans.sa_session.query(trans.model.HistoryDatasetAssociation).get(decoded_item_id)
hda_manager = HDAManager(trans.app)
hda = trans.sa_session.query(model.HistoryDatasetAssociation).get(decoded_item_id)
hda_manager = trans.app.hda_manager
hda = hda_manager.get_accessible(decoded_item_id, trans.user)
item_name = hda.name
elif item_class == 'StoredWorkflow':
wf = trans.sa_session.query(trans.model.StoredWorkflow).get(decoded_item_id)
wf = trans.sa_session.query(model.StoredWorkflow).get(decoded_item_id)
wf = base.security_check(trans, wf, False, True)
item_name = wf.name
elif item_class == 'Visualization':
visualization = trans.sa_session.query(trans.model.Visualization).get(decoded_item_id)
visualization = trans.sa_session.query(model.Visualization).get(decoded_item_id)
visualization = base.security_check(trans, visualization, False, True)
item_name = visualization.title
class_shorthand = PAGE_CLASS_MAPPING[item_class]
-3
View File
@@ -53,9 +53,6 @@ class RoleManager(base.ModelManager):
user_assoc = model.UserRoleAssociation
group_assoc = model.GroupRoleAssociation
def __init__(self, app):
super().__init__(app)
def get(self, trans: ProvidesUserContext, decoded_role_id):
"""
Method loads the role from the DB based on the given role id.
+11 -2
View File
@@ -1,3 +1,5 @@
import logging
from sqlalchemy import (
and_,
true,
@@ -6,16 +8,23 @@ from sqlalchemy.orm import (
joinedload,
)
from galaxy.model.base import SharedModelMapping
log = logging.getLogger(__name__)
class GalaxySessionManager:
"""Manages GalaxySession."""
def __init__(self, model):
def __init__(self, model: SharedModelMapping):
self.model = model
self.sa_session = model.context
def get_session_from_session_key(self, session_key: str):
"""Returns GalaxySession if session_key is valid."""
galaxy_session = self.model.session.query(self.model.GalaxySession).filter(
# going through self.model since this can be used by Galaxy or Toolshed despite
# type annotations
galaxy_session = self.sa_session.query(self.model.GalaxySession).filter(
and_(
self.model.GalaxySession.table.c.session_key == session_key,
self.model.GalaxySession.table.c.is_valid == true())
+2 -1
View File
@@ -25,6 +25,7 @@ from galaxy.managers import (
users
)
from galaxy.model import UserShareAssociation
from galaxy.structured_app import StructuredApp
log = logging.getLogger(__name__)
@@ -40,7 +41,7 @@ class SharableModelManager(base.ModelManager, secured.OwnableManagerMixin, secur
#: the single character abbreviation used in username_and_slug: e.g. 'h' for histories: u/user/h/slug
SINGLE_CHAR_ABBR: Optional[str] = None
def __init__(self, app):
def __init__(self, app: StructuredApp):
super().__init__(app)
# user manager is needed to check access/ownership/admin
self.user_manager = users.UserManager(app)
+2 -1
View File
@@ -62,6 +62,7 @@ class TagsManager:
"""
tag_handler = GalaxyTagHandlerSession(trans.sa_session)
id = trans.security.decode_id(payload.item_id)
item_class = tag_handler.item_tag_assoc_info[payload.item_class].item_class
item_class_name = str(payload.item_class)
item_class = tag_handler.item_tag_assoc_info[item_class_name].item_class
item = trans.sa_session.query(item_class).filter(item_class.id == id).first()
return item
-3
View File
@@ -17,9 +17,6 @@ class DynamicToolManager(ModelManager):
"""
model_class = model.DynamicTool
def __init__(self, app):
super().__init__(app)
def get_tool_by_uuid(self, uuid):
dynamic_tool = self._one_or_none(
self.query().filter(self.model_class.uuid == uuid)
+4 -10
View File
@@ -30,6 +30,7 @@ from galaxy.security.validate_user_input import (
validate_password,
validate_publicname
)
from galaxy.structured_app import BasicApp, StructuredApp
from galaxy.util.hash_util import new_secure_hash
from galaxy.web import url_for
@@ -69,7 +70,7 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin):
# TODO: incorp BaseAPIController.validate_in_users_and_groups
# TODO: incorp CreatesApiKeysMixin
# TODO: incorporate UsesFormDefinitionsMixin?
def __init__(self, app):
def __init__(self, app: BasicApp):
self.model_class = app.model.User
super().__init__(app)
@@ -240,14 +241,6 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin):
except exceptions.ObjectNotFound:
return None
def by_email_like(self, email_with_wildcards, filters=None, order_by=None, **kwargs):
"""
Find a user searching with SQL wildcards.
"""
filters = self._munge_filters(self.model_class.email.like(email_with_wildcards), filters)
order_by = order_by or (model.User.email, )
return super().list(filters=filters, order_by=order_by, **kwargs)
def by_api_key(self, api_key, sa_session=None):
"""
Find a user by API key.
@@ -353,6 +346,7 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin):
Create and return an API key for `user`.
"""
# TODO: seems like this should return the model
# Also TODO: seems unused? drop and see what happens? -John
return api_keys.ApiKeyManager(self.app).create_api_key(user)
def user_can_do_run_as(self, user) -> bool:
@@ -617,7 +611,7 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin):
class UserSerializer(base.ModelSerializer, deletable.PurgableSerializerMixin):
model_manager_class = UserManager
def __init__(self, app):
def __init__(self, app: StructuredApp):
"""
Convert a User and associated data to a dictionary representation.
"""
+2 -6
View File
@@ -8,6 +8,7 @@ import logging
from galaxy import model
from galaxy.managers import sharable
from galaxy.structured_app import StructuredApp
log = logging.getLogger(__name__)
@@ -27,11 +28,6 @@ class VisualizationManager(sharable.SharableModelManager):
annotation_assoc = model.VisualizationAnnotationAssociation
rating_assoc = model.VisualizationRatingAssociation
def __init__(self, app, *args, **kwargs):
"""
"""
super().__init__(app, *args, **kwargs)
# def copy( self, trans, visualization, user, **kwargs ):
# """
# """
@@ -45,7 +41,7 @@ class VisualizationSerializer(sharable.SharableModelSerializer):
model_manager_class = VisualizationManager
SINGLE_CHAR_ABBR = 'v'
def __init__(self, app):
def __init__(self, app: StructuredApp):
super().__init__(app)
self.visualization_manager = self.manager
+3 -2
View File
@@ -26,6 +26,7 @@ from galaxy import (
)
from galaxy.jobs.actions.post import ActionBox
from galaxy.model.item_attrs import UsesAnnotations
from galaxy.structured_app import StructuredApp
from galaxy.tools.parameters import (
params_to_incoming,
visit_input_values
@@ -68,7 +69,7 @@ class WorkflowsManager:
the galaxy.workflow module.
"""
def __init__(self, app):
def __init__(self, app: StructuredApp):
self.app = app
def get_stored_workflow(self, trans, workflow_id, by_stored_id=True):
@@ -305,7 +306,7 @@ CreatedWorkflow = namedtuple("CreatedWorkflow", ["stored_workflow", "workflow",
class WorkflowContentsManager(UsesAnnotations):
def __init__(self, app):
def __init__(self, app: StructuredApp):
self.app = app
self._resource_mapper_function = get_resource_mapper_function(app)
+1
View File
@@ -370,6 +370,7 @@ class JobLike:
class User(Dictifiable, RepresentById):
use_pbkdf2 = True
bootstrap_admin_user = False
api_keys: 'List[APIKeys]'
"""
Data for a Galaxy user or admin and relations to their
histories, credentials, and roles.
+47 -16
View File
@@ -2,10 +2,13 @@
Shared model and mapping code between Galaxy and Tool Shed, trying to
generalize to generic database connections.
"""
import threading
from contextvars import ContextVar
from inspect import (
getmembers,
isclass
)
from typing import Dict, Type
from sqlalchemy import event
from sqlalchemy.orm import (
@@ -16,6 +19,14 @@ from sqlalchemy.orm import (
from galaxy.util.bunch import Bunch
# Create a ContextVar with mutable state, this allows sync tasks in the context
# of a request (which run within a threadpool) to see changes to the ContextVar
# state. See https://github.com/tiangolo/fastapi/issues/953#issuecomment-586006249
# for details
_request_state: Dict[str, str] = {}
REQUEST_ID = ContextVar('request_id', default=_request_state.copy())
# TODO: Refactor this to be a proper class, not a bunch.
class ModelMapping(Bunch):
@@ -23,13 +34,13 @@ class ModelMapping(Bunch):
self.engine = engine
SessionLocal = sessionmaker(autoflush=False, autocommit=True)
versioned_session(SessionLocal)
context = scoped_session(SessionLocal)
context = scoped_session(SessionLocal, scopefunc=self.request_scopefunc)
# For backward compatibility with "context.current"
# deprecated?
context.current = context
self._SessionLocal = SessionLocal
self._session = context
self.local_session = None
self.session = context
self.scoped_registry = context.registry
model_classes = {}
for module in model_modules:
@@ -42,22 +53,30 @@ class ModelMapping(Bunch):
context.remove()
context.configure(bind=engine)
def set_local_session(self):
self.session = self._SessionLocal()
def request_scopefunc(self):
"""
Return a value that is used as dictionary key for sqlalchemy's ScopedRegistry.
def dispose_local_session(self):
self.session = None
This ensures that threads or request contexts will receive a single identical session
from the ScopedRegistry.
"""
return REQUEST_ID.get().get('request') or threading.get_ident()
@property
def session(self):
return self.local_session or self._session
@staticmethod
def set_request_id(request_id):
# Set REQUEST_ID to a new dict.
# This new ContextVar value will only be seen by the current asyncio context
# and descendant threadpools, but not other threads or asyncio contexts.
return REQUEST_ID.set({'request': request_id})
@session.setter
def session(self, session):
# For backward compatibility with "context.current"
if session:
session.current = session
self.local_session = session
def unset_request_id(self, request_id):
# Unconditionally calling self.gx_app.model.session.remove()
# would create a new session if the session was not accessed
# in a request, so we check if there is a sqlalchemy session
# for the current request in the registry.
if request_id in self.scoped_registry.registry:
self.scoped_registry.registry[request_id].close()
del self.scoped_registry.registry[request_id]
@property
def context(self):
@@ -71,6 +90,18 @@ class ModelMapping(Bunch):
return self.context
class SharedModelMapping(ModelMapping):
"""Model mapping containing references to classes shared between Galaxy and ToolShed.
Generally things can be more strongly typed when importing models directly, but we need
a way to do app.model.<CLASS> for common code shared by the tool shed and Galaxy.
"""
User: Type
GalaxySession: Type
APIKeys: Type
PasswordResetToken: Type
def versioned_objects(iter):
for obj in iter:
if hasattr(obj, '__create_version__'):
+1 -1
View File
@@ -25,7 +25,7 @@ class DatabaseHeartbeat:
@property
def sa_session(self):
return self.application_stack.app.model.context
return self.application_stack.app.model.session
@property
def server_name(self):
+5 -3
View File
@@ -5,7 +5,7 @@ are encapsulated here.
import logging
from threading import local
from typing import Optional
from typing import Optional, Type
from sqlalchemy import (
and_,
@@ -39,7 +39,7 @@ from sqlalchemy.sql import exists
from sqlalchemy.types import BigInteger
from galaxy import model
from galaxy.model.base import ModelMapping
from galaxy.model.base import SharedModelMapping
from galaxy.model.custom_types import (
JSONType,
MetadataType,
@@ -2872,10 +2872,12 @@ def _workflow_invocation_update(self):
model.WorkflowInvocation.update = _workflow_invocation_update # type: ignore
class GalaxyModelMapping(ModelMapping):
class GalaxyModelMapping(SharedModelMapping):
security_agent: GalaxyRBACAgent
thread_local_log: Optional[local]
create_tables: bool
User: Type
GalaxySession: Type
def init(file_path, url, engine_options=None, create_tables=False, map_install_models=False,
+5 -3
View File
@@ -1,6 +1,8 @@
import logging
import re
from typing import Dict
from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.sql import select
from sqlalchemy.sql.expression import func
@@ -26,7 +28,7 @@ class TagHandler:
Manages CRUD operations related to tagging objects.
"""
def __init__(self, sa_session):
def __init__(self, sa_session: scoped_session) -> None:
self.sa_session = sa_session
# Minimum tag length.
self.min_tag_len = 1
@@ -39,7 +41,7 @@ class TagHandler:
# Key-value separator.
self.key_value_separators = "=:"
# Initialize with known classes - add to this in subclasses.
self.item_tag_assoc_info = {}
self.item_tag_assoc_info: Dict[str, ItemTagAssocInfo] = {}
def create_tag_handler_session(self):
# Creates a transient tag handler that avoids repeated flushes
@@ -348,7 +350,7 @@ class TagHandler:
class GalaxyTagHandler(TagHandler):
def __init__(self, sa_session):
def __init__(self, sa_session: scoped_session):
from galaxy import model
TagHandler.__init__(self, sa_session)
self.item_tag_assoc_info["History"] = ItemTagAssocInfo(model.History,
+5 -5
View File
@@ -1,5 +1,4 @@
"""Galaxy Quotas"""
import abc
import logging
import galaxy.util
@@ -7,7 +6,7 @@ import galaxy.util
log = logging.getLogger(__name__)
class QuotaAgent(metaclass=abc.ABCMeta):
class QuotaAgent(): # metaclass=abc.ABCMeta
"""Abstraction around querying Galaxy for quota available and used.
Certain parts of the app that deal directly with modifying the quota assume more than
@@ -21,7 +20,7 @@ class QuotaAgent(metaclass=abc.ABCMeta):
the quota in other apps (LDAP maybe?) or via configuration files.
"""
@abc.abstractmethod
# TODO: make abstractmethod after they work better with mypy
def get_quota(self, user):
"""Return quota in bytes or None if no quota is set."""
@@ -34,7 +33,7 @@ class QuotaAgent(metaclass=abc.ABCMeta):
quota_str = 'unlimited'
return quota_str
@abc.abstractmethod
# TODO: make abstractmethod after they work better with mypy
def get_percent(self, trans=None, user=False, history=False, usage=False, quota=False):
"""Return the percentage of any storage quota applicable to the user/transaction."""
@@ -228,7 +227,8 @@ class DatabaseQuotaAgent(QuotaAgent):
return False
def get_quota_agent(config, model):
def get_quota_agent(config, model) -> QuotaAgent:
quota_agent: QuotaAgent
if config.enable_quotas:
quota_agent = galaxy.quota.DatabaseQuotaAgent(model)
else:
+4
View File
@@ -579,6 +579,10 @@ libraries:
import_dir_btn:
type: xpath
selector: '//button[contains(text(), "Import")]'
manage_dataset_permissions_btn: 'button[title="Permissions of ${name}"]'
make_private_btn: '#make-private'
access_dataset_roles: '.access_dataset_roles .multiselect__tag span'
private_dataset_icon: '.fa-key'
labels:
from_history: 'from History'
+28 -8
View File
@@ -4,13 +4,16 @@ from typing import Any, Optional
from kombu import Connection
from galaxy.auth import AuthManager
from galaxy.datatypes.registry import Registry
from galaxy.di import Container
from galaxy.files import ConfiguredFileSources
from galaxy.job_metrics import JobMetrics
from galaxy.model.base import ModelMapping
from galaxy.model.base import ModelMapping, SharedModelMapping
from galaxy.model.mapping import GalaxyModelMapping
from galaxy.model.security import GalaxyRBACAgent
from galaxy.model.security import HostAgent
from galaxy.model.tags import GalaxyTagHandler
from galaxy.quota import QuotaAgent
from galaxy.security.idencoding import IdEncodingHelper
from galaxy.tool_util.deps.views import DependencyResolversView
from galaxy.tool_util.verify import test_data
@@ -20,7 +23,25 @@ from galaxy.webhooks import WebhooksRegistry
from galaxy.workflow.trs_proxy import TrsProxy
class StructuredApp:
class BasicApp(Container):
"""Stripped down version of the ``app`` shared between Galaxy and ToolShed.
Code that is shared between Galaxy and the Tool Shed should be annotated as
using BasicApp instead of StructuredApp below.
"""
name: str
config: Any
application_stack: ApplicationStack
model: SharedModelMapping
security: IdEncodingHelper
auth_manager: AuthManager
toolbox: Any
security_agent: Any
quota_agent: QuotaAgent
datatypes_registry: Registry
class StructuredApp(BasicApp):
"""Interface defining typed description of the Galaxy UniverseApplication.
Ideally nothing that depends on StructuredApp should require
@@ -31,12 +52,9 @@ class StructuredApp:
(cyclical imports), we're just setting the class attributes to
Any.
"""
name: str
is_webapp: bool # is_webapp will be set to true when building WSGI app
new_installation: bool
config: Any
tag_handler: GalaxyTagHandler
application_stack: ApplicationStack
amqp_internal_connection_obj: Optional[Connection]
dependency_resolvers_view: DependencyResolversView
test_data_resolver: test_data.TestDataResolver
@@ -45,10 +63,8 @@ class StructuredApp:
job_metrics: JobMetrics
model: GalaxyModelMapping
install_model: ModelMapping
security: IdEncodingHelper
security_agent: GalaxyRBACAgent
host_security_agent: HostAgent
auth_manager: AuthManager
trs_proxy: TrsProxy
webhooks_registry: WebhooksRegistry
@@ -73,6 +89,10 @@ class StructuredApp:
installed_repository_manager: Any # 'galaxy.tool_shed.galaxy_install.installed_repository_manager.InstalledRepositoryManager'
workflow_scheduling_manager: Any # 'galaxy.workflow.scheduling_manager.WorkflowSchedulingManager'
interactivetool_manager: Any
job_manager: Any # galaxy.jobs.manager.JobManager
user_manager: Any
api_keys_manager: Any
toolbox: Any
@property
def is_job_handler(self) -> bool:
pass
+1 -2
View File
@@ -27,7 +27,6 @@ from galaxy import (
)
from galaxy.exceptions import ToolInputsNotReadyException
from galaxy.job_execution import output_collect
from galaxy.managers.jobs import JobSearch
from galaxy.metadata import get_metadata_compute_strategy
from galaxy.tool_shed.util.repository_util import get_installed_repository
from galaxy.tool_shed.util.shed_util_common import set_image_paths
@@ -553,7 +552,7 @@ class Tool(Dictifiable):
# The job search is only relevant in a galaxy context, and breaks
# loading tools into the toolshed for validation.
if self.app.name == 'galaxy':
self.job_search = JobSearch(app=self.app)
self.job_search = self.app.job_search
def __getattr__(self, name):
lazy_attributes = {
+12 -4
View File
@@ -7,6 +7,7 @@ import tempfile
import zlib
from collections import defaultdict
from threading import Lock
from typing import Dict, List, Tuple
from sqlalchemy.orm import (
defer,
@@ -14,9 +15,13 @@ from sqlalchemy.orm import (
)
from sqlitedict import SqliteDict
from galaxy.model.tool_shed_install import ToolShedRepository
from galaxy.structured_app import StructuredApp
from galaxy.tools.toolbox.base import ToolConfRepository
from galaxy.util import unicodify
from galaxy.util.hash_util import md5_hash_file
log = logging.getLogger(__name__)
CURRENT_TOOL_CACHE_VERSION = 0
@@ -277,8 +282,11 @@ class ToolShedRepositoryCache:
"""
Cache installed ToolShedRepository objects.
"""
local_repositories: List[ToolConfRepository]
repositories: List[ToolShedRepository]
repos_by_tuple: Dict[Tuple[str, str, str], List[ToolConfRepository]]
def __init__(self, app):
def __init__(self, app: StructuredApp):
self.app = app
# Contains ToolConfRepository objects created from shed_tool_conf.xml entries
self.local_repositories = []
@@ -294,10 +302,10 @@ class ToolShedRepositoryCache:
def rebuild(self):
try:
session = self.app.install_model._SessionLocal()
self.repositories = session.query(self.app.install_model.ToolShedRepository).options(
defer(self.app.install_model.ToolShedRepository.metadata),
self.repositories = session.query(ToolShedRepository).options(
defer(ToolShedRepository.metadata),
joinedload('tool_dependencies').subqueryload('tool_shed_repository').options(
defer(self.app.install_model.ToolShedRepository.metadata)
defer(ToolShedRepository.metadata)
),
).all()
repos_by_tuple = defaultdict(list)
+6 -2
View File
@@ -2,9 +2,10 @@ import errno
import json
import logging
import os
from typing import Dict
from galaxy import util
from galaxy.structured_app import StructuredApp
from galaxy.tools.data import TabularToolDataTable
from galaxy.util.template import fill_template
@@ -16,7 +17,10 @@ DEFAULT_VALUE_TRANSLATION_TYPE = 'template'
class DataManagers:
def __init__(self, app, xml_filename=None):
data_managers: Dict[str, 'DataManager']
managed_data_tables: Dict[str, 'DataManager']
def __init__(self, app: StructuredApp, xml_filename=None):
self.app = app
self.data_managers = {}
self.managed_data_tables = {}
+1
View File
@@ -40,6 +40,7 @@ class ValidationContext:
# not appear to be an issue with the len_file_path config option.
self.config.builds_file_path = None
self.genome_builds = GenomeBuilds(self)
self.job_search = None
def __enter__(self):
return self
+4
View File
@@ -32,6 +32,7 @@ from os.path import (
relpath,
sep as separator,
)
from pathlib import Path
try:
from pwd import getpwuid
except ImportError:
@@ -352,6 +353,9 @@ def external_chown(path, pwent, external_chown_script, description="file"):
try:
if not external_chown_script:
raise ValueError('external_chown_script is not defined')
if Path(path).owner() == pwent[0]:
return True
cmd = shlex.split(external_chown_script)
cmd.extend([path, pwent[0], str(pwent[3])])
log.debug('Changing ownership of {} with: {}'.format(path, ' '.join(map(shlex.quote, cmd))))
+4 -2
View File
@@ -3,6 +3,7 @@ import os
import re
import sys
from json import loads
from typing import Dict
from bx.seq.twobit import TwoBitFile
@@ -10,6 +11,7 @@ from galaxy.exceptions import (
ObjectNotFound,
ReferenceDataError,
)
from galaxy.structured_app import StructuredApp
from galaxy.util.bunch import Bunch
log = logging.getLogger(__name__)
@@ -195,10 +197,10 @@ class Genomes:
Provides information about available genome data and methods for manipulating that data.
"""
def __init__(self, app):
def __init__(self, app: StructuredApp):
self.app = app
# Create list of genomes from app.genome_builds
self.genomes = {}
self.genomes: Dict[str, Genome] = {}
# Store internal versions of data tables for twobit and __dbkey__
self._table_versions = {'twobit': None, '__dbkeys__': None}
self.reload_genomes()

Some files were not shown because too many files have changed in this diff Show More