Skip to content

Commit

Permalink
Add functionality for installing extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
ehennestad committed Nov 23, 2024
1 parent cfb374c commit 35150e9
Show file tree
Hide file tree
Showing 8 changed files with 353 additions and 0 deletions.
15 changes: 15 additions & 0 deletions +matnwb/+extension/dispExtensionInfo.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
function dispExtensionInfo(extensionName)
arguments
extensionName (1,1) string
end

T = matnwb.extension.listExtensions();
isMatch = T.name == extensionName;
extensionList = join( compose(" %s", [T.name]), newline );
assert( ...
any(isMatch), ...
'NWB:DisplayExtensionMetadata:ExtensionNotFound', ...
'Extension "%s" was not found in the extension catalog:\n%s', extensionName, extensionList)
metadata = table2struct(T(isMatch, :));
disp(metadata)
end
6 changes: 6 additions & 0 deletions +matnwb/+extension/installAll.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
function installAll()
T = matnwb.extension.listExtensions();
for i = 1:height(T)
matnwb.extension.installExtension( T.name(i) )
end
end
126 changes: 126 additions & 0 deletions +matnwb/+extension/installExtension.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
function installExtension(extensionName)
arguments
extensionName (1,1) string
end

repoTargetFolder = fullfile(userpath, "NWB-Extension-Source");
if ~isfolder(repoTargetFolder); mkdir(repoTargetFolder); end

T = matnwb.extension.listExtensions();
isMatch = T.name == extensionName;

extensionList = join( compose(" %s", [T.name]), newline );
assert( ...
any(isMatch), ...
'NWB:InstallExtension:ExtensionNotFound', ...
'Extension "%s" was not found in the extension catalog:\n', extensionList)

defaultBranchNames = ["main", "master"];

wasDownloaded = false;
for i = 1:2
try
repositoryUrl = T{isMatch, 'src'};
if endsWith(repositoryUrl, '/')
repositoryUrl = extractBefore(repositoryUrl, strlength(repositoryUrl));
end
if contains(repositoryUrl, 'github.com')
downloadUrl = sprintf( '%s/archive/refs/heads/%s.zip', repositoryUrl, defaultBranchNames(i) );

elseif contains(repositoryUrl, 'gitlab.com')
repoPathSegments = strsplit(repositoryUrl, '/');
repoName = repoPathSegments{end};
downloadUrl = sprintf( '%s/-/archive/%s/%s-%s.zip', ...
repositoryUrl, defaultBranchNames(i), repoName, defaultBranchNames(i));
else
error('NWB:InstallExtension:UnknownRepository', ...
'Extension "%s" is located in an unsupported repository / source location. Please create an issue on matnwb''s github page', extensionName)
end
repoTargetFolder = downloadZippedRepo(downloadUrl, repoTargetFolder, true, true);
wasDownloaded = true;
break
catch ME
if strcmp(ME.identifier, 'MATLAB:webservices:HTTP404StatusCodeError')
continue
else
rethrow(ME)
end
end
end
if ~wasDownloaded
error('NWB:InstallExtension:DownloadFailed', ...
'Failed to download spec for extension "%s"', extensionName)
end
L = dir(fullfile(repoTargetFolder, 'spec', '*namespace.yaml'));
assert(...
~isempty(L), ...
'NWB:InstallExtension:NamespaceNotFound', ...
'No namespace file was found for extension "%s"', extension.Identifier ...
)
assert(...
numel(L)==1, ...
'NWB:InstallExtension:MultipleNamespacesFound', ...
'More than one namespace file was found for extension "%s"', extension.Identifier ...
)
generateExtension( fullfile(L.folder, L.name) );
end

function repoFolder = downloadZippedRepo(githubUrl, targetFolder, updateFlag, throwErrorIfFails)
%downloadZippedRepo Download zipped repo

if nargin < 3; updateFlag = false; end
if nargin < 4; throwErrorIfFails = false; end

if isa(updateFlag, 'char') && strcmp(updateFlag, 'update')
updateFlag = true;
end

% Create a temporary path for storing the downloaded file.
[~, ~, fileType] = fileparts(githubUrl);
tempFilepath = [tempname, fileType];

% Download the file containing the addon toolbox
try
tempFilepath = websave(tempFilepath, githubUrl);
fileCleanupObj = onCleanup( @(fname) delete(tempFilepath) );
catch ME
if throwErrorIfFails
rethrow(ME)
end
end

unzippedFiles = unzip(tempFilepath, tempdir);
unzippedFolder = unzippedFiles{1};
if endsWith(unzippedFolder, filesep)
unzippedFolder = unzippedFolder(1:end-1);
end

[~, repoFolderName] = fileparts(unzippedFolder);
targetFolder = fullfile(targetFolder, repoFolderName);

if updateFlag && isfolder(targetFolder)

% Delete current version
if isfolder(targetFolder)
if contains(path, fullfile(targetFolder, filesep))
pathList = strsplit(path, pathsep);
pathList_ = pathList(startsWith(pathList, fullfile(targetFolder, filesep)));
rmpath(strjoin(pathList_, pathsep))
end
try
rmdir(targetFolder, 's')
catch
warning('Could not remove old installation... Please report')
end
end
else
% pass
end

movefile(unzippedFolder, targetFolder);

% Delete the temp zip file
clear fileCleanupObj

repoFolder = targetFolder;
end
53 changes: 53 additions & 0 deletions +matnwb/+extension/listExtensions.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
function extensionTable = listExtensions(options)
arguments
options.Refresh (1,1) logical = false
end

persistent extensionRecords

if isempty(extensionRecords) || options.Refresh
catalogUrl = "https://raw.githubusercontent.com/nwb-extensions/nwb-extensions.github.io/refs/heads/main/data/records.json";
extensionRecords = jsondecode(webread(catalogUrl));
extensionRecords = consolidateStruct(extensionRecords);

extensionRecords = struct2table(extensionRecords);

fieldsKeep = ["name", "version", "last_updated", "src", "license", "maintainers", "readme"];
extensionRecords = extensionRecords(:, fieldsKeep);

for name = fieldsKeep
if ischar(extensionRecords.(name){1})
extensionRecords.(name) = string(extensionRecords.(name));
end
end
end
extensionTable = extensionRecords;
end

function structArray = consolidateStruct(S)
% Get all field names of S
mainFields = fieldnames(S);

% Initialize an empty struct array
structArray = struct();

% Iterate over each field of S
for i = 1:numel(mainFields)
subStruct = S.(mainFields{i}); % Extract sub-struct

% Add all fields of the sub-struct to the struct array
fields = fieldnames(subStruct);
for j = 1:numel(fields)
structArray(i).(fields{j}) = subStruct.(fields{j});
end
end

% Ensure consistency by filling missing fields with []
allFields = unique([fieldnames(structArray)]);
for i = 1:numel(structArray)
missingFields = setdiff(allFields, fieldnames(structArray(i)));
for j = 1:numel(missingFields)
structArray(i).(missingFields{j}) = [];
end
end
end
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,14 @@ workspace/
*.swp
.DS_Store
+tests/env.mat

# Ignore everything in the +types/ folder
+types/*

# Explicitly include these subdirectories
!+types/+core/
!+types/+hdmf_common/
!+types/+hdmf_experimental/
!+types/+untyped/
!+types/+util/

79 changes: 79 additions & 0 deletions nwbInstallExtension.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
function nwbInstallExtension(extensionName)
% nwbInstallExtension - Installs a specified NWB extension.
%
% nwbInstallExtension(extensionName) installs a Neurodata Without Borders
% (NWB) extension to extend the functionality of the core NWB schemas.
% extensionName is a scalar string or a string array, containing the name
% of one or more extensions from the Neurodata Extesions Catalog
%
% Usage:
% nwbInstallExtension(extensionName)
%
% Valid Extension Names:
% - "ndx-miniscope"
% - "ndx-simulation-output"
% - "ndx-ecog"
% - "ndx-fret"
% - "ndx-icephys-meta"
% - "ndx-events"
% - "ndx-nirs"
% - "ndx-hierarchical-behavioral-data"
% - "ndx-sound"
% - "ndx-extract"
% - "ndx-photometry"
% - "ndx-acquisition-module"
% - "ndx-odor-metadata"
% - "ndx-whisk"
% - "ndx-ecg"
% - "ndx-franklab-novela"
% - "ndx-photostim"
% - "ndx-multichannel-volume"
% - "ndx-depth-moseq"
% - "ndx-probeinterface"
% - "ndx-dbs"
% - "ndx-hed"
% - "ndx-ophys-devices"
%
% Example:
% % Install the "ndx-miniscope" extension
% nwbInstallExtension("ndx-miniscope")
%
% See also:
% matnwb.extension.listExtensions, matnwb.extension.installExtension

arguments
extensionName (1,:) string {mustBeMember(extensionName, [...
"ndx-miniscope", ...
"ndx-simulation-output", ...
"ndx-ecog", ...
"ndx-fret", ...
"ndx-icephys-meta", ...
"ndx-events", ...
"ndx-nirs", ...
"ndx-hierarchical-behavioral-data", ...
"ndx-sound", ...
"ndx-extract", ...
"ndx-photometry", ...
"ndx-acquisition-module", ...
"ndx-odor-metadata", ...
"ndx-whisk", ...
"ndx-ecg", ...
"ndx-franklab-novela", ...
"ndx-photostim", ...
"ndx-multichannel-volume", ...
"ndx-depth-moseq", ...
"ndx-probeinterface", ...
"ndx-dbs", ...
"ndx-hed", ...
"ndx-ophys-devices" ...
] ...
)} = []
end
if isempty(extensionName)
T = matnwb.extension.listExtensions();
extensionList = join( compose(" %s", [T.name]), newline );
error("Please specify the name of an extension. Available extensions:\n\n%s\n", extensionList)
else
matnwb.extension.installExtension(extensionName)
end
end
35 changes: 35 additions & 0 deletions resources/function_templates/nwbInstallExtension.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
function nwbInstallExtension(extensionName)
% nwbInstallExtension - Installs a specified NWB extension.
%
% nwbInstallExtension(extensionName) installs a Neurodata Without Borders
% (NWB) extension to extend the functionality of the core NWB schemas.
% extensionName is a scalar string or a string array, containing the name
% of one or more extensions from the Neurodata Extesions Catalog
%
% Usage:
% nwbInstallExtension(extensionName)
%
% Valid Extension Names:
{{extensionNamesDoc}}
%
% Example:
% % Install the "ndx-miniscope" extension
% nwbInstallExtension("ndx-miniscope")
%
% See also:
% matnwb.extension.listExtensions, matnwb.extension.installExtension

arguments
extensionName (1,:) string {mustBeMember(extensionName, [...
{{extensionNames}} ...
] ...
)} = []
end
if isempty(extensionName)
T = matnwb.extension.listExtensions();
extensionList = join( compose(" %s", [T.name]), newline );
error("Please specify the name of an extension. Available extensions:\n\n%s\n", extensionList)
else
matnwb.extension.installExtension(extensionName)
end
end
28 changes: 28 additions & 0 deletions tools/maintenance/matnwb_createNwbInstallExtension.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
function matnwb_createNwbInstallExtension()
% matnwb_createNwbInstallExtension - Create nwbInstallExtension from template
%
% This function can be run to update the list of available extension
% names in the function's arguments block based on the neurodata
% extensions catalog

matnwbRootDir = misc.getMatnwbDir();
fcnTemplate = fileread(fullfile(matnwbRootDir, ...
'resources', 'function_templates', 'nwbInstallExtension.txt'));

extensionTable = matnwb.extension.listExtensions();
extensionNames = extensionTable.name;

indentStr = repmat(' ', 1, 12);
extensionNamesStr = compose("%s""%s""", indentStr, extensionNames);
extensionNamesStr = strjoin(extensionNamesStr, ", ..." + newline);
fcnStr = replace(fcnTemplate, "{{extensionNames}}", extensionNamesStr);

extensionNamesStr = compose("%% - ""%s""", extensionNames);
extensionNamesStr = strjoin(extensionNamesStr, newline);
fcnStr = replace(fcnStr, "{{extensionNamesDoc}}", extensionNamesStr);


fid = fopen(fullfile(matnwbRootDir, 'nwbInstallExtension.m'), "wt");
fwrite(fid, fcnStr);
fclose(fid);
end

0 comments on commit 35150e9

Please sign in to comment.