Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support fetching yaml config files #860

Merged
merged 1 commit into from
May 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

## Unreleased

### Improvements
- Better handle editing polygons with holes ([857](../../pull/857))
- Support fetching yaml config files ([860](../../pull/860))

## 1.14.4

### Improvements
Expand Down
2 changes: 2 additions & 0 deletions girder/girder_large_image/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from .girder_tilesource import getGirderTileSource # noqa
from .loadmodelcache import invalidateLoadModelCache
from .models.image_item import ImageItem
from .rest import addSystemEndpoints
from .rest.large_image_resource import LargeImageResource
from .rest.tiles import TilesItemResource

Expand Down Expand Up @@ -322,6 +323,7 @@ def load(self, info):
curConfig = config.getConfig().get('large_image')
for key, value in (curConfig or {}).items():
large_image.config.setConfig(key, value)
addSystemEndpoints(info['apiRoot'])

girder_tilesource.loadGirderTileSources()
TilesItemResource(info['apiRoot'])
Expand Down
122 changes: 122 additions & 0 deletions girder/girder_large_image/rest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import yaml

from girder import logger
from girder.api import access
from girder.api.describe import Description, autoDescribeRoute
from girder.api.rest import boundHandler
from girder.constants import AccessType, SortDir, TokenScope
from girder.models.file import File
from girder.models.folder import Folder
from girder.models.group import Group
from girder.models.item import Item


def addSystemEndpoints(apiRoot):
"""
This adds endpoints to routes that already exist in Girder.

:param apiRoot: Girder api root class.
"""
apiRoot.folder.route('GET', (':id', 'yaml_config', ':name'), getYAMLConfigFile)


def _mergeDictionaries(a, b):
"""
Merge two dictionaries recursively. If the second dictionary (or any
sub-dictionary) has a special key, value of '__all__': True, the updated
dictionary only contains values from the second dictionary and excludes
the __all__ key.

:param a: the first dictionary. Modified.
:param b: the second dictionary that gets added to the first.
:returns: the modified first dictionary.
"""
if b.get('__all__') is True:
a.clear()
for key in b:
if isinstance(a.get(key), dict) and isinstance(b[key], dict):
_mergeDictionaries(a[key], b[key])
elif key != '__all__' or b[key] is not True:
a[key] = b[key]
return a


def adjustConfigForUser(config, user):
"""
Given the current user, adjust the config so that only relevant and
combined values are used. If the root of the config dictionary contains
"access": {"user": <dict>, "admin": <dict>}, the base values are updated
based on the user's access level. If the root of the config contains
"group": {<group-name>: <dict>, ...}, the base values are updated for
every group the user is a part of.

The order of update is groups in C-sort alphabetical order followed by
access/user and then access/admin as they apply.

:param config: a config dictionary.
"""
if not isinstance(config, dict):
return config
if isinstance(config.get('groups'), dict):
groups = config.pop('groups')
if user:
for group in Group().find(
{'_id': {'$in': user['groups']}}, sort=[('name', SortDir.ASCENDING)]):
if isinstance(groups.get(group['name']), dict):
config = _mergeDictionaries(config, groups[group['name']])
if isinstance(config.get('access'), dict):
accessList = config.pop('access')
if user and isinstance(accessList.get('user'), dict):
config = _mergeDictionaries(config, accessList['user'])
if user and user.get('admin') and isinstance(accessList.get('admin'), dict):
config = _mergeDictionaries(config, accessList['admin'])
return config


@access.public(scope=TokenScope.DATA_READ)
@autoDescribeRoute(
Description('Get a config file.')
.notes(
'This walks up the chain of parent folders until the file is found. '
'If not found, the .config folder in the parent collection or user is '
'checked.\n\nAny yaml file can be returned. If the top-level is a '
'dictionary and contains keys "access" or "groups" where those are '
'dictionaries, the returned value will be modified based on the '
'current user. The "groups" dictionary contains keys that are group '
'names and values that update the main dictionary. All groups that '
'the user is a member of are merged in alphabetical order. If a key '
'and value of "\\__all\\__": True exists, the replacement is total; '
'otherwise it is a merge. If the "access" dictionary exists, the '
'"user" and "admin" subdictionaries are merged if a calling user is '
'present and if the user is an admin, respectively (both get merged '
'for admins).')
.modelParam('id', model=Folder, level=AccessType.READ)
.param('name', 'The name of the file.', paramType='path')
.errorResponse()
)
@boundHandler()
def getYAMLConfigFile(self, folder, name):
user = self.getCurrentUser()
while folder:
item = Item().findOne({'folderId': folder['_id'], 'name': name},
user=user, level=AccessType.READ)
if item:
for file in Item().childFiles(item):
if file['size'] > 10 * 1024 ** 2:
logger.info('Not loading %s -- too large' % file['name'])
continue
with File().open(file) as fptr:
config = yaml.safe_load(fptr)
# combine and adjust config values based on current user
if isinstance(config, dict) and 'access' in config or 'group' in config:
config = adjustConfigForUser(config, user)
return config
if folder['parentCollection'] != 'folder':
if folder['name'] == '.config':
break
folder = Folder().findOne({
'parentId': folder['parentId'],
'parentCollection': folder['parentCollection'],
'name': '.config'})
else:
folder = Folder().load(folder['parentId'], user=user, level=AccessType.READ)
8 changes: 8 additions & 0 deletions girder/test_girder/girder_utilities.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import io
import os
from test.datastore import datastore
from test.utilities import BigTIFFHeader, JFIFHeader, JPEGHeader, PNGHeader, TIFFHeader # noqa
Expand Down Expand Up @@ -38,6 +39,13 @@ def uploadTestFile(fileName, user, assetstore, folderName='Public', name=None):
return uploadFile(imagePath, user=user, assetstore=assetstore, folderName=folderName, name=None)


def uploadText(text, user, assetstore, folder, name):
file = Upload().uploadFromFile(
io.BytesIO(text.encode()), len(text), name,
parentType='folder', parent=folder, user=user, assetstore=assetstore)
return file


def respStatus(resp):
return int(resp.output_status.split()[0])

Expand Down
75 changes: 75 additions & 0 deletions girder/test_girder/test_large_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@

from girder import events
from girder.exceptions import ValidationException
from girder.models.collection import Collection
from girder.models.file import File
from girder.models.folder import Folder
from girder.models.group import Group
from girder.models.item import Item
from girder.models.setting import Setting
except ImportError:
Expand Down Expand Up @@ -400,3 +403,75 @@ def testHistogramCaching(server, admin, user, fsAssetstore):
resp = server.request(path='/large_image/histograms', user=admin)
assert utilities.respStatus(resp) == 200
assert resp.json == 0


@pytest.mark.usefixtures('unbindLargeImage')
@pytest.mark.plugin('large_image')
def testYAMLConfigFile(server, admin, user, fsAssetstore):
# Create some resources to use in the tests
collection = Collection().createCollection(
'collection A', admin)
colFolderA = Folder().createFolder(
collection, 'folder A', parentType='collection',
creator=admin)
colFolderB = Folder().createFolder(
colFolderA, 'folder C', creator=admin)
groupA = Group().createGroup('Group A', admin)

resp = server.request(
path='/folder/%s/yaml_config/sample.json' % str(colFolderB['_id']),
method='GET')
assert utilities.respStatus(resp) == 200
assert resp.json is None

colFolderConfig = Folder().createFolder(
collection, '.config', parentType='collection',
creator=admin)
utilities.uploadText(
json.dumps({'keyA': 'value1'}),
admin, fsAssetstore, colFolderConfig, 'sample.json')
resp = server.request(
path='/folder/%s/yaml_config/sample.json' % str(colFolderB['_id']))
assert utilities.respStatus(resp) == 200
assert resp.json['keyA'] == 'value1'

utilities.uploadText(
json.dumps({'keyA': 'value2'}),
admin, fsAssetstore, colFolderA, 'sample.json')
resp = server.request(
path='/folder/%s/yaml_config/sample.json' % str(colFolderB['_id']))
assert utilities.respStatus(resp) == 200
assert resp.json['keyA'] == 'value2'

utilities.uploadText(
json.dumps({
'keyA': 'value3',
'groups': {
'Group A': {'access': {'user': {'keyA': 'value4'}}}},
'access': {'user': {'keyA': 'value5'}, 'admin': {'keyA': 'value6'}}}),
admin, fsAssetstore, colFolderB, 'sample.json')
resp = server.request(
path='/folder/%s/yaml_config/sample.json' % str(colFolderB['_id']))
assert utilities.respStatus(resp) == 200
assert resp.json['keyA'] == 'value3'

resp = server.request(
path='/folder/%s/yaml_config/sample.json' % str(colFolderB['_id']), user=user)
assert utilities.respStatus(resp) == 200
assert resp.json['keyA'] == 'value5'

resp = server.request(
path='/folder/%s/yaml_config/sample.json' % str(colFolderB['_id']), user=admin)
assert utilities.respStatus(resp) == 200
assert resp.json['keyA'] == 'value6'

Group().addUser(groupA, user)
resp = server.request(
path='/folder/%s/yaml_config/sample.json' % str(colFolderB['_id']), user=user)
assert utilities.respStatus(resp) == 200
assert resp.json['keyA'] == 'value4'

resp = server.request(
path='/folder/%s/yaml_config/sample.json' % str(colFolderB['_id']), user=admin)
assert utilities.respStatus(resp) == 200
assert resp.json['keyA'] == 'value6'