first prototype

This commit is contained in:
2021-05-27 15:09:08 +02:00
parent b8d7e7fa6b
commit d1f44d04c1
11 changed files with 578 additions and 0 deletions

139
.gitignore vendored Normal file
View File

@ -0,0 +1,139 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/

19
example_scicat.py Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env python3
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
from scilog import SciCat
url = "https://dacat.psi.ch/api/v3/"
cat = SciCat(url)
props = cat.proposals
nprops = len(props)
print(f"got {nprops} proposals")
print(props[0])

41
example_scilog.py Executable file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env python3
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("pgroup", help="Expected form: p12345")
clargs = parser.parse_args()
pgroup = clargs.pgroup
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
from scilog import SciLog
url = "https://lnode2.psi.ch/api/v1"
log = SciLog(url)
#print(log.token)
loc = log.get_snippets(title="location", ownerGroup="admin")
assert len(loc) == 1
loc_id = loc[0]["id"]
print(loc_id)
lb = log.get_snippets(snippetType="logbook", ownerGroup=pgroup)
assert len(lb) == 1
lb_id = lb[0]["id"]
print(lb_id)
res = log.post_snippet(snippetType="paragraph", ownerGroup=pgroup, parentId=lb_id, textcontent="<p>from python</p>")
print(res)
snips = log.get_snippets(snippetType="paragraph", ownerGroup=pgroup)
print(snips)

5
scilog/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from .scicat import SciCat
from .scilog import SciLog

5
scilog/autherror.py Normal file
View File

@ -0,0 +1,5 @@
class AuthError(Exception):
pass

44
scilog/config.py Normal file
View File

@ -0,0 +1,44 @@
from pathlib import Path
import json
class Config(dict):
def __init__(self, fname, folder=None):
if folder is not None:
folder = Path(folder)
else:
folder = Path.home()
self.fname = folder / fname
content = self._load()
super().update(content)
def __setitem__(self, name, value):
self.update(**{name: value})
def update(self, **kwargs):
super().update(**kwargs)
self._save()
def _load(self):
fn = self.fname
if fn.exists():
return json_load(fn)
else:
return {}
def _save(self):
json_save(self, self.fname)
def json_save(what, filename, *args, indent=4, sort_keys=True, **kwargs):
with open(filename, "w") as f:
json.dump(what, f, *args, indent=indent, sort_keys=sort_keys, **kwargs)
def json_load(filename, *args, **kwargs):
with open(filename, "r") as f:
return json.load(f, *args, **kwargs)

11
scilog/mkfilt.py Normal file
View File

@ -0,0 +1,11 @@
import json
def make_filter(**kwargs):
items = [{k: v} for k, v in kwargs.items()]
filt = {"where": {"and": items}}
filt = json.dumps(filt)
return {"filter": filt}

69
scilog/scicat.py Normal file
View File

@ -0,0 +1,69 @@
import getpass
from .config import Config
from .utils import post_request, get_request
from .autherror import AuthError
AUTH_HEADERS = {
"Content-type": "application/json",
"Accept": "application/json"
}
class SciCat:
def __init__(self, address):
self.address = address.rstrip("/")
self._token = None
self.config = Config(".scicat-tokens")
def __repr__(self):
return f"SciCat @ {self.address}"
@property
def proposals(self):
url = self.address + "/proposals"
headers = self.auth_headers
return get_request(url, headers=headers)
@property
def auth_headers(self):
headers = AUTH_HEADERS.copy()
headers["Authorization"] = self.token
return headers
@property
def token(self):
username = getpass.getuser()
password = getpass.getpass(prompt=f"SciCat password for {username}: ")
token = self._token
if token is None:
try:
token = self.config[username]
except KeyError:
token = self.authenticate(username, password)
self.config[username] = self._token = token
return token
def authenticate(self, username, password):
url = self.address + "/users/login"
auth_payload = {
"username": username,
"password": password
}
res = post_request(url, auth_payload, AUTH_HEADERS)
try:
token = res["id"]
except KeyError as e:
raise SciCatAuthError(res) from e
else:
return token
class SciCatAuthError(AuthError):
pass

77
scilog/scilog.py Normal file
View File

@ -0,0 +1,77 @@
import getpass
from .config import Config
from .utils import post_request, get_request
from .autherror import AuthError
from .mkfilt import make_filter
AUTH_HEADERS = {
"Content-type": "application/json",
"Accept": "application/json"
}
class SciLog:
def __init__(self, address):
self.address = address.rstrip("/")
self._token = None
self.config = Config(".scilog-tokens")
def __repr__(self):
return f"SciLog @ {self.address}"
def get_snippets(self, **kwargs):
url = self.address + "/basesnippets"
params = make_filter(**kwargs)
headers = self.auth_headers
return get_request(url, params=params, headers=headers)
def post_snippet(self, **kwargs):
url = self.address + "/basesnippets"
payload = kwargs
headers = self.auth_headers
return post_request(url, payload=payload, headers=headers)
@property
def auth_headers(self):
headers = AUTH_HEADERS.copy()
headers["Authorization"] = self.token
return headers
@property
def token(self):
username = getpass.getuser()
token = self._token
if token is None:
try:
token = self.config[username]
except KeyError:
password = getpass.getpass(prompt=f"SciLog password for {username}: ")
token = self.authenticate(username, password)
self.config[username] = self._token = token
return token
def authenticate(self, username, password):
url = self.address + "/users/login"
auth_payload = {
"principal": username,
"password": password
}
res = post_request(url, auth_payload, AUTH_HEADERS)
try:
token = "Bearer " + res["token"]
except KeyError as e:
raise SciLogAuthError(res) from e
else:
return token
class SciLogAuthError(AuthError):
pass

20
scilog/utils.py Normal file
View File

@ -0,0 +1,20 @@
import requests
#TODO: add params/payload and response validation
def get_request(url, params=None, headers=None, timeout=10):
response = requests.get(url, params=params, headers=headers, timeout=timeout, verify=False).json()
return response
def post_request(url, payload=None, headers=None, timeout=10):
response = requests.post(url, json=payload, headers=headers, timeout=timeout, verify=False).json()
return response
def typename(obj):
return type(obj).__name__

148
update_locations.py Executable file
View File

@ -0,0 +1,148 @@
#!/usr/bin/env python3
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
from scicat import SciCat, SciLog
def prepare_location_snippet(log):
snips = log.get_snippets(title="location", ownerGroup="admin")
if snips:
assert len(snips) == 1
loc_id = snips[0]["id"]
return loc_id
new_loc = {
"ownerGroup": "admin",
"accessGroups": ["customer"],
"isPrivate": True,
"title": "location",
"snippetType": "paragraph",
}
snip = log.post_snippet(**new_loc)
loc_id = snip["id"]
return loc_id
def update_locations_and_proposals(log, loc_id, proposals):
_accessGroups, locations, proposalsStorage = _collect_data(proposals)
locationStorage = _update_locations(log, loc_id, locations)
_update_proposals(log, locationStorage, proposalsStorage)
def _collect_data(proposals):
accessGroups = set()
locations = set()
proposalsStorage = []
for prop in proposals:
for ag in prop["accessGroups"]:
accessGroups.add(ag)
loc = prop["MeasurementPeriodList"][0]["instrument"]
locations.add(loc)
proposalsStorage.append({
"ownerGroup": prop["ownerGroup"],
"abstract": prop["abstract"],
"title": prop["title"],
"location": prop["MeasurementPeriodList"][0]["instrument"]
})
return accessGroups, locations, proposalsStorage
def _update_locations(log, loc_id, locations):
locationStorage = dict()
for loc in locations:
#TODO: move this to the first loop?
if loc[:4] != "/PSI":
raise RuntimeError("Unexpected facility prefix")
snips = log.get_snippets(parentId=loc_id, location=loc)
if snips:
assert len(snips) == 1
snip = snips[0]
locationStorage[loc] = snip
continue
group = loc[5:].replace("/", "").lower()
new_snip = {
"ownerGroup": group,
"accessGroups": ["customer"],
"isPrivate": True,
"title": loc.split("/")[-1],
"location": loc,
"contact": group + "@psi.ch",
"snippetType": "image",
"parentId": loc_id,
"file": "files/default_logbook_icon.jpg"
}
if "thumbnail" in loc:
new_snip["file"] = loc["thumbnail"]
snip = log.post_request(**new_snip)
locationStorage[loc] = snip
return locationStorage
def _update_proposals(log, locationStorage, proposalsStorage):
for proposal in proposalsStorage:
ownerGroup = proposal["ownerGroup"]
loc = proposal["location"]
loc = locationStorage[loc]
new_snip = {
"ownerGroup": ownerGroup,
"accessGroups": [loc["ownerGroup"]],
"isPrivate": False,
"name": proposal["title"],
"location": loc["id"],
"description": proposal["abstract"],
"snippetType": "logbook"
}
if "file" in loc:
new_snip["thumbnail"] = loc["file"]
if not new_snip["name"]:
new_snip["name"] = ownerGroup
if not new_snip["description"]:
new_snip["description"] = "No proposal found."
snips = log.get_snippets(snippetType="logbook", ownerGroup=ownerGroup)
if snips:
print(f"Logbook exists already for ownerGroup {ownerGroup}")
continue
print(f"Adding new logbook for ownerGroup {ownerGroup}")
snip = log.post_snippet(**new_snip)
print(snip)
if __name__ == "__main__":
url = "https://dacat.psi.ch/api/v3/"
cat = SciCat(url)
props = cat.proposals
url = "https://lnode2.psi.ch/api/v1"
log = SciLog(url)
loc_id = prepare_location_snippet(log)
update_locations_and_proposals(log, loc_id, props)