-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
744 additions
and
729 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
from abc import ABC, abstractmethod | ||
import h5py | ||
import os | ||
import sys | ||
|
||
if sys.version_info[0] < 3: | ||
raise ImportError('pysnirf2 requires Python > 3') | ||
|
||
|
||
class SnirfFormatError(Exception): | ||
pass | ||
|
||
|
||
class _Group(): | ||
|
||
def __init__(self, gid: h5py.h5g.GroupID): | ||
self._id = gid | ||
if not isinstance(gid, h5py.h5g.GroupID): | ||
raise TypeError('must initialize with a Group ID') | ||
self._group = h5py.Group(self._id) | ||
|
||
def __repr__(self): | ||
return str(list(filter(lambda x: not x.startswith('_'), vars(self).keys()))) | ||
|
||
|
||
|
||
class _IndexedGroup(list, ABC): | ||
""" | ||
Represents the "indexed group" which is defined by v1.0 of the SNIRF | ||
specification as: | ||
If a data element is an HDF5 group and contains multiple sub-groups, | ||
it is referred to as an indexed group. Each element of the sub-group | ||
is uniquely identified by appending a string-formatted index (starting | ||
from 1, with no preceding zeros) in the name, for example, /.../name1 | ||
denotes the first sub-group of data element name, and /.../name2 | ||
denotes the 2nd element, and so on. | ||
""" | ||
|
||
_name: str = '' | ||
_element: _Group = None | ||
|
||
def __init__(self, parent): | ||
if isinstance(parent, (h5py.Group, h5py.File)): | ||
# Because the indexed group is not a true HDF5 group but rather an | ||
# iterable list of HDF5 groups, it takes a base group or file and | ||
# searches its keys, appending the appropriate elements to itself | ||
# in order | ||
self._parent = parent | ||
print('class name', self.__class__.__name__, 'signature', self._name) | ||
i = 1 | ||
for key in self._parent.keys(): | ||
name = str(key).split('/')[-1] | ||
print('Looking for keys starting with', self._name) | ||
if key.startswith(self._name): | ||
if key.endswith(str(i)): | ||
print('adding numbered key', i, name) | ||
self.append(self._element(self._parent[key].id)) | ||
i += 1 | ||
elif i == 1 and key.endswith(self._name): | ||
print('adding non-numbered key', name) | ||
self.append(self._element(self._parent[key].id)) | ||
|
||
else: | ||
raise TypeError('must initialize _IndexedGroup with a Group or File') | ||
|
||
def __new__(cls, *args, **kwargs): | ||
if cls is _IndexedGroup: | ||
raise NotImplementedError('_IndexedGroup is an abstract class') | ||
return super().__new__(cls, *args, **kwargs) | ||
|
||
@abstractmethod | ||
def _append_group(self, gid: h5py.h5g.GroupID): | ||
raise NotImplementedError('_append_group is an abstract method') | ||
|
||
def __repr__(self): | ||
prettylist = '' | ||
for i in range(len(self)): | ||
prettylist += (str(self[i]) + '\n') | ||
return prettylist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
SPEC_SRC = 'https://raw.github.com/fNIRS/snirf/master/snirf_specification.md' | ||
SPEC_VERSION = '1.0' # Version of the spec linked above | ||
TYPELUT = { | ||
'GROUP': '{.}', | ||
'INDEXED_GROUP': '{i}', | ||
'REQUIRED': '*', | ||
'1D_ARRAY': '...]', | ||
'2D_ARRAY': '...]]', | ||
'INT_VALUE': '<i>', | ||
'FLOAT_VALUE': '<f>', | ||
'VAR_STRING': '"s"' | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
from jinja2 import Environment, FileSystemLoader, select_autoescape | ||
import requests | ||
from unidecode import unidecode | ||
from pathlib import Path | ||
from datetime import date | ||
import getpass | ||
import os | ||
|
||
|
||
from data import TYPELUT, SPEC_SRC, SPEC_VERSION | ||
|
||
""" | ||
Generates SNIRF interface and validator from the summary table of the specification | ||
hosted at SPEC_MD_RAW_SRC. | ||
""" | ||
|
||
env = Environment( | ||
loader=FileSystemLoader(searchpath='./'), | ||
autoescape=select_autoescape(), | ||
trim_blocks=True, | ||
lstrip_blocks=True, | ||
) | ||
|
||
template = env.get_template('pysnirf2.jinja') | ||
|
||
# Retrieve the SNIRF specification from GitHub and parse it for the summary table | ||
spec = requests.get(SPEC_SRC) | ||
table = unidecode(spec.text).split('### SNIRF data format summary')[1].split('In the above table, the used notations are explained below')[0] | ||
|
||
rows = table.split('\n') | ||
while '' in rows: | ||
rows.remove('') | ||
|
||
type_codes = [] | ||
|
||
# Parse each row of the summary table for programmatic info | ||
for row in rows[1:]: # Skip header row | ||
delim = row.split('|') | ||
|
||
# Number of leading spaces determines indent level, hierarchy | ||
namews = delim[1].replace('`', '').replace('.','').replace('-', '') | ||
name = namews.replace(' ', '').replace('/', '').replace('{i}', '') | ||
|
||
# Get name: format pairs for each name | ||
if len(name) > 1: # Skip the empty row | ||
type_code = delim[-2].replace(' ', '').replace('`', '') | ||
type_codes.append(type_code) | ||
|
||
# Parse headings in spec for complete HDF style locations with which to build tree | ||
definitions = unidecode(spec.text).split('### SNIRF data container definitions')[1].split('## Appendix')[0] | ||
lines = definitions.split('\n') | ||
while '' in lines: | ||
lines.remove('') | ||
|
||
locations = [] | ||
for line in lines: | ||
line = line.replace(' ', '') | ||
if line.startswith('####'): | ||
locations.append(line.replace('`', '').replace('#', '').replace(',', '')) | ||
|
||
# Create flat list of all nodes | ||
flat = [] | ||
|
||
# Append root | ||
flat.append({ | ||
'name': '', | ||
'location': '/', | ||
'parent': None, | ||
'type': None, | ||
'children': [], | ||
'required': False | ||
}) | ||
|
||
for i, location in enumerate(locations): | ||
type_code = type_codes[i] | ||
name = location.split('/')[-1].split('(')[0] # Remove (i), (j) | ||
parent = location.split('/')[-2].split('(')[0] # Remove (i), (j) | ||
# print(name, type_code) | ||
if type_code is not None: | ||
required = TYPELUT['REQUIRED'] in type_code | ||
else: | ||
required = None | ||
# if name in type_codes.keys(): | ||
# type_code = type_codes[name] | ||
# required = TYPELUT['REQUIRED'] in type_code | ||
# else: | ||
# warn('Found a name with no corresponding type: ' + name) | ||
# type_code = None | ||
# required = None | ||
if not any([location == node['location'] for node in flat]): | ||
flat.append({ | ||
'name': name, | ||
'location': location, | ||
'parent': parent, | ||
'type': type_code, | ||
'children': [], | ||
'required': required | ||
}) | ||
|
||
# Generate data for template | ||
SNIRF = { | ||
'BASE': '', | ||
'TYPES': TYPELUT, | ||
'USER': getpass.getuser(), | ||
'DATE': str(date.today()), | ||
'INDEXED_GROUPS': [], | ||
'GROUPS': [], | ||
} | ||
|
||
# Build list of groups and indexed groups | ||
for node in flat: | ||
if node['type'] is not None: | ||
for other_node in flat: | ||
if other_node['parent'] == node['name']: | ||
node['children'].append(other_node) | ||
if TYPELUT['GROUP'] in node['type']: | ||
SNIRF['GROUPS'].append(node) | ||
elif TYPELUT['INDEXED_GROUP'] in node['type']: | ||
SNIRF['INDEXED_GROUPS'].append(node) | ||
|
||
# Generate the complete Snirf interface from base.py and the template + data | ||
dst = os.path.abspath(os.path.join(os.getcwd(), os.pardir)) | ||
with open('base.py', 'r') as f_base: | ||
with open(dst + '/src/' + 'pysnirf2.py', "w") as f_out: | ||
SNIRF['BASE'] = f_base.read() | ||
f_out.write(template.render(SNIRF)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
{% macro sentencecase(text) %} | ||
{{- text[0]|upper}}{{text[1:] -}} | ||
{% endmacro %} | ||
{{ BASE }} | ||
|
||
# pysnirf2 dot jinja output | ||
# generated by {{ USER }} on {{ DATE }} | ||
{% for GROUP in GROUPS %} | ||
|
||
|
||
class {{ sentencecase(GROUP.name) -}}(_Group): | ||
|
||
{% for CHILD in GROUP.children %} | ||
{{ CHILD.name }} = None | ||
{% endfor %} | ||
|
||
def __init__(self, gid: h5py.h5g.GroupID): | ||
super().__init__(gid) | ||
{% for CHILD in GROUP.children %} | ||
{% if '{{TYPES.INDEXED_GROUP}}' in CHILD.type %} | ||
self.{{- CHILD.name }} = {{ sentencecase(CHILD.name) }}(self._group) # Indexed group | ||
{% elif '{{TYPES.GROUP}}' in CHILD.type %} | ||
if '{{ CHILD.name }}' in self._group.keys(): | ||
self.{{- CHILD.name }} = {{ sentencecase(CHILD.name) }}(self._group['{{CHILD.name}}'].id) # Group | ||
else: | ||
print('{{CHILD.name}}', 'not found in', '{{GROUP.name}}') | ||
{% else %} | ||
if '{{ CHILD.name }}' in self._group.keys(): | ||
if | ||
self.{{- CHILD.name }} = self._group['{{ CHILD.name }}'][0] | ||
else: | ||
print('{{CHILD.name}}', 'not found in', '{{GROUP.name}}') | ||
{% endif %} | ||
{% endfor %} | ||
{% endfor %} | ||
{% for INDEXED_GROUP in INDEXED_GROUPS %} | ||
|
||
|
||
class {{ sentencecase(INDEXED_GROUP.name) -}}Element(_Group): | ||
|
||
{% for CHILD in INDEXED_GROUP.children %} | ||
{{ CHILD.name }} = None | ||
{% endfor %} | ||
|
||
def __init__(self, gid: h5py.h5g.GroupID): | ||
super().__init__(gid) | ||
|
||
|
||
class {{ sentencecase(INDEXED_GROUP.name) -}}(_IndexedGroup): | ||
|
||
_name: str = '{{ INDEXED_GROUP.name }}' | ||
_element: _Group = {{ sentencecase(INDEXED_GROUP.name) -}}Element | ||
|
||
def __init__(self, f: h5py.File): | ||
super().__init__(f) | ||
|
||
def _append_group(self, gid): | ||
self.append({{ sentencecase(INDEXED_GROUP.name) -}}Element(gid)) | ||
{% endfor %} | ||
|
||
|
||
class Snirf(): | ||
|
||
_name = '/' | ||
formatVersion: str = None | ||
nirs: Nirs = () | ||
|
||
def __init__(self, argv): | ||
if not argv.endswith('.snirf'): | ||
path = argv.join('.snirf') | ||
else: | ||
path = argv | ||
if os.path.exists(path): | ||
self._f = h5py.File(path, 'r+') | ||
print('Loading snirf', path) | ||
keys = self._f.keys() | ||
if 'formatVersion' in keys: | ||
self.formatVersion = self._f['formatVersion'][0].decode('ascii') | ||
# else: | ||
# raise SnirfFormatError(path, 'does not have a valid formatVersion') | ||
self.nirs = Nirs(self._f) | ||
|
||
def __repr__(self): | ||
return str(vars(self)) | ||
|
||
def __del__(self): | ||
# TODO: this may be redundant | ||
self._f.close() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
[build-system] | ||
requires = [ | ||
"setuptools>=42", | ||
"wheel" | ||
] | ||
build-backend = "setuptools.build_meta" |
Oops, something went wrong.