From 47d2a744c0d1adfdd18efa059720b2658fad12e8 Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Fri, 12 Mar 2021 14:57:36 -0500 Subject: [PATCH 1/9] use typing in pipestat.py --- docs/changelog.md | 3 + pipestat/_version.py | 2 +- pipestat/pipestat.py | 201 +++++++++++++++++++++++++------------------ 3 files changed, 122 insertions(+), 84 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index b01f3de0..3bd58c65 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format. +## [0.0.4] - unreleased + + ## [0.0.3] - 2021-03-12 ### Added - possibility to initialize the `PipestatManager` object (or use the `pipestat status` CLI) with no results schema defined for pipeline status management; [Issue #1](https://github.com/pepkit/pipestat/issues/1) diff --git a/pipestat/_version.py b/pipestat/_version.py index 27fdca49..b288a61a 100644 --- a/pipestat/_version.py +++ b/pipestat/_version.py @@ -1 +1 @@ -__version__ = "0.0.3" +__version__ = "0.0.4-dev" diff --git a/pipestat/pipestat.py b/pipestat/pipestat.py index 224c92ce..a6d1dfb6 100644 --- a/pipestat/pipestat.py +++ b/pipestat/pipestat.py @@ -1,6 +1,7 @@ from contextlib import contextmanager from copy import deepcopy from logging import getLogger +from typing import Any, Union, List, Dict, Optional import psycopg2 from attmap import PathExAttMap as PXAM @@ -48,14 +49,14 @@ class PipestatManager(dict): def __init__( self, - namespace=None, - record_identifier=None, - schema_path=None, - results_file_path=None, - database_only=False, - config=None, - status_schema_path=None, - flag_file_dir=None, + namespace: str = None, + record_identifier: str = None, + schema_path: str = None, + results_file_path: str = None, + database_only: bool = False, + config: Union[str, dict] = None, + status_schema_path: str = None, + flag_file_dir: str = None, ): """ Initialize the object @@ -77,16 +78,19 @@ def __init__( the status flags structure """ - def _check_cfg_key(cfg, key): + def _check_cfg_key(cfg: dict, key: str) -> bool: if key not in cfg: _LOGGER.warning(f"Key '{key}' not found in config") return False return True - def _mk_abs_via_cfg(path, cfg_path): + def _mk_abs_via_cfg( + path: Optional[str], + cfg_path: Optional[str], + ) -> Optional[str]: if path is None: - return - path = expandpath(path) + return path + assert isinstance(path, str), "Path is expected to be a str" if os.path.isabs(path): return path if cfg_path is None: @@ -99,7 +103,13 @@ def _mk_abs_via_cfg(path, cfg_path): return joined raise OSError(f"Could not make this path absolute: {path}") - def _select_value(arg_name, arg_value, cfg, strict=True, env_var=None): + def _select_value( + arg_name: str, + arg_value: Any, + cfg: dict, + strict: bool = True, + env_var: str = None, + ) -> Any: if arg_value is not None: return arg_value if arg_name not in cfg or cfg[arg_name] is None: @@ -128,6 +138,7 @@ def _select_value(arg_name, arg_value, cfg, strict=True, env_var=None): self._config_path = config elif isinstance(config, dict): self[CONFIG_KEY] = YacAttMap(entries=config) + self._config_path = None else: raise TypeError( "database_config has to be either path to the " @@ -160,17 +171,14 @@ def _select_value(arg_name, arg_value, cfg, strict=True, env_var=None): ) if self._schema_path is not None: _, self[SCHEMA_KEY] = read_yaml_data( - _mk_abs_via_cfg(self._schema_path, config), "schema" + _mk_abs_via_cfg(self._schema_path, self.config_path), "schema" ) self.validate_schema() # determine the highlighted results - # the conditional in the list comprehension below needs to be a - # literal "== True" so that if evaluates to False if 'highlight' - # value is just "truthy", not True self[HIGHLIGHTED_KEY] = [ k for k, v in self.schema.items() - if "highlight" in v and v["highlight"] == True + if "highlight" in v and v["highlight"] is True ] if self[HIGHLIGHTED_KEY]: assert isinstance(self[HIGHLIGHTED_KEY], list), TypeError( @@ -187,7 +195,7 @@ def _select_value(arg_name, arg_value, cfg, strict=True, env_var=None): False, env_var=ENV_VARS["status_schema"], ), - config, + self.config_path, ) or STATUS_SCHEMA ) @@ -203,7 +211,7 @@ def _select_value(arg_name, arg_value, cfg, strict=True, env_var=None): False, ENV_VARS["results_file"], ), - config, + self.config_path, ) if results_file_path: if self[DB_ONLY_KEY]: @@ -252,12 +260,14 @@ def __str__(self): res += f"\nHighlighted results: {', '.join(self.highlighted_results)}" return res - def _get_flag_file(self, record_identifier=None): + def _get_flag_file( + self, record_identifier: str = None + ) -> Union[str, List[str], None]: """ Get path to the status flag file for the specified record :param str record_identifier: unique record identifier - :return str: path to the status flag file + :return str | list[str] | None: path to the status flag file """ from glob import glob @@ -276,19 +286,19 @@ def _get_flag_file(self, record_identifier=None): return file_list[0] else: _LOGGER.debug("No flag files found") - return + return None @property - def highlighted_results(self): + def highlighted_results(self) -> List[str]: """ Highlighted results - :return list[str]: a collection of highlighted results + :return List[str]: a collection of highlighted results """ return self._get_attr(HIGHLIGHTED_KEY) or [] @property - def record_count(self): + def record_count(self) -> int: """ Number of records reported @@ -301,7 +311,7 @@ def record_count(self): ) @property - def namespace(self): + def namespace(self) -> str: """ Namespace the object writes the results to @@ -310,7 +320,7 @@ def namespace(self): return self._get_attr(NAME_KEY) @property - def record_identifier(self): + def record_identifier(self) -> str: """ Unique identifier of the record @@ -319,7 +329,7 @@ def record_identifier(self): return self._get_attr(RECORD_ID_KEY) @property - def schema(self): + def schema(self) -> Dict: """ Schema mapping @@ -328,7 +338,7 @@ def schema(self): return self._get_attr(SCHEMA_KEY) @property - def status_schema(self): + def status_schema(self) -> Dict: """ Status schema mapping @@ -337,7 +347,7 @@ def status_schema(self): return self._get_attr(STATUS_SCHEMA_KEY) @property - def status_schema_source(self): + def status_schema_source(self) -> Dict: """ Status schema source @@ -347,7 +357,7 @@ def status_schema_source(self): return self._get_attr(STATUS_SCHEMA_SOURCE_KEY) @property - def schema_path(self): + def schema_path(self) -> str: """ Schema path @@ -356,7 +366,7 @@ def schema_path(self): return self._schema_path @property - def config_path(self): + def config_path(self) -> str: """ Config path. None if the config was not provided or if provided as a mapping of the config contents @@ -366,7 +376,7 @@ def config_path(self): return getattr(self, "_config_path", None) @property - def result_schemas(self): + def result_schemas(self) -> Dict: """ Result schema mappings @@ -376,7 +386,7 @@ def result_schemas(self): return self._get_attr(RES_SCHEMAS_KEY) @property - def file(self): + def file(self) -> str: """ File path that the object is reporting the results into @@ -385,7 +395,7 @@ def file(self): return self._get_attr(FILE_KEY) @property - def data(self): + def data(self) -> YacAttMap: """ Data object @@ -393,8 +403,8 @@ def data(self): """ return self._get_attr(DATA_KEY) - @property @contextmanager + @property def db_cursor(self): """ Establish connection and get a PostgreSQL database cursor, @@ -414,7 +424,7 @@ def db_cursor(self): finally: self.close_postgres_connection() - def get_status(self, record_identifier=None): + def get_status(self, record_identifier: str = None) -> Optional[str]: """ Get the current pipeline status @@ -434,6 +444,9 @@ def get_status(self, record_identifier=None): else: flag_file = self._get_flag_file(record_identifier=r_id) if flag_file is not None: + assert isinstance( + flag_file, str + ), "Flag file path is expected to be a str, were multiple flags found?" with open(flag_file, "r") as f: status = f.read() return status @@ -441,9 +454,9 @@ def get_status(self, record_identifier=None): f"Could not determine status for '{r_id}' record. " f"No flags found in: {self[STATUS_FILE_DIR]}" ) - return + return None - def _get_attr(self, attr): + def _get_attr(self, attr: str) -> Any: """ Safely get the name of the selected attribute of this object @@ -452,11 +465,9 @@ def _get_attr(self, attr): """ return self[attr] if attr in self else None - def _table_to_dict(self): + def _table_to_dict(self) -> None: """ Create a dictionary from the database table data - - :return dict: database table data in a dict form """ with self.db_cursor as cur: cur.execute(f"SELECT * FROM {self.namespace}") @@ -470,7 +481,7 @@ def _table_to_dict(self): record_identifier=record_id, values={res_id: val} ) - def _init_postgres_table(self): + def _init_postgres_table(self) -> bool: """ Initialize a PostgreSQL table based on the provided schema, if it does not exist. Read the data stored in the database into the @@ -511,12 +522,12 @@ def _init_status_table(self): ) self._create_table(status_table_name, STATUS_TABLE_COLUMNS) - def _create_table(self, table_name, columns): + def _create_table(self, table_name: str, columns: List[str]): """ Create a table :param str table_name: name of the table to create - :param str | list[str] columns: columns definition list, + :param str | List[str] columns: columns definition list, for instance: ['name VARCHAR(50) NOT NULL'] """ columns = mk_list_of_str(columns) @@ -524,7 +535,7 @@ def _create_table(self, table_name, columns): s = sql.SQL(f"CREATE TABLE {table_name} ({','.join(columns)})") cur.execute(s) - def _init_results_file(self): + def _init_results_file(self) -> bool: """ Initialize YAML results file if it does not exist. Read the data stored in the existing file into the memory otherwise. @@ -549,7 +560,7 @@ def _init_results_file(self): self[DATA_KEY] = data return False - def _check_table_exists(self, table_name): + def _check_table_exists(self, table_name: str) -> bool: """ Check if the specified table exists @@ -564,12 +575,15 @@ def _check_table_exists(self, table_name): ) return cur.fetchone()[0] - def _check_record(self, condition_col, condition_val, table_name): + def _check_record( + self, condition_col: str, condition_val: str, table_name: str + ) -> bool: """ Check if the record matching the condition is in the table :param str condition_col: column to base the check on :param str condition_val: value in the selected column + :param str table_name: name of the table ot check the record in :return bool: whether any record matches the provided condition """ with self.db_cursor as cur: @@ -580,7 +594,7 @@ def _check_record(self, condition_col, condition_val, table_name): cur.execute(statement, (condition_val,)) return cur.fetchone()[0] - def _count_rows(self, table_name): + def _count_rows(self, table_name: str) -> int: """ Count rows in a selected table @@ -594,7 +608,9 @@ def _count_rows(self, table_name): cur.execute(statement) return cur.fetchall()[0][0] - def _report_postgres(self, value, record_identifier, table_name=None): + def _report_postgres( + self, value: Dict[str, Any], record_identifier: str, table_name: str = None + ) -> int: """ Check if record with this record identifier in table, create new record if not (INSERT), update the record if yes (UPDATE). @@ -638,14 +654,16 @@ def _report_postgres(self, value, record_identifier, table_name=None): cur.execute(query, values) return cur.fetchone()[0] - def clear_status(self, record_identifier=None, flag_names=None): + def clear_status( + self, record_identifier: str = None, flag_names: List[str] = None + ) -> List[str]: """ Remove status flags :param str record_identifier: name of the record to remove flags for :param Iterable[str] flag_names: Names of flags to remove, optional; if unspecified, all schema-defined flag names will be used. - :return list[str]: Collection of names of flags removed + :return List[str]: Collection of names of flags removed """ r_id = self._strict_record_id(record_identifier) if self.file is not None: @@ -683,7 +701,9 @@ def clear_status(self, record_identifier=None, flag_names=None): else: return [removed] - def get_status_flag_path(self, status_identifier, record_identifier=None): + def get_status_flag_path( + self, status_identifier: str, record_identifier=None + ) -> str: """ Get the path to the status file flag @@ -701,7 +721,7 @@ def get_status_flag_path(self, status_identifier, record_identifier=None): self[STATUS_FILE_DIR], f"{self.namespace}_{r_id}_{status_identifier}.flag" ) - def set_status(self, status_identifier, record_identifier=None): + def set_status(self, status_identifier: str, record_identifier: str = None) -> None: """ Set pipeline run status. @@ -762,13 +782,15 @@ def check_result_exists(self, result_identifier, record_identifier=None): results=[result_identifier], rid=record_identifier ) - def _check_which_results_exist(self, results, rid=None): + def _check_which_results_exist( + self, results: List[str], rid: str = None + ) -> List[str]: """ Check which results have been reported :param str rid: unique identifier of the record - :param list[str] results: names of the results to check - :return bool: whether the specified result has been reported for the + :param List[str] results: names of the results to check + :return List[str]: whether the specified result has been reported for the indicated record in current namespace """ rid = self._strict_record_id(rid) @@ -796,7 +818,7 @@ def _check_which_results_exist(self, results, rid=None): existing.append(r) return existing - def check_record_exists(self, record_identifier=None): + def check_record_exists(self, record_identifier: str = None) -> bool: """ Check if the record exists @@ -821,16 +843,16 @@ def check_record_exists(self, record_identifier=None): def report( self, - values, - record_identifier=None, - force_overwrite=False, - strict_type=True, - return_id=False, - ): + values: Dict[str, Any], + record_identifier: str = None, + force_overwrite: bool = False, + strict_type: bool = True, + return_id: bool = False, + ) -> Union[bool, int]: """ Report a result. - :param dict[str, any] values: dictionary of result-value pairs + :param Dict[str, any] values: dictionary of result-value pairs :param str record_identifier: unique identifier of the record, value in 'record_identifier' column to look for to determine if the record already exists @@ -849,6 +871,7 @@ def report( "There is no way to return the updated object ID while using " "results file as the object backend" ) + updated_ids = False if self.schema is None: raise SchemaNotFoundError("report results") result_identifiers = list(values.keys()) @@ -897,7 +920,9 @@ def report( ) return True if not return_id else updated_ids - def _report_data_element(self, record_identifier, values): + def _report_data_element( + self, record_identifier: str, values: Dict[str, Any] + ) -> None: """ Update the value of a result in a current namespace. @@ -905,7 +930,8 @@ def _report_data_element(self, record_identifier, values): hierarchical mapping structure if needed. :param str record_identifier: unique identifier of the record - :param any values: dict of results identifiers and values to be reported + :param Dict[str,Any] values: dict of results identifiers and values + to be reported """ self[DATA_KEY].setdefault(self.namespace, PXAM()) self[DATA_KEY][self.namespace].setdefault(record_identifier, PXAM()) @@ -913,13 +939,18 @@ def _report_data_element(self, record_identifier, values): self[DATA_KEY][self.namespace][record_identifier][res_id] = val def select( - self, columns=None, condition=None, condition_val=None, offset=None, limit=None - ): + self, + columns: Union[str, List[str]] = None, + condition: str = None, + condition_val: str = None, + offset: int = None, + limit: int = None, + ) -> List[psycopg2.extras.DictRow]: """ Get all the contents from the selected table, possibly restricted by the provided condition. - :param str | list[str] columns: columns to select + :param str | List[str] columns: columns to select :param str condition: condition to restrict the results with, will be appended to the end of the SELECT statement and safely populated with 'condition_val', @@ -928,7 +959,7 @@ def select( in 'condition' with :param int offset: number of records to be skipped :param int limit: max number of records to be returned - :return list[psycopg2.extras.DictRow]: all table contents + :return List[psycopg2.extras.DictRow]: all table contents """ if self.file: raise NotImplementedError( @@ -954,7 +985,9 @@ def select( result = cur.fetchall() return result - def retrieve(self, record_identifier=None, result_identifier=None): + def retrieve( + self, record_identifier: str = None, result_identifier: str = None + ) -> Union[Any, Dict[str, Any]]: """ Retrieve a result for a record. @@ -963,7 +996,7 @@ def retrieve(self, record_identifier=None, result_identifier=None): :param str record_identifier: unique identifier of the record :param str result_identifier: name of the result to be retrieved - :return any | dict[str, any]: a single result or a mapping with all the + :return any | Dict[str, any]: a single result or a mapping with all the results reported for the record """ record_identifier = self._strict_record_id(record_identifier) @@ -1001,7 +1034,9 @@ def retrieve(self, record_identifier=None, result_identifier=None): ) return self.data[self.namespace][record_identifier][result_identifier] - def remove(self, record_identifier=None, result_identifier=None): + def remove( + self, record_identifier: str = None, result_identifier: str = None + ) -> bool: """ Remove a result. @@ -1084,14 +1119,14 @@ def remove(self, record_identifier=None, result_identifier=None): raise return True - def validate_schema(self): + def validate_schema(self) -> None: """ Check schema for any possible issues :raises SchemaError: if any schema format issue is detected """ - def _recursively_replace_custom_types(s): + def _recursively_replace_custom_types(s: dict) -> Dict: """ Replace the custom types in pipestat schema with canonical types @@ -1132,11 +1167,11 @@ def _recursively_replace_custom_types(s): schema = _recursively_replace_custom_types(schema) self[RES_SCHEMAS_KEY] = schema - def assert_results_defined(self, results): + def assert_results_defined(self, results: List[str]) -> None: """ Assert provided list of results is defined in the schema - :param list[str] results: list of results to + :param List[str] results: list of results to check for existence in the schema :raises SchemaError: if any of the results is not defined in the schema """ @@ -1147,7 +1182,7 @@ def assert_results_defined(self, results): f"schema are: {list(known_results)}." ) - def check_connection(self): + def check_connection(self) -> bool: """ Check whether a PostgreSQL connection has been established @@ -1163,7 +1198,7 @@ def check_connection(self): return True return False - def establish_postgres_connection(self, suppress=False): + def establish_postgres_connection(self, suppress: bool = False) -> bool: """ Establish PostgreSQL connection using the config data @@ -1199,7 +1234,7 @@ def establish_postgres_connection(self, suppress=False): ) return True - def close_postgres_connection(self): + def close_postgres_connection(self) -> None: """ Close connection and remove client bound """ @@ -1215,7 +1250,7 @@ def close_postgres_connection(self): f"{self[CONFIG_KEY][CFG_DATABASE_KEY][CFG_HOST_KEY]}" ) - def _strict_record_id(self, forced_value=None): + def _strict_record_id(self, forced_value: str = None) -> str: """ Get record identifier from the outer source or stored with this object From 0ee23f1f547c8d984b12b594d68312f9db66939a Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Fri, 12 Mar 2021 15:01:02 -0500 Subject: [PATCH 2/9] switch decorators --- pipestat/pipestat.py | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pipestat/pipestat.py b/pipestat/pipestat.py index a6d1dfb6..ca83ebb4 100644 --- a/pipestat/pipestat.py +++ b/pipestat/pipestat.py @@ -403,8 +403,8 @@ def data(self) -> YacAttMap: """ return self._get_attr(DATA_KEY) - @contextmanager @property + @contextmanager def db_cursor(self): """ Establish connection and get a PostgreSQL database cursor, diff --git a/setup.py b/setup.py index d7e92fa8..5b437d28 100644 --- a/setup.py +++ b/setup.py @@ -68,6 +68,7 @@ def get_static(name, condition=None): "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Bio-Informatics", ], keywords="project, metadata, bioinformatics, sequencing, ngs, workflow", From ebc7dafb9cdb07f74138096ae472d7805945e9b9 Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Fri, 12 Mar 2021 15:03:46 -0500 Subject: [PATCH 3/9] change exception type --- pipestat/pipestat.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pipestat/pipestat.py b/pipestat/pipestat.py index ca83ebb4..497d1a10 100644 --- a/pipestat/pipestat.py +++ b/pipestat/pipestat.py @@ -90,7 +90,7 @@ def _mk_abs_via_cfg( ) -> Optional[str]: if path is None: return path - assert isinstance(path, str), "Path is expected to be a str" + assert isinstance(path, str), TypeError("Path is expected to be a str") if os.path.isabs(path): return path if cfg_path is None: @@ -444,9 +444,9 @@ def get_status(self, record_identifier: str = None) -> Optional[str]: else: flag_file = self._get_flag_file(record_identifier=r_id) if flag_file is not None: - assert isinstance( - flag_file, str - ), "Flag file path is expected to be a str, were multiple flags found?" + assert isinstance(flag_file, str), TypeError( + "Flag file path is expected to be a str, were multiple flags found?" + ) with open(flag_file, "r") as f: status = f.read() return status From 6431240e72b5e21a1f6f687d681095e5d5c738ca Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Fri, 12 Mar 2021 15:10:33 -0500 Subject: [PATCH 4/9] expect assertionerror --- tests/test_pipestat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pipestat.py b/tests/test_pipestat.py index 2a2907f5..b8eef8df 100644 --- a/tests/test_pipestat.py +++ b/tests/test_pipestat.py @@ -186,7 +186,7 @@ def test_use_other_namespace_file(self, schema_file_path): @pytest.mark.parametrize("pth", [["/$HOME/path.yaml"], 1]) def test_wrong_class_results_file(self, schema_file_path, pth): """ Input string that is not a file path raises an informative error """ - with pytest.raises(TypeError): + with pytest.raises((TypeError, AssertionError)): PipestatManager( namespace="test", results_file_path=pth, schema_path=schema_file_path ) From e33539d88bd44269410d757d9009d96b7be1ff4c Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Mon, 15 Mar 2021 10:00:41 -0400 Subject: [PATCH 5/9] add pre-commit cfg --- .pre-commit-config.yaml | 21 +++++++++++++++++++++ requirements/requirements-dev.txt | 1 + 2 files changed, 22 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..16cccb97 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: trailing-whitespace + - id: check-yaml + - id: check-json + - id: end-of-file-fixer + - id: name-tests-test + - id: requirements-txt-fixer + - id: trailing-whitespace + + - repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black + + - repo: https://github.com/PyCQA/isort + rev: 5.7.0 + hooks: + - id: isort diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt index e69de29b..416634f5 100644 --- a/requirements/requirements-dev.txt +++ b/requirements/requirements-dev.txt @@ -0,0 +1 @@ +pre-commit From 7d60f79081a92ca190f6be0653091fea704af58c Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Tue, 16 Mar 2021 15:39:34 -0400 Subject: [PATCH 6/9] account for nonexistent path, but check if writable --- .gitignore | 3 ++- pipestat/pipestat.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index a2f7f89d..fcf0f3c8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__ *.py[cod] *$py.class +__pycache__/ # C extensions *.so @@ -131,4 +132,4 @@ venv.bak/ dmypy.json # Pyre type checker -.pyre/ \ No newline at end of file +.pyre/ diff --git a/pipestat/pipestat.py b/pipestat/pipestat.py index 497d1a10..1cb27eb5 100644 --- a/pipestat/pipestat.py +++ b/pipestat/pipestat.py @@ -1,7 +1,7 @@ from contextlib import contextmanager from copy import deepcopy from logging import getLogger -from typing import Any, Union, List, Dict, Optional +from typing import Any, Dict, List, Optional, Union import psycopg2 from attmap import PathExAttMap as PXAM @@ -95,7 +95,9 @@ def _mk_abs_via_cfg( return path if cfg_path is None: rel_to_cwd = os.path.join(os.getcwd(), path) - if os.path.exists(rel_to_cwd): + if os.path.exists(rel_to_cwd) or os.access( + os.path.dirname(rel_to_cwd), os.W_OK + ): return rel_to_cwd raise OSError(f"Could not make this path absolute: {path}") joined = os.path.join(os.path.dirname(cfg_path), path) From e96ddedb0cd7219720aaf433c5330de52c53eff6 Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Tue, 16 Mar 2021 15:50:49 -0400 Subject: [PATCH 7/9] add config validation, update yacman req --- pipestat/pipestat.py | 8 ++++---- requirements/requirements-all.txt | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pipestat/pipestat.py b/pipestat/pipestat.py index 1cb27eb5..f3f75262 100644 --- a/pipestat/pipestat.py +++ b/pipestat/pipestat.py @@ -5,6 +5,7 @@ import psycopg2 from attmap import PathExAttMap as PXAM +from jsonschema import validate from psycopg2.extensions import connection from psycopg2.extras import DictCursor, Json from ubiquerg import create_lock, remove_lock @@ -147,10 +148,9 @@ def _select_value( "file to read or a dict" ) # validate config - # TODO: uncomment below when this gets released: https://github.com/pepkit/attmap/pull/75 - # cfg = self[CONFIG_KEY].to_dict(expand=True) - # _, cfg_schema = read_yaml_data(CFG_SCHEMA, "config schema") - # validate(cfg, cfg_schema) + cfg = self[CONFIG_KEY].to_dict(expand=True) + _, cfg_schema = read_yaml_data(CFG_SCHEMA, "config schema") + validate(cfg, cfg_schema) self[NAME_KEY] = _select_value( "namespace", namespace, self[CONFIG_KEY], env_var=ENV_VARS["namespace"] diff --git a/requirements/requirements-all.txt b/requirements/requirements-all.txt index 406e4ad8..e0d427df 100644 --- a/requirements/requirements-all.txt +++ b/requirements/requirements-all.txt @@ -1,7 +1,7 @@ -ubiquerg>=0.6.1 +attmap +jsonschema logmuse>=0.2.5 oyaml -yacman>=0.7.0 psycopg2-binary -attmap -jsonschema \ No newline at end of file +ubiquerg>=0.6.1 +yacman>=0.8.0 From 8dd368dc8ca8bd1f81cb7a610c066d7022624ce6 Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Fri, 2 Apr 2021 07:13:00 -0400 Subject: [PATCH 8/9] temporary black fix; https://github.com/psf/black/issues/2079 --- .github/workflows/black.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index f58e4c63..63e18519 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -8,4 +8,4 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - uses: psf/black@stable + - uses: psf/black@20.8b1 From 532fcff49a11cf61662a82d5a4ffb5bd7550f25c Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Fri, 2 Apr 2021 07:18:40 -0400 Subject: [PATCH 9/9] prep release --- docs/changelog.md | 11 +++++++---- pipestat/_version.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 3bd58c65..8419336f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,15 +1,18 @@ # Changelog -This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format. +This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format. -## [0.0.4] - unreleased +## [0.0.4] - 2021-04-02 +### Added +- config validation +- typing in code ## [0.0.3] - 2021-03-12 ### Added -- possibility to initialize the `PipestatManager` object (or use the `pipestat status` CLI) with no results schema defined for pipeline status management; [Issue #1](https://github.com/pepkit/pipestat/issues/1) +- possibility to initialize the `PipestatManager` object (or use the `pipestat status` CLI) with no results schema defined for pipeline status management; [Issue #1](https://github.com/pepkit/pipestat/issues/1) ## [0.0.2] - 2021-02-22 ### Added -- initial package release \ No newline at end of file +- initial package release diff --git a/pipestat/_version.py b/pipestat/_version.py index b288a61a..81f0fdec 100644 --- a/pipestat/_version.py +++ b/pipestat/_version.py @@ -1 +1 @@ -__version__ = "0.0.4-dev" +__version__ = "0.0.4"