Skip to content
Snippets Groups Projects
Commit 4cc57b80 authored by Paul McCarthy's avatar Paul McCarthy :mountain_bicyclist:
Browse files

Merge branch 'mnt/size' into 'master'

New fsl_generate_module_manifest command

See merge request !13
parents 0dd7347d 24432544
No related branches found
No related tags found
1 merge request!13New fsl_generate_module_manifest command
Pipeline #18876 canceled
# `fsl_add_module` changelog # `fsl_add_module` changelog
## 0.5.0 (Monday 5th June 2023)
- New `fsl_generate_module_manifest` command, to generate a JSON manifest
for a set of files.
- Added ability to refer to 'built-in' manifests by aliases - the default
manifest is referred to as `fslcourse`, and the Graduate Programme manifest
as `gradcourse`, e.g. `fsl_add_module -m gradcourse`.
- Added support for a `size` field in manifest entries, specifying the file
size in bytes.
## 0.4.0 (Tuesday 24th January 2023) ## 0.4.0 (Tuesday 24th January 2023)
- Changed the default behaviour so that only files in the `fsl_course_data` - Changed the default behaviour so that only files in the `fsl_course_data`
......
...@@ -9,5 +9,5 @@ ...@@ -9,5 +9,5 @@
""" """
__version__ = '0.4.0' __version__ = '0.5.0'
"""``fsl_add_module`` version number.""" """``fsl_add_module`` version number."""
#!/usr/bin/env python #!/usr/bin/env python
# #
# plugin_manifest.py - Classes and functions for working with FSL plugin # manifest.py - Classes and functions for working with FSL plugin
# manifest files. # manifest files.
# #
# Author: Paul McCarthy <pauldmccarthy@gmail.com> # Author: Paul McCarthy <pauldmccarthy@gmail.com>
# #
...@@ -15,6 +15,7 @@ list of FSL plugin definitions, e.g.:: ...@@ -15,6 +15,7 @@ list of FSL plugin definitions, e.g.::
"category" : "fsl_course_data", "category" : "fsl_course_data",
"url" : "http://.../UnixIntro.zip", "url" : "http://.../UnixIntro.zip",
"checksum" : "f8222...", "checksum" : "f8222...",
"size" : "15677939",
"description" : "Data for the unix introduction", "description" : "Data for the unix introduction",
"version" : "1.2.3", "version" : "1.2.3",
"destination" : "~/" "destination" : "~/"
...@@ -24,6 +25,7 @@ list of FSL plugin definitions, e.g.:: ...@@ -24,6 +25,7 @@ list of FSL plugin definitions, e.g.::
"category" : "fsl_atlases", "category" : "fsl_atlases",
"url" : "http://localhost:8000/abcd123_mouse_atlas.tar.gz", "url" : "http://localhost:8000/abcd123_mouse_atlas.tar.gz",
"checksum" : "658701ff9a49b06f0d0ee38bce36a1c370fdfd0751ab17c2fd7b7b561d3b92e0", "checksum" : "658701ff9a49b06f0d0ee38bce36a1c370fdfd0751ab17c2fd7b7b561d3b92e0",
"size" : "123841532",
"description" : "ABCD123 FSL mouse atlas derived from hand segmentations of 3813 small chocolate mice.", "description" : "ABCD123 FSL mouse atlas derived from hand segmentations of 3813 small chocolate mice.",
"terms_of_use" : "https://chocolate-mouse.com/terms-of-use", "terms_of_use" : "https://chocolate-mouse.com/terms-of-use",
"destination" : "$FSLDIR/data/atlases/" "destination" : "$FSLDIR/data/atlases/"
...@@ -39,6 +41,7 @@ optional but recommended: ...@@ -39,6 +41,7 @@ optional but recommended:
- ``category``: The plugin category, used to organise plugins. - ``category``: The plugin category, used to organise plugins.
- ``checksum``: A SHA256 checksum of the plugin archive file, used to - ``checksum``: A SHA256 checksum of the plugin archive file, used to
verify that it has been successfully downloaded. verify that it has been successfully downloaded.
- ``size``: Size of the file in bytes
- ``description``: An extended description of the plugin - ``description``: An extended description of the plugin
- ``version``: A version identifier for the plugin (used solely for - ``version``: A version identifier for the plugin (used solely for
descriptive purposes) descriptive purposes)
...@@ -98,12 +101,15 @@ class Plugin: ...@@ -98,12 +101,15 @@ class Plugin:
"""SHA256 checksum of the plugin archive file, used to verify downloads. """SHA256 checksum of the plugin archive file, used to verify downloads.
""" """
size : Optional[int] = None
"""Size of the archive file in bytes. """
version : Optional[str] = None version : Optional[str] = None
"""Version identifier for the plugin - displayed to the user, but not """Version identifier for the plugin - displayed to the user, but not
used for any other purpose. used for any other purpose.
""" """
termsOfUse : Optional[str] = None terms_of_use : Optional[str] = None
"""Terms of use for the plugin - displayed to the user during installation. """Terms of use for the plugin - displayed to the user during installation.
""" """
...@@ -137,13 +143,21 @@ class Plugin: ...@@ -137,13 +143,21 @@ class Plugin:
@property @property
def fileName(self): def fileName(self):
"""Return a suitable name to use for the downloaded plugin file. """Return a suitable name to use for the downloaded plugin file.
The file name may be prefixed with the plugin file category.
""" """
fname = op.basename(urlparse.urlparse(self.url).path) fname = self.rawFileName
if self.category not in (None, UNCATEGORISED): if self.category not in (None, UNCATEGORISED):
fname = f'{self.category}_{fname}' fname = f'{self.category}_{fname}'
return routines.sanitiseFileName(fname) return routines.sanitiseFileName(fname)
@property
def rawFileName(self):
"""Return the file name of the plugin file as it appears in te URL.
"""
return op.basename(urlparse.urlparse(self.url).path)
class Manifest(dict): class Manifest(dict):
"""The ``Manifest``class simply a container for :class:`Plugin` instances. """The ``Manifest``class simply a container for :class:`Plugin` instances.
A ``Manifest`` is a dict containing ``{name : Plugin}`` mappings. A ``Manifest`` is a dict containing ``{name : Plugin}`` mappings.
...@@ -154,27 +168,29 @@ class Manifest(dict): ...@@ -154,27 +168,29 @@ class Manifest(dict):
def addPlugin(self, def addPlugin(self,
url : Union[str, pathlib.Path], url : Union[str, pathlib.Path],
name : Optional[str] = None, name : Optional[str] = None,
description : Optional[str] = None, description : Optional[str] = None,
category : Optional[str] = None, category : Optional[str] = None,
checksum : Optional[str] = None, checksum : Optional[str] = None,
version : Optional[str] = None, size : Optional[int] = None,
termsOfUse : Optional[str] = None, version : Optional[str] = None,
destination : Optional[str] = None) -> str: terms_of_use : Optional[str] = None,
destination : Optional[str] = None) -> str:
"""Add a new :class:`Plugin` to the manifest. """Add a new :class:`Plugin` to the manifest.
:arg url: URL of plugin archive file. :arg url: URL of plugin archive file.
:arg name: Plugin name. Defaults to filename component of :arg name: Plugin name. Defaults to filename component of
``url``. ``url``.
:arg description: Extended description of plugin :arg description: Extended description of plugin
:arg category: Plugin category :arg category: Plugin category
:arg checksum: SHA256 checksum of archive file. :arg checksum: SHA256 checksum of archive file.
:arg version: Version identifier for the plugin. :arg size: Size of archive file in bytes.
:arg termsOfUse: Terms of use for the plugin. :arg version: Version identifier for the plugin.
:arg destination: Default installation directory. :arg terms_of_use: Terms of use for the plugin.
:returns: The registered plugin name (equivalent to ``name``, :arg destination: Default installation directory.
if provided). :returns: The registered plugin name (equivalent to ``name``,
if provided).
""" """
# support paths to local files # support paths to local files
...@@ -213,8 +229,9 @@ class Manifest(dict): ...@@ -213,8 +229,9 @@ class Manifest(dict):
category=category, category=category,
description=description, description=description,
checksum=checksum, checksum=checksum,
size=size,
version=version, version=version,
termsOfUse=termsOfUse, terms_of_use=terms_of_use,
destination=destination, destination=destination,
origDestination=origDestination, origDestination=origDestination,
archiveFile=archiveFile, archiveFile=archiveFile,
...@@ -264,7 +281,7 @@ def downloadManifest(url : Union[str, pathlib.Path]) -> Manifest: ...@@ -264,7 +281,7 @@ def downloadManifest(url : Union[str, pathlib.Path]) -> Manifest:
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as d:
dest = op.join(d, 'manifest.txt') dest = op.join(d, 'manifest.txt')
routines.downloadFile(url, dest, progress=False) routines.downloadFile(url, dest, progress=False)
rawmanifest = open(dest, 'rt').read() rawmanifest = open(dest, 'rt', encoding='utf-8').read()
try: try:
rawmanifest = json.loads(rawmanifest) rawmanifest = json.loads(rawmanifest)
...@@ -276,14 +293,17 @@ def downloadManifest(url : Union[str, pathlib.Path]) -> Manifest: ...@@ -276,14 +293,17 @@ def downloadManifest(url : Union[str, pathlib.Path]) -> Manifest:
# plugins must at least contain a URL # plugins must at least contain a URL
try: try:
try: size = int(plugin.get('size'))
except Exception: size = None
manifest.addPlugin( manifest.addPlugin(
url=plugin['url'], url=plugin['url'],
name=plugin.get('name'), name=plugin.get('name'),
description=plugin.get('description'), description=plugin.get('description'),
category=plugin.get('category'), category=plugin.get('category'),
checksum=plugin.get('checksum'), checksum=plugin.get('checksum'),
size=size,
version=plugin.get('version'), version=plugin.get('version'),
termsOfUse=plugin.get('terms_of_use'), terms_of_use=plugin.get('terms_of_use'),
destination=plugin.get('destination')) destination=plugin.get('destination'))
except KeyError as e: except KeyError as e:
raise ManifestInvalid(f'The manifest file {url} ' raise ManifestInvalid(f'The manifest file {url} '
...@@ -291,3 +311,15 @@ def downloadManifest(url : Union[str, pathlib.Path]) -> Manifest: ...@@ -291,3 +311,15 @@ def downloadManifest(url : Union[str, pathlib.Path]) -> Manifest:
'definition: {plugin}') from e 'definition: {plugin}') from e
return manifest return manifest
def genManifest(manifest : Manifest) -> str:
"""Converts the given manifest to JSON. """
fields = ['name', 'url', 'category', 'checksum', 'size', 'description',
'version', 'destination', 'terms_of_use']
plugins = list(manifest.values())
plugins = [dataclasses.asdict(p) for p in plugins]
plugins = [{f : p[f] for f in fields} for p in plugins]
plugins = [{k : v for k, v in p.items() if v is not None } for p in plugins]
return json.dumps(plugins, indent=4)
...@@ -73,14 +73,14 @@ def test_loadManifest_different_sources(): ...@@ -73,14 +73,14 @@ def test_loadManifest_different_sources():
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
fam.loadManifest(fam.parseArgs(['-m', 'nomanifest'])) fam.loadManifest(fam.parseArgs(['-m', 'nomanifest']))
# otherwise should work ok # Specify plugin URL
m, p = fam.loadManifest( m, p = fam.loadManifest(
fam.parseArgs(['-m', 'nomanifest', 'http://abc.com/plugin.zip'])) fam.parseArgs(['-m', 'nomanifest', 'http://abc.com/plugin.zip']))
assert p == ['plugin.zip'] assert p == ['plugin.zip']
assert list(m.keys()) == ['plugin.zip'] assert list(m.keys()) == ['plugin.zip']
# default manifest # default manifest
with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST_URL', url): with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST', url):
m, p = fam.loadManifest(fam.parseArgs([])) m, p = fam.loadManifest(fam.parseArgs([]))
assert len(p) == 0 assert len(p) == 0
assert list(m.keys()) == ['abc', 'def'] assert list(m.keys()) == ['abc', 'def']
...@@ -158,7 +158,7 @@ def test_loadManifest_destination_specified(): ...@@ -158,7 +158,7 @@ def test_loadManifest_destination_specified():
def test_selectPlugins_from_filepath_and_url(): def test_selectPlugins_from_filepath_and_url():
with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST_URL', None): with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST', None):
args = fam.parseArgs('abc.zip def.zip -d dest'.split()) args = fam.parseArgs('abc.zip def.zip -d dest'.split())
m, p = fam.loadManifest(args) m, p = fam.loadManifest(args)
plugins = fam.selectPlugins(args, m, p) plugins = fam.selectPlugins(args, m, p)
...@@ -188,7 +188,7 @@ def test_selectPlugins_name_from_manifest(): ...@@ -188,7 +188,7 @@ def test_selectPlugins_name_from_manifest():
def test_selectPlugins_no_destination_specified(): def test_selectPlugins_no_destination_specified():
with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST_URL', None): with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST', None):
args = fam.parseArgs('abc.zip def.zip'.split()) args = fam.parseArgs('abc.zip def.zip'.split())
m, p = fam.loadManifest(args) m, p = fam.loadManifest(args)
...@@ -389,9 +389,9 @@ def test_main_noargs(): ...@@ -389,9 +389,9 @@ def test_main_noargs():
# patch os.environ in case FSLDIR is set # patch os.environ in case FSLDIR is set
fammod = 'fsl.scripts.fsl_add_module' fammod = 'fsl.scripts.fsl_add_module'
with mock.patch(f'{fammod}.DEFAULT_MANIFEST_URL', f'{cwd}/manifest.json'), \ with mock.patch(f'{fammod}.DEFAULT_MANIFEST', f'{cwd}/manifest.json'), \
mock.patch(f'{fammod}.DEFAULT_CATEGORY', None), \ mock.patch(f'{fammod}.DEFAULT_CATEGORY', None), \
mock.patch(f'{fammod}.ARCHIVE_DIR', f'{cwd}/archives'), \ mock.patch(f'{fammod}.ARCHIVE_DIR', f'{cwd}/archives'), \
mock.patch.dict(os.environ, clear=True): mock.patch.dict(os.environ, clear=True):
# user will be asked what plugins they want, # user will be asked what plugins they want,
...@@ -414,7 +414,7 @@ def test_main_list(): ...@@ -414,7 +414,7 @@ def test_main_list():
with open('manifest.json', 'wt') as f: with open('manifest.json', 'wt') as f:
f.write(json.dumps(manifest)) f.write(json.dumps(manifest))
with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST_URL', with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST',
f'{cwd}/manifest.json'): f'{cwd}/manifest.json'):
fam.main(['-l']) fam.main(['-l'])
fam.main('-l -v'.split()) fam.main('-l -v'.split())
...@@ -511,8 +511,8 @@ def test_main_plugin_paths(): ...@@ -511,8 +511,8 @@ def test_main_plugin_paths():
args = f'./abc.zip http://localhost:{srv.port}/def.zip'.split() args = f'./abc.zip http://localhost:{srv.port}/def.zip'.split()
fammod = 'fsl.scripts.fsl_add_module' fammod = 'fsl.scripts.fsl_add_module'
with mock.patch(f'{fammod}.DEFAULT_MANIFEST_URL', None), \ with mock.patch(f'{fammod}.DEFAULT_MANIFEST', None), \
mock.patch(f'{fammod}.ARCHIVE_DIR', f'{cwd}/archives'), \ mock.patch(f'{fammod}.ARCHIVE_DIR', f'{cwd}/archives'), \
mock.patch.dict(os.environ, clear=True): mock.patch.dict(os.environ, clear=True):
with mock_input(''): with mock_input(''):
...@@ -615,7 +615,7 @@ def test_main_skip_already_downloaded(): ...@@ -615,7 +615,7 @@ def test_main_skip_already_downloaded():
# direct download - if file exists, it is # direct download - if file exists, it is
# assumed to be ok, and not re-downloaded. # assumed to be ok, and not re-downloaded.
with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST_URL', None): with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST', None):
fam.main(f'{url["abc"]} -a {archiveDir} -d {destDir} -f'.split()) fam.main(f'{url["abc"]} -a {archiveDir} -d {destDir} -f'.split())
assert os.stat(archivePath['abc']).st_mtime_ns == mtime['abc'] assert os.stat(archivePath['abc']).st_mtime_ns == mtime['abc']
check_dir(destDir, ['a/b', 'c/d']) check_dir(destDir, ['a/b', 'c/d'])
...@@ -652,7 +652,7 @@ def test_main_customise_plugin_dir(): ...@@ -652,7 +652,7 @@ def test_main_customise_plugin_dir():
os.mkdir(defdest) os.mkdir(defdest)
os.mkdir(ghidest) os.mkdir(ghidest)
with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST_URL', None), \ with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST', None), \
mock_input('c', abcdest, defdest, ghidest): mock_input('c', abcdest, defdest, ghidest):
fam.main(f'abc.zip def.zip ghi.zip'.split()) fam.main(f'abc.zip def.zip ghi.zip'.split())
...@@ -743,7 +743,7 @@ def test_default_categories(): ...@@ -743,7 +743,7 @@ def test_default_categories():
make_archive('d.zip', 'd/d.txt') make_archive('d.zip', 'd/d.txt')
# default -> only fsl_course_data downloaded # default -> only fsl_course_data downloaded
with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST_URL', with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST',
f'{cwd}/manifest.json'): f'{cwd}/manifest.json'):
fam.main('-f -a archives -d .'.split()) fam.main('-f -a archives -d .'.split())
check_dir('.', check_dir('.',
...@@ -758,7 +758,7 @@ def test_default_categories(): ...@@ -758,7 +758,7 @@ def test_default_categories():
shutil.rmtree('d') shutil.rmtree('d')
# --category all -> all modules downloaded # --category all -> all modules downloaded
with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST_URL', with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST',
f'{cwd}/manifest.json'): f'{cwd}/manifest.json'):
fam.main('-f -a archives -d . -c all'.split()) fam.main('-f -a archives -d . -c all'.split())
check_dir('.', check_dir('.',
...@@ -775,7 +775,7 @@ def test_default_categories(): ...@@ -775,7 +775,7 @@ def test_default_categories():
shutil.rmtree('d') shutil.rmtree('d')
# --category <something> -> <something> modules downloaded # --category <something> -> <something> modules downloaded
with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST_URL', with mock.patch('fsl.scripts.fsl_add_module.DEFAULT_MANIFEST',
f'{cwd}/manifest.json'): f'{cwd}/manifest.json'):
fam.main('-f -a archives -d . -c patches'.split()) fam.main('-f -a archives -d . -c patches'.split())
check_dir('.', check_dir('.',
......
#!/usr/bin/env python
#
# test_fsl_generate_module_manifest.py -
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
import os.path as op
import json
import shlex
import textwrap as tw
from . import tempdir, touch
import fsl.add_module.routines as routines
import fsl.scripts.fsl_generate_module_manifest as fgmm
def test_fsl_generate_module_manifest():
plugins = ['1.zip', '2.zip', '3.zip']
existing = tw.dedent("""
[{
"name": "1.zip",
"url": "http://a.com/1.zip",
"category": "cat1",
"description": "desc1",
"checksum": "baba",
"size": 3
},
{
"name": "2.zip",
"url": "http://a.com/2.zip",
"category": "cat2",
"description": "desc2",
"checksum": "abcdf",
"size": 4
},
{
"name": "3.zip",
"url": "http://a.com/3.zip",
"category": "cat3",
"description": "desc3",
"checksum": "aew",
"size": 10
}]
""").strip()
with tempdir():
for p in plugins:
touch(p)
fgmm.main(shlex.split('-o manifest.json') + plugins)
with open('manifest.json', 'rt', encoding='utf-8') as f:
got = json.load(f)
assert len(got) == len(plugins)
for expp, gotp in zip(plugins, got):
assert gotp['name'] == expp
assert gotp['size'] == op.getsize(expp)
assert gotp['checksum'] == routines.calcChecksum(expp)
with open('existing.json', 'wt', encoding='utf-8') as f:
f.write(existing)
fgmm.main(shlex.split('-o manifest.json -m existing.json') + plugins)
with open('manifest.json', 'rt', encoding='utf-8') as f:
got = json.load(f)
assert len(got) == len(plugins)
for i, (expp, gotp) in enumerate(zip(plugins, got), 1):
assert gotp['name'] == expp
assert gotp['size'] == op.getsize(expp)
assert gotp['checksum'] == routines.calcChecksum(expp)
assert gotp['category'] == f'cat{i}'
assert gotp['description'] == f'desc{i}'
assert gotp['url'] == f'http://a.com/{i}.zip'
#!/usr/bin/env python #!/usr/bin/env python
# #
# test_plugin_manifest.py - # test_manifest.py -
# #
# Author: Paul McCarthy <pauldmccarthy@gmail.com> # Author: Paul McCarthy <pauldmccarthy@gmail.com>
# #
...@@ -14,7 +14,7 @@ from unittest import mock ...@@ -14,7 +14,7 @@ from unittest import mock
from . import tempdir, touch from . import tempdir, touch
import fsl.add_module.plugin_manifest as plgman import fsl.add_module.manifest as plgman
def test_downloadManifest(): def test_downloadManifest():
...@@ -115,3 +115,17 @@ def test_addPlugin(): ...@@ -115,3 +115,17 @@ def test_addPlugin():
assert manifest['plugin13'].url == 'http://abc.com/plugin13' assert manifest['plugin13'].url == 'http://abc.com/plugin13'
manifest.addPlugin('http://abc.com/plugin14', name='plugin13') manifest.addPlugin('http://abc.com/plugin14', name='plugin13')
assert manifest['plugin13'].url == 'http://abc.com/plugin14' assert manifest['plugin13'].url == 'http://abc.com/plugin14'
def test_genManifest():
m = plgman.Manifest()
m['plugin1'] = plgman.Plugin(url='http://a.b.c.1', name='plugin1', category='abc')
m['plugin2'] = plgman.Plugin(url='http://a.b.c.2', name='plugin2', description='Second')
exp = [{'name' : 'plugin1', 'url' : 'http://a.b.c.1', 'category' : 'abc'},
{'name' : 'plugin2', 'url' : 'http://a.b.c.2', 'category' : 'uncategorised',
'description' : 'Second'}]
got = plgman.genManifest(m)
assert exp == json.loads(got)
...@@ -14,9 +14,9 @@ import pathlib as plib ...@@ -14,9 +14,9 @@ import pathlib as plib
import pytest import pytest
import fsl.add_module.routines as routines import fsl.add_module.routines as routines
import fsl.add_module.plugin_manifest as plgman import fsl.add_module.manifest as plgman
import fsl.add_module.ui as ui import fsl.add_module.ui as ui
from . import tempdir, server, mock_input, make_archive, touch from . import tempdir, server, mock_input, make_archive, touch
......
...@@ -14,22 +14,22 @@ import pathlib ...@@ -14,22 +14,22 @@ import pathlib
from typing import Tuple, List, Union from typing import Tuple, List, Union
from fsl.add_module.routines import (expand, from fsl.add_module.routines import (expand,
calcChecksum, calcChecksum,
downloadFile, downloadFile,
CorruptArchive, CorruptArchive,
extractArchive) extractArchive)
from fsl.add_module.plugin_manifest import (Plugin, from fsl.add_module.manifest import (Plugin,
Manifest, Manifest,
downloadManifest) downloadManifest)
from fsl.add_module.messages import (info, from fsl.add_module.messages import (info,
important, important,
question, question,
warning, warning,
error, error,
prompt, prompt,
EMPHASIS, EMPHASIS,
UNDERLINE) UNDERLINE)
def downloadPluginManifest(url : Union[str, pathlib.Path]) -> Manifest: def downloadPluginManifest(url : Union[str, pathlib.Path]) -> Manifest:
...@@ -62,10 +62,15 @@ def printPlugin(plugin : Plugin, ...@@ -62,10 +62,15 @@ def printPlugin(plugin : Plugin,
:arg verbose: If ``True``, more information is printed. :arg verbose: If ``True``, more information is printed.
""" """
if index: header = plugin.name
info(f'{index:2d} {plugin.name:25s}', EMPHASIS, indent=2)
else: if index is not None:
info(f'{plugin.name:25s}', EMPHASIS, indent=2) header = f'{index:2d} {header}'
if plugin.size is not None:
mbytes = plugin.size / 1048576
header = f'{header} [{mbytes:0.2f} MB]'
info(header, EMPHASIS, indent=2)
if plugin.version is not None: if plugin.version is not None:
info(f'[version {plugin.version}]', indent=4) info(f'[version {plugin.version}]', indent=4)
...@@ -73,11 +78,11 @@ def printPlugin(plugin : Plugin, ...@@ -73,11 +78,11 @@ def printPlugin(plugin : Plugin,
if plugin.description is not None: if plugin.description is not None:
info(plugin.description, indent=4, wrap=True) info(plugin.description, indent=4, wrap=True)
if plugin.termsOfUse is not None: if plugin.terms_of_use is not None:
info('Installation of this plugin is subject ' info('Installation of this plugin is subject '
'to the following terms of use:', 'to the following terms of use:',
indent=4) indent=4)
info(plugin.termsOfUse, indent=6, wrap=True) info(plugin.terms_of_use, indent=6, wrap=True)
if verbose: if verbose:
for item in ('url', 'destination', 'checksum'): for item in ('url', 'destination', 'checksum'):
......
...@@ -35,9 +35,9 @@ Plugin manifest file ...@@ -35,9 +35,9 @@ Plugin manifest file
The plugin manifest file is a JSON file which contains information about all The plugin manifest file is a JSON file which contains information about all
plugins that are available. An official manifest file is hosted on the FSL plugins that are available. An official manifest file is hosted on the FSL
website, but an alternative manifest file may be specified when this script website, but an alternative manifest file may be specified when this script is
is invoked. See the :mod:`.plugin_manifest` module for more details on the invoked. See the :mod:`.manifest` module for more details on the format of a
format of a plugin manifest file. plugin manifest file.
Usage examples Usage examples
...@@ -102,24 +102,32 @@ import argparse ...@@ -102,24 +102,32 @@ import argparse
from typing import List, Tuple from typing import List, Tuple
import fsl.add_module.ui as ui import fsl.add_module.ui as ui
import fsl.add_module.routines as routines import fsl.add_module.routines as routines
import fsl.add_module.plugin_manifest as plgman import fsl.add_module.manifest as plgman
from fsl.add_module import __version__ from fsl.add_module import __version__
from fsl.add_module.messages import (info, from fsl.add_module.messages import (info,
important, important,
warning, warning,
error, error,
EMPHASIS, EMPHASIS,
UNDERLINE) UNDERLINE)
DEFAULT_MANIFEST_URL = 'http://fsl.fmrib.ox.ac.uk/fslcourse/downloads/manifest.json' OFFICIAL_MANIFEST_URLS = {
"""Location of the official FSL plugin manifest file, downloaded if an 'fslcourse' : 'http://fsl.fmrib.ox.ac.uk/fslcourse/downloads/manifest.json',
alternate manifest file is not specified. 'gradcourse' : 'https://fsl.fmrib.ox.ac.uk/fslcourse/graduate/downloads/manifest.json'
}
"""Locations of the official FSL plugin manifest files, downloaded if an
alternate manifest file is not specified. These 'built-in' manifest URLs
can be referred to by their aliases.
""" """
DEFAULT_MANIFEST = 'fslcourse'
"""Default manifest to use, if one is not specified."""
DEFAULT_CATEGORY = 'fsl_course_data' DEFAULT_CATEGORY = 'fsl_course_data'
"""Default value for the ``--category`` command-line option, when the """Default value for the ``--category`` command-line option, when the
user does not specify an alternative ``--manifest`` or ``--category``. user does not specify an alternative ``--manifest`` or ``--category``.
...@@ -139,9 +147,12 @@ def parseArgs(argv : List[str]) -> argparse.Namespace: ...@@ -139,9 +147,12 @@ def parseArgs(argv : List[str]) -> argparse.Namespace:
'version' : 'Print version and exit.', 'version' : 'Print version and exit.',
'verbose' : 'Output more information.', 'verbose' : 'Output more information.',
'list' : 'Print all available modules and exit. All other ' 'list' : 'Print all available modules and exit. All other '
'options, apart from --manifest, are ignored.', 'options, apart from --manifest and --verbose, are '
'ignored.',
'module' : 'Name or URL of FSL module to download.', 'module' : 'Name or URL of FSL module to download.',
'manifest' : 'URL to module manifest file.', 'manifest' : 'Name of official manifest file to download, or '
'alternate URL to module manifest file (default: '
f'{DEFAULT_MANIFEST})',
'archiveDir' : 'Directory to cache downloaded files in.', 'archiveDir' : 'Directory to cache downloaded files in.',
'category' : 'Only display available modules from the specified ' 'category' : 'Only display available modules from the specified '
'category. Defaults to "fsl_course_data", unless ' 'category. Defaults to "fsl_course_data", unless '
...@@ -187,12 +198,23 @@ def parseArgs(argv : List[str]) -> argparse.Namespace: ...@@ -187,12 +198,23 @@ def parseArgs(argv : List[str]) -> argparse.Namespace:
# course data. If we are using a custom manifest, # course data. If we are using a custom manifest,
# we don't want to set a default category. # we don't want to set a default category.
if args.manifest is None: if args.manifest is None:
args.manifest = DEFAULT_MANIFEST_URL args.manifest = DEFAULT_MANIFEST
if args.manifest == DEFAULT_MANIFEST_URL and args.category is None: if args.manifest == DEFAULT_MANIFEST and args.category is None:
args.category = DEFAULT_CATEGORY args.category = DEFAULT_CATEGORY
if args.category == 'all': if args.category == 'all':
args.category = None args.category = None
# Built-in manifest referred to by alias, e.g. "fslcourse"?
if not (args.manifest is None or
args.manifest.startswith('http') or
op.exists(args.manifest)):
try:
args.manifest = OFFICIAL_MANIFEST_URLS[args.manifest]
except KeyError:
warning(f'Unknown manifest: {args.manifest}. Known manifests: ' +
", ".join(OFFICIAL_MANIFEST_URLS.keys()) + ' (or give a '
'URL to an alternative manifest file)')
if args.archiveDir: if args.archiveDir:
args.archiveDir = op.abspath(op.expanduser(args.archiveDir)) args.archiveDir = op.abspath(op.expanduser(args.archiveDir))
......
#!/usr/bin/env python
#
# fsl_generate_module_manifest.py - Generate a template manifest.json file for
# use by fsl_add_module.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""The ``fsl_generate_module_manifest`` script is intended for use by FSL
dataset maintainers. It can be used to generate a JSON manifest file which
describes a set of archive/plugin files that are to be made available for
download.
"""
import sys
import argparse
import contextlib as ctxlib
import os.path as op
from typing import List
import fsl.add_module.manifest as plgman
import fsl.add_module.routines as routines
def parseArgs(argv : List[str]) -> argparse.Namespace:
"""Parse command-line arguments. The user is expected to provide a set of
input archive/plugin files and a destination file to save the JSON
manifest. An existing manifest file/URL can optionally be provided -
metadata will be copied over from this file for archive files with
matching names.
"""
parser = argparse.ArgumentParser(
'fsl_generate_module_manifest',
usage='fsl_generate_module_manifest [options] infile [infile ...]',
description='Generate a manifest.json file for use by fsl_add_module')
parser.add_argument('-m', '--manifest',
help='Previous version of manifest to copy metadata '
'from. The name, category, url, description, and '
'destination fields are all copied across for '
'archive files with matching names.')
parser.add_argument('-o', '--outfile',
help='Location to save new manifest (default: stdout)')
parser.add_argument('infile', nargs='+',
help='Archive file to include in new manifest.')
return parser.parse_args(argv)
def main(argv=None):
"""Main routine. Generates a JSON manifest describing a set of files.
"""
if argv is None:
argv = sys.argv[1:]
args = parseArgs(argv)
if args.manifest is not None:
oldmanifest = plgman.downloadManifest(args.manifest)
oldmanifest = {p.rawFileName : p for p in oldmanifest.values()}
else:
oldmanifest = plgman.Manifest()
newmanifest = plgman.Manifest()
for infile in args.infile:
basename = op.basename(infile)
if basename in oldmanifest:
plugin = oldmanifest[basename]
else:
plugin = plgman.Plugin(url='', name=basename)
plugin.size = op.getsize(infile)
plugin.checksum = routines.calcChecksum(infile)
newmanifest[plugin.name] = plugin
newmanifest = plgman.genManifest(newmanifest)
if args.outfile is None:
print(newmanifest)
else:
with open(args.outfile, 'wt', encoding='utf-8') as f:
f.write(newmanifest)
if __name__ == '__main__':
main()
...@@ -52,7 +52,8 @@ setup( ...@@ -52,7 +52,8 @@ setup(
entry_points={ entry_points={
'console_scripts' : [ 'console_scripts' : [
'fsl_add_module = fsl.scripts.fsl_add_module:main', 'fsl_add_module = fsl.scripts.fsl_add_module:main',
'fsl_generate_module_manifest = fsl.scripts.fsl_generate_module_manifest:main',
] ]
} }
) )
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment