Skip to content
Snippets Groups Projects
Verified Commit 64f211c2 authored by Daniel Hornung's avatar Daniel Hornung
Browse files

Merge branch 'dev' into f-high-level-serialize

parents fd0650ae 2f7cb996
Branches
Tags
2 merge requests!189ENH: add convenience functions,!185High level API serialization
Pipeline #58804 passed
Showing
with 1209 additions and 520 deletions
......@@ -57,9 +57,8 @@ mypy:
tags: [ docker ]
stage: linting
script:
- pip install mypy types-PyYAML types-jsonschema types-requests types-setuptools types-lxml types-python-dateutil pytest
- pip install .[mypy,test]
- make mypy
allow_failure: true
# run unit tests
unittest_py3.8:
......@@ -70,8 +69,7 @@ unittest_py3.8:
script: &python_test_script
# Python docker has problems with tox and pip so use plain pytest here
- touch ~/.pylinkahead.ini
- pip install pynose pytest pytest-cov jsonschema>=4.4.0 setuptools
- pip install .
- pip install .[test]
- python -m pytest unittests
# This needs to be changed once Python 3.9 isn't the standard Python in Debian
......@@ -113,15 +111,8 @@ unittest_py3.13:
tags: [ docker ]
stage: test
needs: [ ]
image: python:3.13-rc
script:
# TODO: Replace by '*python_test_script' as soon as 3.13 has been officially released.
# Python docker has problems with tox and pip so use plain pytest here
- apt update && apt install -y cargo
- touch ~/.pylinkahead.ini
- pip install pynose pytest pytest-cov jsonschema>=4.4.0 setuptools
- pip install .
- python -m pytest unittests
image: python:3.13
script: *python_test_script
# Trigger building of server image and integration tests
trigger_build:
......
......@@ -9,16 +9,91 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ###
* New setup extra `test` which installs the dependencies for testing.
### Changed ###
### Deprecated ###
### Removed ###
### Fixed ###
* [#73](https://gitlab.com/linkahead/linkahead-pylib/-/issues/73)
`Entity.to_xml` now detects potentially infinite recursion and prevents an error
* [#89](https://gitlab.com/linkahead/linkahead-pylib/-/issues/89)
`to_xml` does not add `noscript` or `TransactionBenchmark` tags anymore
* [#103](https://gitlab.com/linkahead/linkahead-pylib/-/issues/103)
`authentication/interface/on_response()` does not overwrite
`auth_token` if new value is `None`
* [#119](https://gitlab.com/linkahead/linkahead-pylib/-/issues/119)
The diff returned by compare_entities now uses id instead of name as key if either property does not have a name
### Security ###
### Documentation ###
## [0.16.0] - 2024-11-13 ##
### Added ###
* `ParentList` and `PropertyList` now have a `filter` function that allows to select a subset of
the contained elements by ID and/or name.
* Official support for Python 3.13
* Added arguments to `describe_diff` that allow customizing the labels for the 'old' and the 'new' diffs.
* Optional `realm` argument for `linkahead_admin.py set_user_password`
which defaults to `None`, i.e., the server's default realm.
### Changed ###
* `compare_entities` is now case insensitive with respect to property and
recordtype names
* `_ParentList` is now called `ParentList`
* `_Properties` is now called `PropertyList`
* `ParentList.remove` is now case insensitive when a name is used.
### Deprecated ###
* the use of the arguments `old_entity` and `new_entity` in `compare_entities`
is now deprecated. Please use `entity0` and `entity1` respectively instead.
### Fixed ###
* [gitlab.indiscale.com#200](https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/issues/200)
``linkahead_admin.py`` prints reasonable error messages when users
or roles don't exist.
## [0.15.1] - 2024-08-21 ##
### Deprecated ###
* `connection.get_username`. Use `la.Info().user_info.name` instead.
### Fixed ###
* [#128](https://gitlab.com/linkahead/linkahead-pylib/-/issues/128)
Assign `datetime.date` or `datetime.datetime` values to `DATETIME`
properties.
### Documentation ###
* Added docstrings for `linkahead.models.Info` and `linkahead.models.UserInfo`.
## [0.15.0] - 2024-07-09 ##
### Added ###
* Support for Python 3.12
* The `linkahead` module now opts into type checking and supports mypy.
* [#112](https://gitlab.com/linkahead/linkahead-pylib/-/issues/112)
`Entity.update_acl` now supports optional `**kwargs` that are passed to the
`Entity.update` method that is called internally, thus allowing, e.g.,
updating the ACL despite possible naming collisions with `unique=False`.
* a `role` argument for `get_entity_by_name` and `get_entity_by_id`
### Changed ###
### Deprecated ###
* Using environment variable PYLINKAHEADINI instead of PYCAOSDBINI.
### Removed ###
......@@ -34,10 +109,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#120](https://gitlab.com/linkahead/linkahead-pylib/-/issues/120) Unwanted
subproperties in reference properties.
### Security ###
### Documentation ###
* Added documentation and a tutorial example for the usage of the `page_length`
argument of `execute_query`.
## [0.14.0] - 2024-02-20
### Added ###
......
......@@ -20,6 +20,6 @@ authors:
given-names: Stefan
orcid: https://orcid.org/0000-0001-7214-8125
title: CaosDB - Pylib
version: 0.14.0
version: 0.16.0
doi: 10.3390/data4020083
date-released: 2024-02-20
date-released: 2024-11-13
......@@ -2,20 +2,40 @@
## Installation ##
### Requirements ###
### How to install ###
First ensure that python with at least version 3.8 is installed. Should this not be
the case, you can use the [Installing python](#installing-python-) guide for your OS.
PyCaosDB needs at least Python 3.8. Additionally, the following packages are required (they will
typically be installed automatically):
#### Generic installation ####
- `lxml`
- `PyYaml`
- `PySocks`
To install this LinkAhead python client locally, use `pip`/`pip3`:
Optional packages:
- `keyring`
```sh
pip install linkahead
```
#### Additional dependencies ####
To test using tox, you also need to install tox:
`pip install tox`
To install dependencies used by optional functionality, the following pip extras
keywords are defined:
- `test` for testing with pytest
- `mypy` for mypy and types
- `jsonschema`
- `keyring`
### How to install ###
These extras can be installed using:
```sh
pip install .[KEYWORD]
```
A current list of the dependencies installed with this program as well as those installed with
the keywords can be found in `setup.py`s `setup_package()` method, in the `metadata` dictionary
entries `install_requires` and `extras_require`.
### Installing python ###
#### Linux ####
......@@ -51,34 +71,7 @@ cd /Applications/Python\ 3.9/
sudo ./Install\ Certificates.command
```
After these steps, you may continue with the [Generic
installation](#generic-installation).
#### Generic installation ####
To install PyCaosDB locally, use `pip3` (also called `pip` on some systems):
```sh
pip3 install --user caosdb
```
---
Alternatively, obtain the sources from GitLab and install from there (`git` must be installed for
this option):
```sh
git clone https://gitlab.com/caosdb/caosdb-pylib
cd caosdb-pylib
pip3 install --user .
```
For installation of optional packages, install with an additional option, e.g. for
validating with the caosdb json schema:
```sh
pip3 install --user .[jsonschema]
```
After these steps, you may continue with the [Generic installation](#generic-installation) section.
## Configuration ##
......@@ -87,7 +80,7 @@ is described in detail in the [configuration section of the documentation](https
## Try it out ##
Start Python and check whether the you can access the database. (You will be asked for the
Start Python and check whether you can access the database. (You will be asked for the
password):
```python
......@@ -107,6 +100,7 @@ Now would be a good time to continue with the [tutorials](tutorials/index).
- Run all tests: `tox` or `make unittest`
- Run a specific test file: e.g. `tox -- unittests/test_schema.py`
- Run a specific test function: e.g. `tox -- unittests/test_schema.py::test_config_files`
- To run using pytest: `pytest .`
## Documentation ##
We use sphinx to create the documentation. Docstrings in the code should comply
......@@ -114,12 +108,6 @@ with the Googly style (see link below).
Build documentation in `build/` with `make doc`.
### Requirements ###
- `sphinx`
- `sphinx-autoapi`
- `recommonmark`
### How to contribute ###
- [Google Style Python Docstrings](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html)
......@@ -127,7 +115,7 @@ Build documentation in `build/` with `make doc`.
- [References to other documentation](https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#role-external)
### Troubleshooting ###
If the client is to be executed directly from the `/src` folder, an initial `.\setup.py install --user` must be called.
If the client is to be executed directly from the `/src` folder, an initial `.\setup.py install` must be called.
## Migration ##
TODO
# To be found be the caosdb package, the INI file must be located either in
# - $CWD/pylinkahead.ini
# - $HOME/.pylinkahead.ini
# - the location given in the env variable PYCAOSDBINI
# - the location given in the env variable PYLINKAHEADINI
[Connection]
# URL of the CaosDB server
......
......@@ -48,7 +48,7 @@ from setuptools import find_packages, setup
ISRELEASED = False
MAJOR = 0
MINOR = 14
MINOR = 16
MICRO = 1
# Do not tag as pre-release until this commit
# https://github.com/pypa/packaging/pull/515
......@@ -187,11 +187,27 @@ def setup_package():
'PyYAML>=5.4.1',
'future',
],
extras_require={'keyring': ['keyring>=13.0.0'],
'jsonschema': ['jsonschema>=4.4.0']},
extras_require={
"jsonschema": ["jsonschema>=4.4.0"],
"keyring": ["keyring>=13.0.0"],
"mypy": [
"mypy",
"types-PyYAML",
"types-jsonschema",
"types-requests",
"types-setuptools",
"types-lxml",
"types-python-dateutil",
],
"test": [
"pytest",
"pytest-cov",
"coverage>=4.4.2",
"jsonschema>=4.4.0",
]
},
setup_requires=["pytest-runner>=2.0,<3dev"],
tests_require=["pytest", "pytest-cov", "coverage>=4.4.2",
"jsonschema>=4.4.0"],
package_data={
'linkahead': ['py.typed', 'cert/indiscale.ca.crt', 'schema-pycaosdb-ini.yml'],
},
......
......@@ -25,14 +25,14 @@ import sphinx_rtd_theme # noqa: E402
# -- Project information -----------------------------------------------------
project = 'pylinkahead'
copyright = '2023, IndiScale GmbH'
copyright = '2024, IndiScale GmbH'
author = 'Daniel Hornung'
# The short X.Y version
version = '0.14.1'
version = '0.16.1'
# The full version, including alpha/beta/rc tags
# release = '0.5.2-rc2'
release = '0.14.1-dev'
release = '0.16.1-dev'
# -- General configuration ---------------------------------------------------
......
# Configuration of PyLinkAhead #
The behavior of PyLinkAhead is defined via a configuration that is provided using configuration files.
PyLinkAhead tries to read from the inifile specified in the environment variable `PYCAOSDBINI` or
PyLinkAhead tries to read from the inifile specified in the environment variable `PYLINKAHEADINI` or
alternatively in `~/.pylinkahead.ini` upon import. After that, the ini file `pylinkahead.ini` in the
current working directory will be read additionally, if it exists.
......
......@@ -75,3 +75,54 @@ Examples
b = input("Press any key to cleanup.")
# cleanup everything after the user presses any button.
c.delete()
Finding parents and properties
--------
To find a specific parent or property of an Entity, its
ParentList or PropertyList can be filtered using names, ids, or
entities. A short example:
.. code-block:: python3
import linkahead as db
# Setup a record with six properties
r = db.Record()
p1_1 = db.Property(id=101, name="Property 1")
p1_2 = db.Property(name="Property 1")
p2_1 = db.Property(id=102, name="Property 2")
p2_2 = db.Property(id=102)
p2_3 = db.Property(id=102, name="Other Property")
p3 = db.Property(id=104, name="Other Property")
r.add_property(p1_1).add_property(p1_2).add_property(p2_1)
r.add_property(p2_2).add_property(p2_3).add_property(p3)
properties = r.properties
# As r only has one property with id 101, this returns a list containing only p1_1
properties.filter(pid=101)
# Result: [p1_1]
# Filtering with name="Property 1" returns both p1_1 and p1_2, as they share their name
properties.filter(name="Property 1")
# Result: [p1_1, p1_2]
# If both name and pid are given, matching is based only on pid for all entities that have an id
properties.filter(pid="102", name="Other Property")
# Result: [p2_1, p2_2, p2_3]
# However, filtering with name="Property 1" and id=101 returns both p1_1 and p1_2, because
# p1_2 does not have an id and matches the name
properties.filter(pid="101", name="Property 1")
# Result: [p1_1, p1_2]
# We can also filter using an entity, in which case the name and id of the entity are used:
properties.filter(pid="102", name="Property 2") == properties.filter(p2_1)
# Result: True
# If we only need properties that match both id and name, we can set the parameter
# conjunction to True:
properties.filter(pid="102", name="Property 2", conjunction=True)
# Result: [p2_1]
The filter function of ParentList works analogously.
......@@ -15,6 +15,7 @@ advanced usage of the Python client.
Data-Insertion
errors
Entity-Getters
paginated_queries
caching
data-model-interface
complex_data_models
......
Query pagination
================
When retrieving many entities, you may not want to retrieve all at once, e.g.,
for performance reasons or to prevent connection timeouts, but rather in a
chunked way. For that purpose, there is the ``page_length`` parameter in the
:py:meth:`~linkahead.common.models.execute_query` function. If this is set to a
non-zero integer, the behavior of the function changes in that it returns a
Python `generator <https://docs.python.org/3/glossary.html#term-generator>`_
which can be used, e.g., in loops or in list comprehension. The generator yields
a :py:class:`~linkahead.common.models.Container` containing the next
``page_length`` many entities from the query result.
The following example illustrates this on the demo server.
.. code-block:: python
import linkahead as db
# 10 at the time of writing of this example
print(db.execute_query("FIND MusicalInstrument"))
# Retrieve in pages of length 5 and iterate over the pages
for page in db.execute_query("FIND MusicalInstrument", page_length=5):
# each page is a container
print(type(page))
# exactly page_length=5 for the first N-1 pages,
# and possibly less for the last page
print(len(page))
# the items on each page are subclasses of Entity
print(type(page[0]))
# The id of the first entity on the page is different for all pages
print(page[0].id)
# You can use this in a list comprehension to fill a container
container_paginated = db.Container().extend(
[ent for page in db.execute_query("FIND MusicalInstrument", page_length=5) for ent in page]
)
# The result is the same as in the unpaginated case, but the
# following can cause connection timeouts in case of very large
# retrievals
container_at_once = db.execute_query("FIND MusicalInstrument")
for ent1, ent2 in zip(container_paginated, container_at_once):
print(ent1.id == ent2.id) # always true
As you can see, you can iterate over a paginated query and then access the
entities on each page during the iteration.
.. note::
The ``page_length`` keyword is ignored for ``COUNT`` queries where
:py:meth:`~linkahead.common.models.execute_query` always returns the integer
result and in case of ``unique=True`` where always exactly one
:py:class:`~linkahead.common.models.Entity` is returned.
.. warning::
Be careful when combining query pagination with insert, update, or delete
operations. If your database changes while iterating over a paginated query,
the client will raise a
:py:exc:`~linkahead.exceptions.PagingConsistencyError` since the server
can't guarantee that the query results haven't changed in the meantime.
......@@ -24,7 +24,7 @@
"""LinkAhead Python bindings.
Tries to read from the inifile specified in the environment variable `PYCAOSDBINI` or
Tries to read from the inifile specified in the environment variable `PYLINKAHEADINI` or
alternatively in `~/.pylinkahead.ini` upon import. After that, the ini file `pylinkahead.ini` in
the current working directory will be read additionally, if it exists.
......@@ -42,7 +42,7 @@ from .common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER, LIST,
REFERENCE, TEXT)
# Import of the basic API classes:
from .common.models import (ACL, ALL, FIX, NONE, OBLIGATORY, RECOMMENDED,
SUGGESTED, Container, DropOffBox, Entity, File,
SUGGESTED, Container, DropOffBox, Entity, File, Parent,
Info, Message, Permissions, Property, Query,
QueryTemplate, Record, RecordType, delete,
execute_query, get_global_acl,
......
This diff is collapsed.
......@@ -165,7 +165,8 @@ Returns
-------
out: named tuple
See the standard library :func:`functools.lru_cache` for details."""
See the standard library :func:`functools.lru_cache` for details.
"""
return _cached_access.cache_info()
......
......@@ -91,7 +91,7 @@ def get_server_properties() -> dict[str, Optional[str]]:
props: dict[str, Optional[str]] = dict()
for elem in xml.getroot():
props[elem.tag] = elem.text
props[str(elem.tag)] = str(elem.text)
return props
......@@ -156,7 +156,10 @@ def generate_password(length: int):
def _retrieve_user(name: str, realm: Optional[str] = None, **kwargs):
con = get_connection()
try:
return con._http_request(method="GET", path="User/" + (realm + "/" + name if realm is not None else name), **kwargs).read()
return con._http_request(
method="GET",
path="User/" + (realm + "/" + name if realm is not None else name),
**kwargs).read()
except HTTPForbiddenError as e:
e.msg = "You are not permitted to retrieve this user."
raise
......@@ -198,7 +201,9 @@ def _update_user(name: str,
if entity is not None:
params["entity"] = str(entity)
try:
return con.put_form_data(entity_uri_segment="User/" + (realm + "/" + name if realm is not None else name), params=params, **kwargs).read()
return con.put_form_data(entity_uri_segment="User/" + (realm + "/" +
name if realm is not None else name),
params=params, **kwargs).read()
except HTTPResourceNotFoundError as e:
e.msg = "User does not exist."
raise e
......@@ -246,7 +251,9 @@ def _insert_user(name: str,
def _insert_role(name, description, **kwargs):
con = get_connection()
try:
return con.post_form_data(entity_uri_segment="Role", params={"role_name": name, "role_description": description}, **kwargs).read()
return con.post_form_data(entity_uri_segment="Role",
params={"role_name": name, "role_description": description},
**kwargs).read()
except HTTPForbiddenError as e:
e.msg = "You are not permitted to insert a new role."
raise
......@@ -259,7 +266,9 @@ def _insert_role(name, description, **kwargs):
def _update_role(name, description, **kwargs):
con = get_connection()
try:
return con.put_form_data(entity_uri_segment="Role/" + name, params={"role_description": description}, **kwargs).read()
return con.put_form_data(entity_uri_segment="Role/" + name,
params={"role_description": description},
**kwargs).read()
except HTTPForbiddenError as e:
e.msg = "You are not permitted to update this role."
raise
......@@ -301,8 +310,10 @@ def _set_roles(username, roles, realm=None, **kwargs):
body = xml2str(xml)
con = get_connection()
try:
body = con._http_request(method="PUT", path="UserRoles/" + (realm + "/" +
username if realm is not None else username), body=body, **kwargs).read()
body = con._http_request(method="PUT",
path="UserRoles/" + (realm + "/" +
username if realm is not None else username),
body=body, **kwargs).read()
except HTTPForbiddenError as e:
e.msg = "You are not permitted to set this user's roles."
raise
......@@ -369,7 +380,8 @@ Returns
body = xml2str(xml)
con = get_connection()
try:
return con._http_request(method="PUT", path="PermissionRules/" + role, body=body, **kwargs).read()
return con._http_request(method="PUT", path="PermissionRules/" + role, body=body,
**kwargs).read()
except HTTPForbiddenError as e:
e.msg = "You are not permitted to set this role's permissions."
raise
......@@ -381,7 +393,9 @@ Returns
def _get_permissions(role, **kwargs):
con = get_connection()
try:
return PermissionRule._parse_body(con._http_request(method="GET", path="PermissionRules/" + role, **kwargs).read())
return PermissionRule._parse_body(con._http_request(method="GET",
path="PermissionRules/" + role,
**kwargs).read())
except HTTPForbiddenError as e:
e.msg = "You are not permitted to retrieve this role's permissions."
raise
......@@ -429,7 +443,8 @@ priority : bool, optional
if permission is None:
raise ValueError(f"Permission is missing in PermissionRule xml: {elem}")
priority = PermissionRule._parse_boolean(elem.get("priority"))
return PermissionRule(elem.tag, permission, priority if priority is not None else False)
return PermissionRule(str(elem.tag), permission,
priority if priority is not None else False)
@staticmethod
def _parse_body(body: str):
......
This diff is collapsed.
......@@ -20,11 +20,11 @@
# ** end header
from __future__ import annotations # Can be removed with 3.10.
import copy
from lxml import etree
import copy
from typing import TYPE_CHECKING
import sys
from lxml import etree
if TYPE_CHECKING:
from typing import Optional
......@@ -87,7 +87,8 @@ class Transition:
return self._to_state
def __repr__(self):
return f'Transition(name="{self.name}", from_state="{self.from_state}", to_state="{self.to_state}", description="{self.description}")'
return (f'Transition(name="{self.name}", from_state="{self.from_state}", '
f'to_state="{self.to_state}", description="{self.description}")')
def __eq__(self, other):
return (
......@@ -103,9 +104,9 @@ class Transition:
@staticmethod
def from_xml(xml: etree._Element) -> "Transition":
to_state = [to.get("name")
for to in xml if to.tag.lower() == "tostate"]
for to in xml if str(to.tag).lower() == "tostate"]
from_state = [
from_.get("name") for from_ in xml if from_.tag.lower() == "fromstate"
from_.get("name") for from_ in xml if str(from_.tag).lower() == "fromstate"
]
return Transition(
name=xml.get("name"),
......@@ -199,7 +200,7 @@ class State:
result._id = xml.get("id")
result._description = xml.get("description")
transitions = [
Transition.from_xml(t) for t in xml if t.tag.lower() == "transition"
Transition.from_xml(t) for t in xml if str(t.tag).lower() == "transition"
]
if transitions:
result._transitions = set(transitions)
......
......@@ -101,11 +101,14 @@ class Version():
# pylint: disable=redefined-builtin
def __init__(self, id: Optional[str] = None, date: Optional[str] = None,
username: Optional[str] = None, realm: Optional[str] = None,
predecessors: Optional[List[Version]] = None, successors: Optional[List[Version]] = None,
predecessors: Optional[List[Version]] = None,
successors: Optional[List[Version]] = None,
is_head: Union[bool, str, None] = False,
is_complete_history: Union[bool, str, None] = False):
"""Typically the `predecessors` or `successors` should not "link back" to an existing Version
object."""
"""Typically the `predecessors` or `successors` should not "link back" to an existing
Version object.
"""
self.id = id
self.date = date
self.username = username
......@@ -205,8 +208,8 @@ object."""
version : Version
a new version instance
"""
predecessors = [Version.from_xml(p) for p in xml if p.tag.lower() == "predecessor"]
successors = [Version.from_xml(s) for s in xml if s.tag.lower() == "successor"]
predecessors = [Version.from_xml(p) for p in xml if str(p.tag).lower() == "predecessor"]
successors = [Version.from_xml(s) for s in xml if str(s.tag).lower() == "successor"]
return Version(id=xml.get("id"), date=xml.get("date"),
is_head=xml.get("head"),
is_complete_history=xml.get("completeHistory"),
......
......@@ -102,7 +102,7 @@ def validate_yaml_schema(valobj: dict[str, dict[str, Union[int, str, bool]]]):
def _read_config_files() -> list[str]:
"""Read config files from different paths.
Read the config from either ``$PYCAOSDBINI`` or home directory (``~/.pylinkahead.ini``), and
Read the config from either ``$PYLINKAHEADINI`` or home directory (``~/.pylinkahead.ini``), and
additionally adds config from a config file in the current working directory
(``pylinkahead.ini``).
If deprecated names are used (starting with 'pycaosdb'), those used in addition but the files
......@@ -131,15 +131,18 @@ def _read_config_files() -> list[str]:
warnings.warn("\n\nYou have a config file with the old naming scheme (pycaosdb.ini). "
f"Please use the new version and rename\n"
f" {ini_cwd_caosdb}\nto\n {ini_cwd}", DeprecationWarning)
if "PYCAOSDBINI" in environ:
warnings.warn("\n\nYou have an environment variable PYCAOSDBINI. "
"Please rename it to PYLINKAHEADINI.")
# End: LinkAhead rename block ##################################################
if "PYCAOSDBINI" in environ:
if not isfile(expanduser(environ["PYCAOSDBINI"])):
if "PYLINKAHEADINI" in environ:
if not isfile(expanduser(environ["PYLINKAHEADINI"])):
raise RuntimeError(
f"No configuration file found at\n{expanduser(environ['PYCAOSDBINI'])}"
"\nwhich was given via the environment variable PYCAOSDBINI"
f"No configuration file found at\n{expanduser(environ['PYLINKAHEADINI'])}"
"\nwhich was given via the environment variable PYLINKAHEADINI"
)
return_var.extend(configure(expanduser(environ["PYCAOSDBINI"])))
return_var.extend(configure(expanduser(environ["PYLINKAHEADINI"])))
else:
if isfile(ini_user_caosdb):
return_var.extend(configure(ini_user_caosdb))
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment