diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 0f780ade..8eee5665 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -29,5 +29,6 @@ jobs: # run: | # conda install pytest python-dateutil==2.8.2 pint==0.21 - name: Test with pytest + working-directory: python/idsse_common/test run: | - pytest python/idsse_common/test --cov=python/idsse_common python/idsse_common/test --cov-report=term-missing \ No newline at end of file + pytest --cov=../idsse/common --cov-report=term \ No newline at end of file diff --git a/.gitignore b/.gitignore index 70333773..35155fe8 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ __pycache__ .venv .pytest_cache .coverage* +.python-version build/ dist/ *.egg* \ No newline at end of file diff --git a/python/idsse_common/build/lib/idsse/common/__init__.py b/python/idsse_common/build/lib/idsse/common/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/python/idsse_common/build/lib/idsse/common/aws_utils.py b/python/idsse_common/build/lib/idsse/common/aws_utils.py deleted file mode 100644 index caca0c58..00000000 --- a/python/idsse_common/build/lib/idsse/common/aws_utils.py +++ /dev/null @@ -1,213 +0,0 @@ -"""Helper function for listing directories and retrieving s3 objects""" -# ------------------------------------------------------------------------------- -# Created on Tue Feb 14 2023 -# -# Copyright (c) 2023 Regents of the University of Colorado. All rights reserved. -# -# Contributors: -# Geary J Layne -# -# ------------------------------------------------------------------------------- - -import logging -import os -from datetime import datetime, timedelta, timezone -from typing import Sequence, Tuple - -from .path_builder import PathBuilder -from .utils import TimeDelta, datetime_gen, exec_cmd - -logger = logging.getLogger(__name__) - - -class AwsUtils(): - """AWS Utility Class""" - - def __init__(self, - basedir: str, - subdir: str, - file_base: str, - file_ext: str) -> None: - self.path_builder = PathBuilder(basedir, subdir, file_base, file_ext) - - def get_path(self, issue: datetime, valid: datetime) -> str: - """Delegates to instant PathBuilder to get full path given issue and valid - - Args: - issue (datetime): Issue date time - valid (datetime): Valid date time - - Returns: - str: Absolute path to file or object - """ - lead = TimeDelta(valid-issue) - return self.path_builder.build_path(issue=issue, valid=valid, lead=lead) - - def aws_ls(self, path: str, prepend_path: bool = True) -> Sequence[str]: - """Execute an 'ls' on the AWS s3 bucket specified by path - - Args: - path (str): s3 bucket - - Returns: - Sequence[str]: The results sent to stdout from executing an 'ls' on passed path - """ - try: - commands = ["s5cmd", "--no-sign-request", "ls", path] - commands_result = exec_cmd(commands) - except FileNotFoundError: - commands = ["aws", "s3", "--no-sign-request", "ls", path] - commands_result = exec_cmd(commands) - except PermissionError: - return [] - if prepend_path: - return [os.path.join(path, filename.split(' ')[-1]) for filename in commands_result] - return [filename.split(' ')[-1] for filename in commands_result] - - def aws_cp(self, path: str, dest: str) -> bool: - """Execute an 'cp' on the AWS s3 bucket specified by path, dest - - Args: - path (str): Relative or Absolute path to the object to be copied - dest (str): The destination location - - Returns: - bool: Returns True if copy is successful - """ - try: - logger.debug('First attempt with s5cmd') - commands = ["s5cmd", "--no-sign-request", "cp", path, dest] - exec_cmd(commands) - return True - except FileNotFoundError: - try: - logger.debug('Second attempt with aws command line') - commands = ["aws", "s3", "--no-sign-request", "cp", path, dest] - exec_cmd(commands) - return True - finally: - pass - return False - - def check_for(self, issue: datetime, valid: datetime) -> Tuple[datetime, str]: - """Checks if an object passed issue/valid exists - - Args: - issue (datetime): The issue date/time used to format the path to the object's location - valid (datetime): The valid date/time used to format the path to the object's location - - Returns: - Tuple[datetime, str]: If object exists the valid date/time (indicated by object's - location) and the object's location (path) is returned as a tuple, - else the tuple(None, None) is returned - """ - lead = TimeDelta(valid-issue) - filenames = self.aws_ls(self.get_path(issue, valid), prepend_path=False) - filename = self.path_builder.build_filename(issue=issue, valid=valid, lead=lead) - for fname in filenames: - if fname.endswith(filename): - return (valid, filename) - return None - - def get_issues(self, - num_issues: int = 1, - issue_start: datetime = None, - issue_end: datetime = datetime.now(timezone.utc) - ) -> Sequence[Tuple[datetime, str]]: - """Determine the available issue date/times - - Args: - num_issues (int, optional): Maximum number of issue to return. Defaults to 1. - issue_start (datetime, optional): The oldest date/time to look for. Defaults to None. - issue_end (datetime, optional): The newest date/time to look for. Defaults to None. - - Returns: - Sequence[Tuple[datetime, str]]: A sequence of issue date/time and filepath - """ - issues_found = [] - if issue_start: - datetimes = datetime_gen(issue_end, timedelta(hours=-1), issue_start, num_issues) - else: - datetimes = datetime_gen(issue_end, timedelta(hours=-1)) - for issue_dt in datetimes: - if issue_start and issue_dt < issue_start: - break - try: - dir_path = self.path_builder.build_dir(issue=issue_dt) - issue_set = {self.path_builder.get_issue(file_path) - for file_path in self.aws_ls(dir_path) - if file_path.endswith(self.path_builder.file_ext)} - issues_found.extend(sorted(issue_set, reverse=True)) - if num_issues and len(issues_found) >= num_issues: - break - except PermissionError: - pass - return issues_found[:num_issues] - - def get_valids(self, - issue: datetime, - valid_start: datetime = None, - valid_end: datetime = None) -> Sequence[Tuple[datetime, str]]: - """Get all objects consistent with the passed issue date/time and filter by valid range - - Args: - issue (datetime): The issue date/time used to format the path to the object's location - valid_start (datetime, optional): All returned objects will be for - valids >= valid_start. Defaults to None. - valid_end (datetime, optional): All returned objects will be for valids <= valid_end. - Defaults to None. - - Returns: - Sequence[Tuple[datetime, str]]: A sequence of tuples with valid date/time (indicated by - object's location) and the object's location (path) - """ - if valid_start and valid_start == valid_end: - return [self.check_for(issue, valid_start)] - - dir_path = self.path_builder.build_dir(issue=issue) - - valid_file = [(self.path_builder.get_valid(file_path), file_path) - for file_path in self.aws_ls(dir_path) - if file_path.endswith(self.path_builder.file_ext)] - - if valid_start: - if valid_end: - valid_file = [(valid, filename) - for valid, filename in valid_file - if valid_start <= valid <= valid_end] - else: - valid_file = [(valid, filename) - for valid, filename in valid_file - if valid >= valid_start] - elif valid_end: - valid_file = [(valid, filename) - for valid, filename in valid_file - if valid <= valid_end] - - return valid_file - - -def _test(): - basedir = 's3://noaa-nbm-grib2-pds/' - subdir = 'blend.{issue.year:04d}{issue.month:02d}{issue.day:02d}/{issue.hour:02d}/core/' - file_base = 'blend.t{issue.hour:02d}z.core.f{lead.hour:03d}' - file_ext = '.co.grib2' - - issue = datetime(2023, 2, 12, 14) - aws_util = AwsUtils(basedir, subdir, file_base, file_ext) - ls_result = [valid for valid, _ in aws_util.get_valids(issue=issue)] - logging.info(ls_result) - - -if __name__ == '__main__': - import sys - - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(module)s - %(levelname)s : %(message)s', - handlers=[ - logging.StreamHandler(sys.stdout) - ] - ) - - _test() diff --git a/python/idsse_common/build/lib/idsse/common/config copy.py b/python/idsse_common/build/lib/idsse/common/config copy.py deleted file mode 100644 index ea4d40ba..00000000 --- a/python/idsse_common/build/lib/idsse/common/config copy.py +++ /dev/null @@ -1,203 +0,0 @@ -"""Utility for loading configuration data""" -# ------------------------------------------------------------------------------ -# Created on Wed Mar 01 2023 -# -# Copyright (c) 2023 Regents of the University of Colorado. All rights reserved. -# -# Contributors: -# Geary J Layne -# -# ------------------------------------------------------------------------------- - -import glob -import json -import logging -from inspect import signature -from typing import Self, Union - -logger = logging.getLogger(__name__) - - -class Config: - """Configuration data class""" - - def __init__(self, - config: Union[dict, str], - keys: Union[list, str] = None, - recursive: bool = False, - ignore_missing: bool = False) -> None: - - self._previous = None - self._next = None - - # if keys is None, indicating default key (class name) should be used - if keys is None: - keys = self.__class__.__name__ - # check if keys is empty, set to empty list, results in not looking for sub structs - elif isinstance(keys, str) and keys == '': - keys = [] - - # check is config is a string representation of a dictionary - if isinstance(config, str): - try: - config = json.loads(config) - except Exception: # pylint: disable=broad-exception-caught - logger.debug('Unsuccessful loading config as string rep of dict') - - # if config is a string use it to find filepaths - if isinstance(config, str): - filepaths = glob.glob(config, recursive=recursive) - if len(filepaths) == 0: - raise FileNotFoundError - elif len(filepaths) == 1: - self._from_filepath(filepaths[0], keys) - else: - self._from_filepaths(filepaths, keys) - # since it is not a string, assuming it is a dictionary or list of dictionaries - else: - if isinstance(config, list): - self._from_config_dicts(config, keys) - else: - self._from_config_dict(config, keys) - - # check for all expected config attributes - if not ignore_missing: - for k, v in self.__dict__.items(): - if v is None and k not in ['_next', '_previous']: - raise NameError(f'name ({k}) not found in config') - - @property - def first(self) -> Self: - """Get the first configuration""" - if self._previous is None: - return self - return self._previous.first - - @property - def next(self) -> Self: - """Move to the next configuration""" - return self._next - - @property - def previous(self) -> Self: - """Move to the previous configuration""" - return self._previous - - @property - def last(self) -> Self: - """Get the last configuration""" - if self._next is None: - return self - return self._next.last - - def _load_from_filepath(self, filepath) -> dict: - with open(filepath, 'r', encoding='utf8') as file: - return json.load(file) - - def _from_filepath(self, filepath, keys) -> Self: - config_dicts = self._load_from_filepath(filepath) - if isinstance(config_dicts, list): - self._from_config_dicts(config_dicts, keys) - else: - self._from_config_dict(config_dicts, keys) - - def _from_filepaths(self, filepaths, keys) -> Self: - config_dicts = [self._load_from_filepath(filepath) - for filepath in filepaths] - self._from_config_dicts(config_dicts, keys) - - def _from_config_dict(self, config_dict: dict, keys: str) -> Self: - if keys is not None: - if isinstance(keys, str): - config_dict = config_dict[keys] - else: - for key in keys: - config_dict = config_dict[key] - # update the instance dictionary to hold all configuration attributes - self.__dict__.update(config_dict) - - def _from_config_dicts(self, config_dicts, keys: str) -> Self: - self._from_config_dict(config_dicts[0], keys) - for config_dict in config_dicts[1:]: - # if inherited class takes only one argument - if len(signature(type(self)).parameters) == 1: - self._next = type(self)(config_dict) - else: - self._next = type(self)(config_dict, keys) - self._next._previous = self # pylint: disable=protected-access - - -def _example(): - class NameAsKeyConfig(Config): - """Testing config class the uses class name as key""" - def __init__(self, config: Union[dict, str]) -> None: - """To create a config that uses it's class name when look for nested config, - pass None to the super.__init()""" - self.idiom = None - self.metaphor = None - super().__init__(config, None) - - class WithoutKeyConfig(Config): - """Testing config class the uses no key""" - def __init__(self, config: Union[dict, str]) -> None: - """To create a config that does NOT look for config nested under a key, - pass an empty string to the super.__init()""" - self.idiom = None - self.metaphor = None - super().__init__(config, '') - - class RequiresKeyConfig(Config): - """Testing config class that requires key to be provided""" - def __init__(self, config: Union[dict, str], key: Union[list, str]) -> None: - """To create a config that requires a user provided name when look for nested config, - pass a key as string or list of string to the super.__init()""" - self.idiom = None - self.metaphor = None - super().__init__(config, key) - - def get_config_dict(key: Union[list, str]) -> dict: - idioms = ['Out of hand', - 'See eye to eye', - 'Under the weather', - 'Cut the mustard'] - metaphors = ['blanket of stars', - 'weighing on my mind', - 'were a glaring light', - 'floated down the river'] - if key is None: - return {'idiom': random.choice(idioms), - 'metaphor': random.choice(metaphors)} - if isinstance(key, str): - key = [key] - config_dict = {'idiom': random.choice(idioms), - 'metaphor': random.choice(metaphors)} - for k in reversed(key): - config_dict = {k: config_dict} - return config_dict - - # example of config that uses class name to identify relevant block of data - config_dict = get_config_dict('NameAsKeyConfig') - print(config_dict) - config = NameAsKeyConfig(config_dict) - print('Idiom:', config.idiom) - print('Metaphor:', config.metaphor) - - # example of config with block of data at top level - config_dict = get_config_dict(None) - print(config_dict) - config = WithoutKeyConfig(config_dict) - print('Idiom:', config.idiom) - print('Metaphor:', config.metaphor) - - # example of config with relevant block of data nested - config_dict = get_config_dict('NestKey') - print(config_dict) - config = RequiresKeyConfig(config_dict, 'NestKey') - print('Idiom:', config.idiom) - print('Metaphor:', config.metaphor) - - -if __name__ == '__main__': - import random - - _example() diff --git a/python/idsse_common/build/lib/idsse/common/config.py b/python/idsse_common/build/lib/idsse/common/config.py deleted file mode 100644 index 1c2de7fc..00000000 --- a/python/idsse_common/build/lib/idsse/common/config.py +++ /dev/null @@ -1,203 +0,0 @@ -"""Utility for loading configuration data""" -# ------------------------------------------------------------------------------ -# Created on Wed Mar 01 2023 -# -# Copyright (c) 2023 Regents of the University of Colorado. All rights reserved. -# -# Contributors: -# Geary J Layne -# -# ------------------------------------------------------------------------------- - -import glob -import json -import logging -from inspect import signature -from typing import Self, Union - -logger = logging.getLogger(__name__) - - -class Config: - """Configuration data class""" - - def __init__(self, - config: Union[dict, str], - keys: Union[list, str] = None, - recursive: bool = False, - ignore_missing: bool = False) -> None: - - self._previous = None - self._next = None - - # if keys is None, indicating default key (class name) should be used - if keys is None: - keys = self.__class__.__name__ - # check if keys is empty, set to empty list, results in not looking for sub structs - elif isinstance(keys, str) and keys == '': - keys = [] - - # check is config is a string representation of a dictionary - if isinstance(config, str): - try: - config = json.loads(config) - except Exception: # pylint: disable=broad-exception-caught - logger.debug('Unsuccessful loading config as string rep of dict') - - # if config is a string use it to find filepaths - if isinstance(config, str): - filepaths = glob.glob(config, recursive=recursive) - if len(filepaths) == 0: - raise FileNotFoundError - elif len(filepaths) == 1: - self._from_filepath(filepaths[0], keys) - else: - self._from_filepaths(filepaths, keys) - # since it is not a string, assuming it is a dictionary or list of dictionaries - else: - if isinstance(config, list): - self._from_config_dicts(config, keys) - else: - self._from_config_dict(config, keys) - - # check for all expected config attributes - if not ignore_missing: - for k, v in self.__dict__.items(): - if v is None and k not in ['_next', '_previous']: - raise NameError(f'name ({k}) not found in config') - - @property - def first(self) -> Self: - """Get the first configuration""" - if self._previous is None: - return self - return self._previous.first - - @property - def next(self) -> Self: - """Move to the next configuration""" - return self._next - - @property - def previous(self) -> Self: - """Move to the previous configuration""" - return self._previous - - @property - def last(self) -> Self: - """Get the last configuration""" - if self._next is None: - return self - return self._next.last - - def _load_from_filepath(self, filepath) -> dict: - with open(filepath, 'r', encoding='utf8') as file: - return json.load(file) - - def _from_filepath(self, filepath, keys) -> Self: - config_dicts = self._load_from_filepath(filepath) - if isinstance(config_dicts, list): - self._from_config_dicts(config_dicts, keys) - else: - self._from_config_dict(config_dicts, keys) - - def _from_filepaths(self, filepaths, keys) -> Self: - config_dicts = [self._load_from_filepath(filepath) - for filepath in filepaths] - self._from_config_dicts(config_dicts, keys) - - def _from_config_dict(self, config_dict: dict, keys: str) -> Self: - if keys is not None: - if isinstance(keys, str): - config_dict = config_dict[keys] - else: - for key in keys: - config_dict = config_dict[key] - # update the instance dictionary to hold all configuration attributes - self.__dict__.update(config_dict) - - def _from_config_dicts(self, config_dicts, keys: str) -> Self: - self._from_config_dict(config_dicts[0], keys) - for config_dict in config_dicts[1:]: - # if inherited class takes only one argument - if len(signature(type(self)).parameters) == 1: - self._next = type(self)(config_dict) - else: - self._next = type(self)(config_dict, keys) - self._next._previous = self # pylint: disable=protected-access - - -def _example(): - class NameAsKeyConfig(Config): - """Testing config class the uses class name as key""" - def __init__(self, config: Union[dict, str]) -> None: - """To create a config that uses it's class name when look for nested config, - pass None to the super.__init()""" - self.idiom = None - self.metaphor = None - super().__init__(config, None) - - class WithoutKeyConfig(Config): - """Testing config class the uses no key""" - def __init__(self, config: Union[dict, str]) -> None: - """To create a config that does NOT look for config nested under a key, - pass an empty string to the super.__init()""" - self.idiom = None - self.metaphor = None - super().__init__(config, '') - - class RequiresKeyConfig(Config): - """Testing config class that requires key to be provided""" - def __init__(self, config: Union[dict, str], key: Union[list, str]) -> None: - """To create a config that requires a user provided name when look for nested config, - pass a key as string or list of string to the super.__init()""" - self.idiom = None - self.metaphor = None - super().__init__(config, key) - - def get_config_dict(key: Union[list, str]) -> dict: - idioms = ['Out of hand', - 'See eye to eye', - 'Under the weather', - 'Cut the mustard'] - metaphors = ['blanket of stars', - 'weighing on my mind', - 'were a glaring light', - 'floated down the river'] - if key is None: - return {'idiom': random.choice(idioms), - 'metaphor': random.choice(metaphors)} - if isinstance(key, str): - key = [key] - config_dict = {'idiom': random.choice(idioms), - 'metaphor': random.choice(metaphors)} - for k in reversed(key): - config_dict = {k: config_dict} - return config_dict - - # example of config that uses class name to identify relevant block of data - config_dict = get_config_dict('NameAsKeyConfig') - logging.info(config_dict) - config = NameAsKeyConfig(config_dict) - logging.info('Idiom:', config.idiom) - logging.info('Metaphor:', config.metaphor) - - # example of config with block of data at top level - config_dict = get_config_dict(None) - logging.info(config_dict) - config = WithoutKeyConfig(config_dict) - logging.info('Idiom:', config.idiom) - logging.info('Metaphor:', config.metaphor) - - # example of config with relevant block of data nested - config_dict = get_config_dict('NestKey') - logging.info(config_dict) - config = RequiresKeyConfig(config_dict, 'NestKey') - logging.info('Idiom:', config.idiom) - logging.info('Metaphor:', config.metaphor) - - -if __name__ == '__main__': - import random - - _example() diff --git a/python/idsse_common/build/lib/idsse/common/correlation_id.py b/python/idsse_common/build/lib/idsse/common/correlation_id.py deleted file mode 100644 index dd56e732..00000000 --- a/python/idsse_common/build/lib/idsse/common/correlation_id.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Module for providing the tools to incorporate correlation id""" -# ------------------------------------------------------------------------------ -# Created on Thu Apr 27 2023 -# -# Copyright (c) 2023 Regents of the University of Colorado. All rights reserved. -# -# Contributors: -# Geary Layne -# -# ------------------------------------------------------------------------------ - -import logging -import uuid -from contextvars import ContextVar - - -# correlation_id: ContextVar[uuid.UUID] = \ -# ContextVar('correlation_id', -# default=uuid.UUID('00000000-0000-0000-0000-000000000000')) -correlation_id: ContextVar[str] = \ - ContextVar('correlation_id', - default=f'None:{uuid.UUID("a0000000-0000-0000-0000-000000000000")}') - - -class AddCorrelationIdFilter(logging.Filter): - """"Provides correlation id parameter for the logger""" - def filter(self, record): - record.correlation_id = correlation_id.get() - return True diff --git a/python/idsse_common/build/lib/idsse/common/json_message.py b/python/idsse_common/build/lib/idsse/common/json_message.py deleted file mode 100644 index dde457f9..00000000 --- a/python/idsse_common/build/lib/idsse/common/json_message.py +++ /dev/null @@ -1,70 +0,0 @@ -"""A module for managing the json messages used to communicate between services""" -# ------------------------------------------------------------------------------ -# Created on Tue May 09 2023 -# -# Copyright (c) 2023 Regents of the University of Colorado. All rights reserved. -# -# Contributors: -# Geary Layne -# -# ------------------------------------------------------------------------------ - -import json -from typing import Tuple, Union -from uuid import uuid4, UUID - - -def get_corr_id(message: Union[str, dict]) -> Tuple[str, UUID, str]: - """Extract the correlation id from a json message. - The correlation id is made of three part, originator, uuid, issue date/time - - Args: - message (Union[str, json]): The message to be search as either a string or json obj - - Returns: - Tuple[str, uuid, str]: A tuple containing originator, uuid, and issue date/time - """ - if isinstance(message, str): - message = json.loads(message) - - corr_id = message.get('corrId', None) - if not corr_id: - return corr_id - - corr_id = (corr_id.get('originator', None), - corr_id.get('uuid', None), - corr_id.get('issueDt', None)) - - if any(corr_id): - return corr_id - return None - - -def add_corr_id(message: Union[dict, str], - originator: str, - uuid_: Union[UUID, str] = None, - issue_dt: str = None) -> dict: - """Add (or overwrites) the three part correlation id to a json message - - Args: - message (Union[dict, str]): The message to be updated - originator (str): String representation of the originating service - uuid_ (Union[UUID, str], optional): A UUID. Defaults to None. - issue_dt (str, optional): The specific issue date/time associated with the message. - Defaults to None. - - Returns: - dict: The json message in dictionary form - """ - if isinstance(message, str): - message = json.loads(message) - - if not uuid_: - uuid_ = uuid4() - if not issue_dt: - issue_dt = '_' - message['corrId'] = {'originator': originator, - 'uuid': f'{uuid_}', - 'issueDt': issue_dt} - - return message diff --git a/python/idsse_common/build/lib/idsse/common/log_util.py b/python/idsse_common/build/lib/idsse/common/log_util.py deleted file mode 100644 index d05d59d3..00000000 --- a/python/idsse_common/build/lib/idsse/common/log_util.py +++ /dev/null @@ -1,104 +0,0 @@ -"""Module for providing the tools logging including the incorporation of correlation id""" -# ------------------------------------------------------------------------------ -# Created on Thu Apr 27 2023 -# -# Copyright (c) 2023 Regents of the University of Colorado. All rights reserved. -# -# Contributors: -# Geary Layne -# -# ------------------------------------------------------------------------------ - -import logging -import time -import uuid -from contextvars import ContextVar -from datetime import datetime -from typing import Union - -from .utils import to_iso - -# flake8: noqa E501 -# pylint: disable=line-too-long -# cSpell:words gmtime - -# correlation_id: ContextVar[uuid.UUID] = \ -# ContextVar('correlation_id', -# default=uuid.UUID('00000000-0000-0000-0000-000000000000')) -corr_id_context_var: ContextVar[str] = ContextVar('correlation_id') - - -def set_corr_id_context_var(originator: str, key: uuid = None, issue_dt: Union[str, datetime] = None) -> None: - if not key: - key = uuid.uuid4() - - if issue_dt: - if not isinstance(issue_dt, str): - issue_dt = to_iso(issue_dt) - corr_id_context_var.set(f'{originator};{key};{issue_dt}') - else: - corr_id_context_var.set(f'{originator};{key};_') - - -def get_corr_id_context_var_str(): - return corr_id_context_var.get() - -def get_corr_id_context_var_parts(): - return tuple([part for part in corr_id_context_var.get().split(';')]) - -class AddCorrelationIdFilter(logging.Filter): - """"Provides correlation id parameter for the logger""" - def filter(self, record): - record.corr_id = corr_id_context_var.get() - return True - -class CorrIdFilter(logging.Filter): - """"Provides correlation id parameter for the logger""" - def __init__(self, corr_id, name: str = "") -> None: - self._corr_id = corr_id - super().__init__(name) - - def filter(self, record): - return not hasattr(record, 'correlation_id') or self._corr_id == record.corr_id - -class UTCFormatter(logging.Formatter): - """"Provides a callable time format converter for the logging""" - converter = time.gmtime - - -def get_default_log_config(level, with_corr_id=True): - set_corr_id_context_var('None', uuid.UUID('00000000-0000-0000-0000-000000000000')) - if with_corr_id: - format_str = '%(asctime)-15s %(name)-5s %(levelname)-8s %(corr_id)s %(module)s::%(funcName)s(line %(lineno)d) %(message)s' - else: - format_str = '%(asctime)-15s %(name)-5s %(levelname)-8s %(module)s::%(funcName)s(line %(lineno)d) %(message)s' - - return { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'standard': { - '()': UTCFormatter, - 'format': format_str - }, - }, - 'filters': { - 'corr_id': { - '()': AddCorrelationIdFilter, - }, - }, - 'handlers': { - 'default': { - 'class': 'logging.StreamHandler', - 'stream': 'ext://sys.stdout', - 'formatter': 'standard', - 'filters': ['corr_id', ], - }, - }, - 'loggers': { - '': { - 'level': level, - 'handlers': ['default', ], - }, - } -} diff --git a/python/idsse_common/build/lib/idsse/common/logs.py b/python/idsse_common/build/lib/idsse/common/logs.py deleted file mode 100644 index a3a3e942..00000000 --- a/python/idsse_common/build/lib/idsse/common/logs.py +++ /dev/null @@ -1,571 +0,0 @@ -# # -*- coding: utf-8 -*- - -# haggis: a library of general purpose utilities -# -# Copyright (C) 2019 Joseph R. Fox-Rabinovitz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -# Author: Joseph Fox-Rabinovitz -# Version: 13 Apr 2019: Initial Coding -# Version: 30 Oct 2022: Added support for logging.LoggerAdapter - -# pylint: skip-file -# flake8: skip-file - -""" -Utilities for extending and configuring the logging framework. -This module is called ``logs`` instead of ``logging`` to avoid conflicts -with the builtin module. Since this module is a helper, it is expected -to be imported alongside the builtin module. -""" - -import abc -import logging -import sys -import warnings - - -__all__ = [ - 'KEEP', 'KEEP_WARN', 'OVERWRITE', 'OVERWRITE_WARN', 'RAISE', - 'add_logging_level', 'add_trace_level', 'configure_logger', - 'reset_handlers', - 'LogMaxFilter', 'MetaLoggableType', -] - -#: Default format string for the root logger. This string is set up by -#: the :py:func:`configure_logger` method. -_log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' - -#: When adding a new logging level, with :py:func:`add_logging_level`, -#: silently keep the old level in case of conflict. -KEEP = 'keep' - - -#: When adding a new logging level, with :py:func:`add_logging_level`, -#: keep the old level in case of conflict, and issue a warning. -KEEP_WARN = 'keep-warn' - - -#: When adding a new logging level, with :py:func:`add_logging_level`, -#: silently overwrite any existing level in case of conflict. -OVERWRITE = 'overwrite' - - -#: When adding a new logging level, with :py:func:`add_logging_level`, -#: overwrite any existing level in case of conflict, and issue a -#: warning. -OVERWRITE_WARN = 'overwrite-warn' - - -#: When adding a new logging level, with :py:func:`add_logging_level`, -#: raise an error in case of conflict. -RAISE = 'raise' - - -class MetaLoggableType(abc.ABCMeta): - """ - A metaclass for assigning a logger with a properly named channel to - classes. - The logger channel will be the fully qualified name of the class - including package and module prefixes. - .. py:attribute:: __namespace__ - If this attribute is found in the class definition, it will be - prefixed to the qualified name (with a dot). - .. py:attribute:: logger - This attribute is assigned to all new classes based on the name - and possibly :py:attr:`__namespace__`. - """ - - def __init__(cls, name, bases, dct): - """ - Intialize a newly created class with a :py:attr:`logger` - attribute. - If a `__namespace__` attribute is found in the class, its contents is - prefixed to the logger name. - """ - name = cls.__module__ + '.' + cls.__qualname__ - if '__namespace__' in dct: - name = dct['__namespace__'] + '.' + name - cls.logger = logging.getLogger(name) - dct['logger'] = cls.logger - return super().__init__(name, bases, dct) - - -def add_logging_level(level_name, level_num, method_name=None, - if_exists=KEEP, *, exc_info=False, stack_info=False): - """ - Comprehensively add a new logging level to the :py:mod:`logging` - module and the currently configured logging class. - The `if_exists` parameter determines the behavior if the level - name is already an attribute of the :py:mod:`logging` module or if - the method name is already present, unless the attributes are - configured to the exact values requested. Partial registration is - considered a conflict. Even a complete registration will be - overwritten if ``if_exists in (OVERWRITE, OVERWRITE_WARN)`` (without - a warning of course). - This function also accepts alternate default values for the keyword - arguments ``exc_info`` and ``stack_info`` that are optional for - every logging method. Setting alternate defaults allows levels for - which exceptions or stacks are always logged. - Parameters - ---------- - level_name : str - Becomes an attribute of the :py:mod:`logging` module with the - value ``level_num``. - level_num : int - The numerical value of the new level. - method_name : str - The name of the convenience method for both :py:mod:`logging` - itself and the class returned by - :py:func:`logging.getLoggerClass` (usually just - :py:class:`logging.Logger`). If ``method_name`` is not - specified, ``level_name.lower()`` is used instead. - if_exists : {KEEP, KEEP_WARN, OVERWRITE, OVERWRITE_WARN, RAISE} - What to do if a level with `level_name` appears to already be - registered in the :py:mod:`logging` module: - :py:const:`KEEP` - Silently keep the old level as-is. - :py:const:`KEEP_WARN` - Keep the old level around and issue a warning. - :py:const:`OVERWRITE` - Silently overwrite the old level. - :py:const:`OVERWRITE_WARN` - Overwrite the old level and issue a warning. - :py:const:`RAISE` - Raise an error. - The default is :py:const:`KEEP_WARN`. - exc_info : bool - Default value for the ``exc_info`` parameter of the new method. - stack_info : bool - Default value for the ``stack_info`` parameter of the new - method. - Examples - -------- - >>> add_logging_level('TRACE', logging.DEBUG - 5) - >>> logging.getLogger(__name__).setLevel("TRACE") - >>> logging.getLogger(__name__).trace('that worked') - >>> logging.trace('so did this') - >>> logging.TRACE - 5 - >>> add_logging_level('XTRACE', 2, exc_info=True) - >>> logging.getLogger(__name__).setLevel(logging.XTRACE) - >>> try: - >>> 1 / 0 - >>> except: - >>> # This line will log the exception - >>> logging.getLogger(__name__).xtrace('that failed') - >>> # This one will not - >>> logging.xtrace('so did this', exc_info=False) - The ``TRACE`` level can be added using :py:func:`add_trace_level`. - Note - ---- - Before adding new levels, please see the cautionary note here: - https://docs.python.org/3/howto/logging.html#custom-levels. - """ - # This method was inspired by the answers to Stack Overflow post - # http://stackoverflow.com/q/2183233/2988730, especially - # http://stackoverflow.com/a/13638084/2988730 - def for_logger_adapter(self, msg, *args, **kwargs): - self.log(level_num, msg, *args, **kwargs) - - def for_logger_class(self, msg, *args, **kwargs): - if self.isEnabledFor(level_num): - kwargs.setdefault('exc_info', exc_info) - kwargs.setdefault('stack_info', stack_info) - self._log(level_num, msg, args, **kwargs) - - def for_logging_module(*args, **kwargs): - kwargs.setdefault('exc_info', exc_info) - kwargs.setdefault('stack_info', stack_info) - logging.log(level_num, *args, **kwargs) - - if not method_name: - method_name = level_name.lower() - if method_name == level_name: - raise ValueError('Method name must differ from level name') - - # The number of items required for a full registration is 5 - items_found = 0 - # Items that are found complete but are not expected values - items_conflict = 0 - - # Lock because logger class and level name are queried and set - logging._acquireLock() - try: - registered_num = logging.getLevelName(level_name) - logger_class = logging.getLoggerClass() - logger_adapter = logging.LoggerAdapter - - if registered_num != 'Level ' + level_name: - items_found += 1 - if registered_num != level_num: - if if_exists == RAISE: - # Technically this is not an attribute issue, but for - # consistency - raise AttributeError( - 'Level {!r} already registered in logging ' - 'module'.format(level_name) - ) - items_conflict += 1 - - if hasattr(logging, level_name): - items_found += 1 - if getattr(logging, level_name) != level_num: - if if_exists == RAISE: - raise AttributeError( - 'Level {!r} already defined in logging ' - 'module'.format(level_name) - ) - items_conflict += 1 - - if hasattr(logging, method_name): - items_found += 1 - logging_method = getattr(logging, method_name) - if not callable(logging_method) or \ - getattr(logging_method, '_original_name', None) != \ - for_logging_module.__name__: - if if_exists == RAISE: - raise AttributeError( - 'Function {!r} already defined in logging ' - 'module'.format(method_name) - ) - items_conflict += 1 - - if hasattr(logger_class, method_name): - items_found += 1 - logger_method = getattr(logger_class, method_name) - if not callable(logger_method) or \ - getattr(logger_method, '_original_name', None) != \ - for_logger_class.__name__: - if if_exists == RAISE: - raise AttributeError( - 'Method {!r} already defined in logger ' - 'class'.format(method_name) - ) - items_conflict += 1 - - if hasattr(logger_adapter, method_name): - items_found += 1 - adapter_method = getattr(logger_adapter, method_name) - if not callable(adapter_method) or \ - getattr(adapter_method, '_original_name', None) != \ - for_logger_adapter.__name__: - if if_exists == RAISE: - raise AttributeError( - 'Method {!r} already defined in logger ' - 'adapter'.format(method_name) - ) - items_conflict += 1 - - if items_found > 0: - # items_found >= items_conflict always - if (items_conflict or items_found < 5) and \ - if_exists in (KEEP_WARN, OVERWRITE_WARN): - action = 'Keeping' if if_exists == KEEP_WARN else 'Overwriting' - if items_conflict: - problem = 'has conflicting definition' - items = items_conflict - else: - problem = 'is partially configured' - items = items_found - warnings.warn( - 'Logging level {!r} {} already ({}/5 items): {}'.format( - level_name, problem, items, action) - ) - - if if_exists in (KEEP, KEEP_WARN): - return - - # Make sure the method names are set to sensible values, but - # preserve the names of the old methods for future verification. - for_logging_module._original_name = for_logging_module.__name__ - for_logging_module.__name__ = method_name - for_logger_class._original_name = for_logger_class.__name__ - for_logger_class.__name__ = method_name - for_logger_adapter._original_name = for_logger_adapter.__name__ - for_logger_adapter.__name__ = method_name - - # Actually add the new level - logging.addLevelName(level_num, level_name) - setattr(logging, level_name, level_num) - setattr(logging, method_name, for_logging_module) - setattr(logger_class, method_name, for_logger_class) - setattr(logger_adapter, method_name, for_logger_adapter) - finally: - logging._releaseLock() - - -def add_trace_level(if_exists=KEEP_WARN): - """ - Add a new ``TRACE`` level to the :py:mod:`logging` module. - The numerical trace level is ``5`` lower than - :py:data:`~logging.DEBUG`. It does not log stack or exception - information by default. A ``trace`` method will be added to the - :py:mod:`logging` module and to the current default - :py:class:`~logging.Logger` class. - """ - add_logging_level('TRACE', logging.DEBUG - 5, 'trace', if_exists) - - -def LogMaxFilter(level, inclusive=True): - """ - Create a level-based filter that caps the maximum allowed log level. - Levels can be compared either exclusively or inclusively to the - threshold. - Parameters - ---------- - level : int - The cutoff level: only messages below this will be passed - through. - inclusive : bool - If True, messages at level will be cut off. Otherwise, only - messages strictly more severe than `level` will be cut off. - Return - ------ - filter : - A callable filter that operates on log records. - Notes - ----- - This function returns a callable rather than an object with a - ``filter`` method, so it is not compatible with `logging` before - Python 3.2. - """ - if not isinstance(level, int): - level = logging.getLevelName(level) - if not isinstance(level, int): - raise TypeError('Numerical level or ' - 'level that maps to number required') - comparator = level.__gt__ if inclusive else level.__ge__ - - def filter(log_record): - return comparator(log_record.levelno) - - return filter - - -def _get_formatter(format=None): - """ - Retrieve a consistent :py:class:`~logging.Formatter` object based - on the input format. - If `format` is already a :py:class:`~logging.Formatter`, return it - as-is. If :py:obj:`None`, use a default format string. Otherwise, it - is expected to be a string that initializes a proper - :py:class:`~logging.Formatter` instance. - """ - if isinstance(format, logging.Formatter): - return format - if format is None: - format = _log_format - return logging.Formatter(format) - - -def configure_logger(log_file=None, file_level='NOTSET', - log_stderr=True, stderr_level='WARNING', - log_stdout=False, stdout_level='INFO', - format_string=None, trace_warnings=True): - """ - Set up the root logger based on the input parameters. - A ``TRACE`` level is added to the :py:mod:`logging` module. The - system-level automatic exception handler is set up to log uncaught - errors. Warnings will always be captured by the logger, with - optional tracebacks being logged by default. - Parameters - ---------- - log_file : None or str - If not :py:obj:`None`, messages with level greater than or equal - to `file_level` will go to the specified file. - file_level : str - The name of the minimum logging level that will be written to - the file log if `log_file` is set. Defaults to ``'NOTSET'``. - Case insensitive. - log_stderr : bool - If :py:obj:`True`, messages with level greater than or equal to - `stderr_level` will be output to standard error. Defaults to - :py:obj:`True`. - stderr_level : str - The name of the minimum logging level that will be output to - standard error if `log_stderr` is set. Defaults to - ``'WARNING'``. - log_stdout : bool - If :py:obj:`True`, messages with level greater than or equal to - `stdout_level` will be output to standard output. Defaults to - :py:obj:`False`. If `log_stderr` is set as well, only levels - strictly less than `stderr_level` will be printed to standard - output. - stdout_level : str - The name of the minimum logging level that will be output to - standard error if `log_stdout` is set. Defaults to ``'INFO'``. - format_string : str - The log format. A missing (:py:obj:`None`) `format_string` - defaults to - ``'%(asctime)s - %(name)s - %(levelname)s - %(message)s'``. - trace_warnings : bool - Whether or not to print trace backs for actual warnings (not log - entries with a warning level) caught by the Python global - warning logger. Defaults to :py:obj:`True`. Custom - :py:meth:`~logging.Logger.warning` methods are hooked into the - logger for ``"py.warnings"``. - """ - add_trace_level() - - # System-level logging hook courtesy of Stack Overflow post - # http://stackoverflow.com/a/16993115/2988730 - def exception_handler(*args): - """ - An exception hook that is meant to replace the default - :py:func:`sys.excepthook`. Logs all uncaught exceptions to the - root logger except for :py:exc:`KeyboardInterrupt`, which is - passed directly to the default system hook. - An additional hook can be called after logging the exception - """ - if isinstance(args[0], KeyboardInterrupt): - sys.__excepthook__(*args) - else: - root.critical("Uncaught exception", exc_info=args) - - sys.excepthook = exception_handler - - if trace_warnings: - def warning_logger(self, msg, *args, **kwargs): - """ - A replacement for the :py:meth:`~logging.Logger.warning` - method that is set for the ``"py.warnings"`` logger to show - a stack trace for logged warnings. Note that warnings logged - explicitly with ``logger.log(logging.WARNING, ...)`` will - not pass through this handler. - """ - if self.isEnabledFor(logging.WARNING): - kwargs.setdefault('stack_info', True) - self._log(logging.WARNING, msg, args, **kwargs) - - logger = logging.getLogger("py.warnings") - - # Bind methods according to http://stackoverflow.com/a/1015405/2988730 - logger.warning = warning_logger.__get__(logger, type(logger)) - logger.warn = logger.warning # For good measure - - logging.captureWarnings(True) - - # Remaining logger setup courtesy of Stack Overflow post - # http://stackoverflow.com/a/24978464/2988730 - formatter = _get_formatter(format_string) - - root = logging.getLogger() - # Apparently, stdout won't show up without this... - root.setLevel(logging.NOTSET) - - if log_file: - reset_handlers(logging.FileHandler(log_file, mode='w'), - level=file_level, format=formatter, logger=root) - if log_stderr: - reset_handlers(logging.StreamHandler(sys.stderr), level=stderr_level, - format=formatter, logger=root, filter_type=True, - filter_hook=lambda h: h.stream == sys.stderr) - if log_stdout: - stdout_handler = logging.StreamHandler(sys.stdout) - reset_handlers(stdout_handler, level=stdout_level, format=formatter, - logger=root, filter_type=True, - filter_hook=lambda h: h.stream == sys.stdout) - if log_stderr: - stdout_handler.addFilter(LogMaxFilter( - logging.getLevelName(stderr_level), False)) - - -def reset_handlers(handler, level='NOTSET', format=None, logger=None, - filter_type=None, filter_hook=None, remove_hook=None): - """ - Remove all handlers of a given class from `logger` (root by - default), and replaces them with `handler`. - If a handler that is being removed has a ``close`` method, it will - be called, unless `remove_hook` is explicitly set. - If both `filter_type` and `filter_hook` are set, both conditions - must be met in order for a handler to be removed. - Parameters - ---------- - handler : logging.Handler - The new handler to place in the list. - level : str - The case insensitive name of the minimum logging level to - set for `handler`. Defaults to ``'NOTSET'``. This will not - affect the level set for the logger. - format : None or str or logging.Formatter - Format for the log output strings. - logger : None or logging.Logger - The logger to set the handler for. Defaults to the root logger. - Neither child nor ancestor loggers will be affected by this - operation. - filter_type : None, bool or type - The type of objects to remove from the current list of handlers. - If a superclass of `handler`, it will be used as the filter - instead of ``type(handler)``. Any other type will raise an error. - If :py:obj:`None`, then filtering by type will be done only if - `filter_hook` is not set. A :py:class:`bool` explicitly sets - filtering by ``type(handler)`` on and off regardless of - `filter_hook`. - filter_hook : None or callable - A function that accepts a :py:class:`~logging.Handler` and - returns a :py:class:`bool`. :py:obj:`True` indicates that an - object should be removed from the list of handlers. - remove_hook : None or callable - A function that accepts a :py:class:`~logging.Handler` and - performs some additional action such as closing it. The default - behavior is to invoke ``close()`` on all handlers that are being - removed if they have that method. - """ - if filter_type is None: - if filter_hook is None: - filter_type = type(handler) - elif filter_type is True: - filter_type = type(handler) - elif filter_type is False: - filter_type = None - else: - if not isinstance(handler, filter_type): - raise TypeError(f'{type(handler)} is not a subclass of {filter_type}') - - if filter_hook is None: - if filter_type is None: - def filter_func(h): - return True - else: - def filter_func(h): - return isinstance(h, filter_type) - else: - if filter_type is None: - filter_func = filter_hook - else: - def filter_func(h): - return isinstance(h, filter_type) and filter_hook(h) - - if remove_hook is None: - def remove_hook(h): - if hasattr(h, 'close') and callable(h.close): - h.close() - - logging._acquireLock() - try: - if logger is None: - logger = logging.getLogger() - - for h in list(logger.handlers): - if filter_func(h): - remove_hook(h) - logger.removeHandler(h) - - handler.setLevel(logging.getLevelName(level)) - handler.setFormatter(_get_formatter(format)) - logger.addHandler(handler) - finally: - logging._releaseLock() diff --git a/python/idsse_common/build/lib/idsse/common/path_builder.py b/python/idsse_common/build/lib/idsse/common/path_builder.py deleted file mode 100644 index b6e725c1..00000000 --- a/python/idsse_common/build/lib/idsse/common/path_builder.py +++ /dev/null @@ -1,368 +0,0 @@ -"""Module for building (and parsing) paths that are dependent on issue, valid, and lead""" -# ------------------------------------------------------------------------------- -# Created on Thu Feb 16 2023 -# -# Copyright (c) 2023 Regents of the University of Colorado. All rights reserved. -# -# Contributors: -# Geary J Layne -# -# ------------------------------------------------------------------------------- - -import os -import re -from datetime import datetime, timedelta -from typing import Dict, Self, Tuple, Union - -from .utils import TimeDelta - - -class PathBuilder(): - """Path Builder Class""" - - def __init__(self, - basedir: str, - subdir: str, - file_base: str, - file_ext: str) -> None: - self._basedir = basedir - self._subdir = subdir - self._file_base = file_base - self._file_ext = file_ext - - def __str__(self) -> str: - return f"'{self._basedir}','{self._subdir}','{self._file_base}','{self._file_ext}'" - - def __repr__(self) -> str: - return (f"PathBuilder(basedir='{self._basedir}', subdir='{self._subdir}', " - f"file_base='{self._file_base}', file_ext='{self._file_ext}')") - - @classmethod - def from_dir_filename(cls, dir_fmt: str, filename_fmt: str) -> Self: - """Construct a PathBuilder object from specified directory and filename formats - - Args: - dir_fmt (str): Format string for the directory - filename_fmt (str): Format string for the filename - - Returns: - Self: The newly created PathBuilder object - """ - return PathBuilder(dir_fmt, '', filename_fmt, '') - - @classmethod - def from_path(cls, path_fmt: str) -> Self: - """Construct a PathBuilder object from specified path formats - - Args: - path_fmt (str): Format string for the path, should contain both directory and filename - - Returns: - Self: The newly created PathBuilder object - """ - idx = path_fmt.rindex(os.path.sep) - return PathBuilder(path_fmt[:idx], '', path_fmt[:idx], '') - - @property - def dir_fmt(self): - """Provides access to the directory format str""" - return os.path.join(self._basedir, self._subdir) - - @property - def filename_fmt(self): - """Provides access to the filename format str""" - if not self._file_ext or self._file_ext.startswith('.'): - return f'{self._file_base}{self._file_ext}' - return f'{self._file_base}.{self._file_ext}' - - @property - def file_ext(self): - """Provides access to the file extension format str""" - if self._file_ext: - return self._file_ext - return self._file_base[self._file_base.rindex('.'):] - - @file_ext.setter - def file_ext(self, ext): - """Set the file extension format str""" - self._file_ext = ext - - @property - def path_fmt(self): - """Provides access to the path format str""" - return os.path.join(self.dir_fmt, self.filename_fmt) - - def build_dir(self, - issue: datetime = None, - valid: datetime = None, - lead: Union[timedelta, TimeDelta] = None) -> str: - """Attempts to build the directory with provided arguments - - Args: - issue (datetime, optional): Issue datetime, should be provided is the - directory is dependant on it. Defaults to None. - valid (datetime, optional): Valid datetime, should be provided is the - directory is dependant on it. . Defaults to None. - lead (Union[timedelta, TimeDelta], optional): Lead can be provided in addition - to issue or valid. Defaults to None. - - Returns: - str: Directory as a string - """ - if issue is None: - return None - lead = self._insure_lead(issue, valid, lead) - return self.dir_fmt.format(issue=issue, valid=valid, lead=lead) - - def build_filename(self, - issue: datetime = None, - valid: datetime = None, - lead: Union[timedelta, TimeDelta] = None) -> str: - """Attempts to build the filename with provided arguments - - Args: - issue (datetime, optional): Issue datetime, should be provided is the - filename is dependant on it. Defaults to None. - valid (datetime, optional): Valid datetime, should be provided is the - filename is dependant on it. . Defaults to None. - lead (Union[timedelta, TimeDelta], optional): Lead can be provided in addition - to issue or valid. Defaults to None. - - Returns: - str: File name as a string - """ - lead = self._insure_lead(issue, valid, lead) - return self.filename_fmt.format(issue=issue, valid=valid, lead=lead) - - def build_path(self, - issue: datetime = None, - valid: datetime = None, - lead: Union[timedelta, TimeDelta] = None) -> str: - """Attempts to build the path with provided arguments - - Args: - issue (datetime, optional): Issue datetime, should be provided is the - path is dependant on it. Defaults to None. - valid (datetime, optional): Valid datetime, should be provided is the - path is dependant on it. . Defaults to None. - lead (Union[timedelta, TimeDelta], optional): Lead can be provided in addition - to issue or valid. Defaults to None. - - Returns: - str: Path as a string - """ - lead = self._insure_lead(issue, valid, lead) - return self.path_fmt.format(issue=issue, valid=valid, lead=lead) - - def parse_dir(self, dir_: str) -> dict: - """Extracts issue, valid, and/or lead information from the provided directory - - Args: - dir_ (str): A directory consistent with this PathBuilder - - Returns: - dict: Lookup of all information identified and extracted - """ - return self._parse(dir_, self.dir_fmt) - - def parse_filename(self, filename: str) -> dict: - """Extracts issue, valid, and/or lead information from the provided filename - - Args: - dir_ (str): A filename consistent with this PathBuilder - - Returns: - dict: Lookup of all information identified and extracted - """ - return self._parse(filename, self.filename_fmt) - - def parse_path(self, path: str) -> dict: - """Extracts issue, valid, and/or lead information from the provided path - - Args: - dir_ (str): A path consistent with this PathBuilder - - Returns: - dict: Lookup of all information identified and extracted - """ - return self._parse(path, self.path_fmt) - - def get_issue(self, path: str) -> datetime: - """Retrieves the issue date/time from the provided path - - Args: - path (str): A path consistent with this PathBuilder - - Returns: - datetime: After parsing the path, builds and returns the issue date/time - """ - time_args = self.parse_path(path) - return self.get_issue_from_time_args(time_args) - - def get_valid(self, path: str) -> datetime: - """Retrieves the valid date/time from the provided path - - Args: - path (str): A path consistent with this PathBuilder - - Returns: - datetime: After parsing the path, builds and returns the valid date/time - """ - time_args = self.parse_path(path) - return self.get_valid_from_time_args(time_args) - - @staticmethod - def get_issue_from_time_args(parsed_args: Dict, - valid: datetime = None, - lead: timedelta = None) -> datetime: - """Static method for creating an issue date/time from parsed arguments and optional inputs - - Args: - parsed_args (dict): A dictionary of issue, valid and/or lead info resulting from parsing - a path, dir, or filename - valid (datetime, optional): Depending on info found during parsing, valid date/time - can be useful. Defaults to None. - lead (timedelta, optional): Depending on info found during parsing, lead time - can be useful. . Defaults to None. - - Returns: - datetime: Issue date/time - """ - if 'issue.year' in parsed_args: - return datetime(parsed_args.get('issue.year'), - parsed_args.get('issue.month'), - parsed_args.get('issue.day'), - parsed_args.get('issue.hour', 0), - parsed_args.get('issue.minute', 0), - parsed_args.get('issue.second', 0), - parsed_args.get('issue.microsecond', 0)) - - if lead is None: - lead = PathBuilder.get_lead_from_time_args(parsed_args) - - if valid is None and 'valid.year' in parsed_args: - valid = PathBuilder.get_valid_from_time_args(parsed_args) - - if valid and lead: - return valid - lead - - @staticmethod - def get_valid_from_time_args(parsed_args: dict, - issue: datetime = None, - lead: timedelta = None) -> datetime: - """Static method for creating an valid date/time from parsed arguments and optional inputs - - Args: - parsed_args (dict): A dictionary of issue, valid and/or lead info resulting from parsing - a path, dir, or filename - issue (datetime, optional): Depending on info found during parsing, issue date/time - can be useful. Defaults to None. - lead (timedelta, optional): Depending on info found during parsing, lead time - can be useful. . Defaults to None. - - Returns: - datetime: Valid date/time - """ - if 'valid.year' in parsed_args: - return datetime(parsed_args.get('valid.year'), - parsed_args.get('valid.month'), - parsed_args.get('valid.day'), - parsed_args.get('valid.hour', 0), - parsed_args.get('valid.minute', 0), - parsed_args.get('valid.second', 0), - parsed_args.get('valid.microsecond', 0)) - - if lead is None: - lead = PathBuilder.get_lead_from_time_args(parsed_args) - - if issue is None and 'issue.year' in parsed_args: - issue = PathBuilder.get_issue_from_time_args(parsed_args) - - if issue and lead: - return issue + lead - - return None - - @staticmethod - def get_lead_from_time_args(time_args: dict) -> timedelta: - """Static method for creating an lead time from parsed arguments and optional inputs - - Args: - parsed_args (dict): A dictionary of issue, valid and/or lead info resulting from parsing - a path, dir, or filename - issue (datetime, optional): Depending on info found during parsing, issue date/time - can be useful. Defaults to None. - valid (datetime, optional): Depending on info found during parsing, valid date/time - can be useful. Defaults to None. - - Returns: - timedelta: Lead time - """ - return timedelta(hours=time_args['lead.hour']) - - @staticmethod - def _insure_lead(issue: datetime, - valid: datetime, - lead: Union[timedelta, TimeDelta]) -> TimeDelta: - if lead: - if isinstance(lead, timedelta): - return TimeDelta(lead) - return lead - if issue and valid: - return TimeDelta(valid-issue) - return None - - def _parse(self, string: str, format_str: str) -> Dict: - def get_between(string: str, pre: str, post: str) -> Tuple[str, str]: - idx1 = string.index(pre) + len(pre) - idx2 = string.index(post, idx1) - return (string[idx1:idx2], string[idx2:]) - - def parse_args(key: str, value: str, result: Dict): - for arg in key.split('{')[1:]: - var_name, var_size = arg.split(':') - var_type = var_size[-2:-1] - var_size = int(var_size[:-2]) - match var_type: - case 'd': - result[var_name] = int(value[:var_size]) - case _: - raise ValueError(f'Unknown format type: {var_type}') - key = key[var_size:] - value = value[var_size:] - - constants = [part for part in re.split(r'{.*?}', format_str) if part] - - arg_lookup = {} - for i in range(1, len(constants)): - pre = constants[i-1] - post = constants[i] - format_between, format_str = get_between(format_str, pre, post) - string_between, string = get_between(string, pre, post) - arg_lookup[format_between] = string_between - - time_args = {} - for k, v in arg_lookup.items(): - parse_args(k, v, time_args) - - return time_args - - -def _test(): - basedir = 's3://noaa-nbm-grib2-pds/' - subdir = 'blend.{issue.year:04d}{issue.month:02d}{issue.day:02d}/{issue.hour:02d}/core/' - file_base = 'blend.t{issue.hour:02d}z.core.f{lead.hour:03d}.co' - file_ext = '.grib2.idx' - - # pb = PathBuilder(basedir=basedir, subdir=subdir, file_base=file_base, file_ext=file_ext) - pb = PathBuilder.from_dir_filename(basedir+subdir, file_base+file_ext) - - path = 's3://noaa-nbm-grib2-pds/blend.20230211/14/core/blend.t14z.core.f011.co.grib2.idx' - - logging.info(pb) - logging.info('valid', pb.get_valid(path)) - - -if __name__ == '__main__': - import logging - _test() diff --git a/python/idsse_common/build/lib/idsse/common/publish_confirm.py b/python/idsse_common/build/lib/idsse/common/publish_confirm.py deleted file mode 100644 index 27eb1a24..00000000 --- a/python/idsse_common/build/lib/idsse/common/publish_confirm.py +++ /dev/null @@ -1,345 +0,0 @@ -# -*- coding: utf-8 -*- -# pylint: disable=C0111,C0103,R0205 - -import functools -import logging -import logging.config -import json -import time - -import pika -from threading import Thread -from pika.exchange_type import ExchangeType - -from idsse.common.log_util import get_default_log_config, set_corr_id_context_var - -logger = logging.getLogger(__name__) - - -class PublishConfirm(Thread): - """This is a publisher that will handle unexpected interactions - with RabbitMQ such as channel and connection closures for any process. - If RabbitMQ closes the connection, it will reopen it. You should - look at the output, as there are limited reasons why the connection may - be closed, which usually are tied to permission related issues or - socket timeouts. - - """ - def __init__(self, url, exchange='data', queue='_data'): - """Setup the example publisher object, passing in the URL we will use - to connect to RabbitMQ. - :param str url: The URL connecting to RabbitMQ - :param str exchange: The RabbitMQ exchange on which to publish - :param str queue: The RabbitMQ default queue - """ - Thread.__init__(self, daemon=True) - - self._connection = None - self._channel = None - - self._deliveries = {} - self._acked = 0 - self._nacked = 0 - self._message_number = 0 - - self._stopping = False - self._url = url - self._exchange = exchange - self._queue = queue - - def connect(self): - """This method connects to RabbitMQ, returning the connection handle. - When the connection is established, the on_connection_open method - will be invoked by pika. - :rtype: pika.SelectConnection - """ - logger.info('Connecting to %s', self._url) - return pika.SelectConnection( - pika.URLParameters(self._url), - on_open_callback=self.on_connection_open, - on_open_error_callback=self.on_connection_open_error, - on_close_callback=self.on_connection_closed) - - def on_connection_open(self, _unused_connection): - """This method is called by pika once the connection to RabbitMQ has - been established. It passes the handle to the connection object in - case we need it, but in this case, we'll just mark it unused. - :param pika.SelectConnection _unused_connection: The connection - """ - logger.info('Connection opened') - self.open_channel() - - def on_connection_open_error(self, _unused_connection, err): - """This method is called by pika if the connection to RabbitMQ - can't be established. - :param pika.SelectConnection _unused_connection: The connection - :param Exception err: The error - """ - logger.error('Connection open failed, reopening in 5 seconds: %s', err) - self._connection.ioloop.call_later(5, self._connection.ioloop.stop) - - def on_connection_closed(self, _unused_connection, reason): - """This method is invoked by pika when the connection to RabbitMQ is - closed unexpectedly. Since it is unexpected, we will reconnect to - RabbitMQ if it disconnects. - :param pika.connection.Connection _unused_connection: The closed connection obj - :param Exception reason: exception representing reason for loss of - connection. - """ - self._channel = None - if self._stopping: - self._connection.ioloop.stop() - else: - logger.warning('Connection closed, reopening in 5 seconds: %s', - reason) - self._connection.ioloop.call_later(5, self._connection.ioloop.stop) - - def open_channel(self): - """This method will open a new channel with RabbitMQ by issuing the - Channel.Open RPC command. When RabbitMQ confirms the channel is open - by sending the Channel.OpenOK RPC reply, the on_channel_open method - will be invoked. - """ - logger.info('Creating a new channel') - self._connection.channel(on_open_callback=self.on_channel_open) - - def on_channel_open(self, channel): - """This method is invoked by pika when the channel has been opened. - The channel object is passed in so we can make use of it. - Since the channel is now open, we'll declare the exchange to use. - :param pika.channel.Channel channel: The channel object - """ - logger.info('Channel opened') - self._channel = channel - self.add_on_channel_close_callback() - self.setup_exchange(self._exchange) - - def add_on_channel_close_callback(self): - """This method tells pika to call the on_channel_closed method if - RabbitMQ unexpectedly closes the channel. - """ - logger.info('Adding channel close callback') - self._channel.add_on_close_callback(self.on_channel_closed) - - def on_channel_closed(self, channel, reason): - """Invoked by pika when RabbitMQ unexpectedly closes the channel. - Channels are usually closed if you attempt to do something that - violates the protocol, such as re-declare an exchange or queue with - different parameters. In this case, we'll close the connection - to shutdown the object. - :param pika.channel.Channel channel: The closed channel - :param Exception reason: why the channel was closed - """ - logger.warning('Channel %i was closed: %s', channel, reason) - self._channel = None - if not self._stopping: - self._connection.close() - - def setup_exchange(self, exchange_name): - """Setup the exchange on RabbitMQ by invoking the Exchange.Declare RPC - command. When it is complete, the on_exchange_declareok method will - be invoked by pika. - :param str|unicode exchange_name: The name of the exchange to declare - """ - logger.info('Declaring exchange %s', exchange_name) - # Note: using functools.partial is not required, it is demonstrating - # how arbitrary data can be passed to the callback when it is called - cb = functools.partial(self.on_exchange_declareok, - userdata=exchange_name) - self._channel.exchange_declare(exchange=exchange_name, - exchange_type=ExchangeType.topic, - callback=cb) - - def on_exchange_declareok(self, _unused_frame, userdata): - """Invoked by pika when RabbitMQ has finished the Exchange.Declare RPC - command. - :param pika.Frame.Method _unused_frame: Exchange.DeclareOk response frame - :param str|unicode userdata: Extra user data (exchange name) - """ - logger.info('Exchange declared: %s', userdata) - self.setup_queue(self._queue) - - def setup_queue(self, queue_name): - """Setup the queue on RabbitMQ by invoking the Queue.Declare RPC - command. When it is complete, the on_queue_declareok method will - be invoked by pika. - :param str|unicode queue_name: The name of the queue to declare. - """ - logger.info('Declaring queue %s', queue_name) - self._channel.queue_declare(queue=queue_name, - callback=self.on_queue_declareok) - - def on_queue_declareok(self, _unused_frame): - """Method invoked by pika when the Queue.Declare RPC call made in - setup_queue has completed. In this method we will bind the queue - and exchange together with the routing key by issuing the Queue.Bind - RPC command. When this command is complete, the on_bindok method will - be invoked by pika. - :param pika.frame.Method _unused_frame: The Queue.DeclareOk frame - """ - logger.info('Binding %s to %s with %s', self._exchange, self._queue, '#') - self._channel.queue_bind(self._queue, - self._exchange, - routing_key='#', # Default wildcard key to consume everything - callback=self.on_bindok) - - def on_bindok(self, _unused_frame): - """This method is invoked by pika when it receives the Queue.BindOk - response from RabbitMQ. Since we know we're now setup and bound, it's - time to start publishing.""" - logger.info('Queue bound') - self.start_publishing() - - def start_publishing(self): - """This method will enable delivery confirmations and schedule the - first message to be sent to RabbitMQ - """ - logger.info('Issuing consumer related RPC commands') - self.enable_delivery_confirmations() - # self.schedule_next_message() - - def enable_delivery_confirmations(self): - """Send the Confirm.Select RPC method to RabbitMQ to enable delivery - confirmations on the channel. The only way to turn this off is to close - the channel and create a new one. - When the message is confirmed from RabbitMQ, the - on_delivery_confirmation method will be invoked passing in a Basic.Ack - or Basic.Nack method from RabbitMQ that will indicate which messages it - is confirming or rejecting. - """ - logger.info('Issuing Confirm.Select RPC command') - self._channel.confirm_delivery(self.on_delivery_confirmation) - - def on_delivery_confirmation(self, method_frame): - """Invoked by pika when RabbitMQ responds to a Basic.Publish RPC - command, passing in either a Basic.Ack or Basic.Nack frame with - the delivery tag of the message that was published. The delivery tag - is an integer counter indicating the message number that was sent - on the channel via Basic.Publish. Here we're just doing housekeeping - to keep track of stats and remove message numbers that we expect - a delivery confirmation of from the list used to keep track of messages - that are pending confirmation. - :param pika.frame.Method method_frame: Basic.Ack or Basic.Nack frame - """ - confirmation_type = method_frame.method.NAME.split('.')[1].lower() - ack_multiple = method_frame.method.multiple - delivery_tag = method_frame.method.delivery_tag - - logger.debug('Received %s for delivery tag: %i (multiple: %s)', - confirmation_type, delivery_tag, ack_multiple) - - if confirmation_type == 'ack': - self._acked += 1 - elif confirmation_type == 'nack': - self._nacked += 1 - - del self._deliveries[delivery_tag] - - if ack_multiple: - for tmp_tag in list(self._deliveries.keys()): - if tmp_tag <= delivery_tag: - self._acked += 1 - del self._deliveries[tmp_tag] - """ - NOTE: at some point you would check self._deliveries for stale - entries and decide to attempt re-delivery - """ - - logger.debug( - 'Published %i messages, %i have yet to be confirmed, ' - '%i were acked and %i were nacked', self._message_number, - len(self._deliveries), self._acked, self._nacked) - - def publish_message(self, message, key=None): - """If the class is not stopping, publish a message to RabbitMQ, - appending a list of deliveries with the message number that was sent. - This list will be used to check for delivery confirmations in the - on_delivery_confirmations method. - """ - if self._channel is None or not self._channel.is_open: - return - - # We expect a JSON message format, do a check here... - try: - properties = pika.BasicProperties(content_type='application/json', - content_encoding='utf-8') - - self._channel.basic_publish(self._exchange, key, - json.dumps(message, ensure_ascii=True), - properties) - except Exception as e: - logger.error('Publish message problem : %s', str(e)) - self._message_number += 1 - self._deliveries[self._message_number] = message - logger.debug('Published message # %i', self._message_number) - - def run(self): - """Run the thread, i.e. get connection etc... - """ - logging.config.dictConfig(get_default_log_config('INFO')) - - self._connection = self.connect() - self._connection.ioloop.start() - - while not self._stopping: - time.sleep(5) - - if self._connection is not None and not self._connection.is_closed: - # Finish closing - self._connection.ioloop.start() - - def stop(self): - """Stop the example by closing the channel and connection. We - set a flag here so that we stop scheduling new messages to be - published. The IOLoop is started because this method is - invoked by the Try/Catch below when KeyboardInterrupt is caught. - Starting the IOLoop again will allow the publisher to cleanly - disconnect from RabbitMQ. - """ - logger.info('Stopping') - self._stopping = True - self.close_channel() - self.close_connection() - - def close_channel(self): - """Invoke this command to close the channel with RabbitMQ by sending - the Channel.Close RPC command. - """ - if self._channel is not None: - logger.info('Closing the channel') - self._channel.close() - - def close_connection(self): - """This method closes the connection to RabbitMQ.""" - if self._connection is not None: - logger.info('Closing connection') - self._connection.close() - - -def main(): - - # Connect to localhost:5672 as guest with the password guest and virtual host "/" (%2F) - expub = EventPublisher( - 'amqp://guest:guest@localhost:5672/%2F?connection_attempts=3&heartbeat=3600', - 'data.available', '_data.check' - ) - # Start the object thread, give it a moment... - expub.start() - time.sleep(1) - - while True: - try: - # print('Type JSON message, use Ctl-d to exit') - msg = input() - key = 'new.data.test' - expub.publish_message(msg, key) - except Exception as e: - # print('Exception in main : ', str(e)) - logger.info('Exiting from test loop : ' + str(e)) - break - expub.stop() - logger.info('Stopping...') - - -if __name__ == '__main__': - main() diff --git a/python/idsse_common/build/lib/idsse/common/utils.py b/python/idsse_common/build/lib/idsse/common/utils.py deleted file mode 100644 index 9deb79d7..00000000 --- a/python/idsse_common/build/lib/idsse/common/utils.py +++ /dev/null @@ -1,168 +0,0 @@ -"""A collection of useful classes and utility functions""" -# ------------------------------------------------------------------------------- -# Created on Wed Feb 15 2023 -# -# Copyright (c) 2023 Regents of the University of Colorado. All rights reserved. -# -# Contributors: -# Geary J Layne -# -# ------------------------------------------------------------------------------- - -import copy -import logging -from datetime import datetime, timedelta -from subprocess import Popen, PIPE, TimeoutExpired -from typing import Sequence - -logger = logging.getLogger(__name__) - - -class TimeDelta(): - """Wrapper class for datetime.timedelta to add helpful properties""" - def __init__(self, time_delta: timedelta) -> None: - self._td = time_delta - - @property - def minute(self): - """Property to get the number of minutes this instance represents""" - return int(self._td / timedelta(minutes=1)) - - @property - def hour(self): - """Property to get the number of hours this instance represents""" - return int(self._td / timedelta(hours=1)) - - @property - def day(self): - """Property to get the number of days this instance represents""" - return self._td.days - - -class Map(dict): - """Wrapper class for python dictionary with dot access""" - def __init__(self, *args, **kwargs): - super(Map, self).__init__(*args, **kwargs) - for arg in args: - if isinstance(arg, dict): - for k, v in arg.iteritems(): - self[k] = v - - if kwargs: - for k, v in kwargs.items(): - self[k] = v - - def __getattr__(self, attr): - return self.get(attr) - - def __setattr__(self, key, value): - self.__setitem__(key, value) - - def __setitem__(self, key, value): - super(Map, self).__setitem__(key, value) - self.__dict__.update({key: value}) - - def __delattr__(self, item): - self.__delitem__(item) - - def __delitem__(self, key): - super(Map, self).__delitem__(key) - del self.__dict__[key] - - -def exec_cmd(commands: Sequence[str], timeout: int = None) -> Sequence[str]: - """Execute the passed commands via a Popen call - - Args: - commands (Sequence[str]): The commands to be executed - - Raises: - RuntimeError: When execution results in an error code - - Returns: - Sequence[str]: Result of executing the commands - """ - logger.debug('Making system call %s', commands) - # with Popen(commands, stdout=PIPE, stderr=PIPE) as proc: - # out = proc.readlines() - process = Popen(commands, stdout=PIPE, stderr=PIPE) - try: - outs, errs = process.communicate(timeout=timeout) - except TimeoutExpired: - process.kill() - outs, errs = process.communicate() - if process.returncode != 0: - # the process was not successful - raise OSError(process.returncode, errs.decode()) - try: - ans = outs.decode().splitlines() - except Exception as e: # pylint: disable=broad-exception-caught - raise RuntimeError(e) from e - return ans - - -def to_iso(date_time: datetime) -> str: - """Format a datetime instance to an ISO string""" - logger.debug(f'Datetime ({datetime}) to iso') - return date_time.strftime('%Y-%m-%dT%H:%M:%SZ') - - -def to_compact(date_time: datetime) -> str: - """Format a datetime instance to an compact string""" - logger.debug(f'Datetime ({datetime}) to compact -- {__name__}') - return date_time.strftime('%Y%m%d%H%M%S') - - -def hash_code(string: str) -> int: - """Creates a hash code from provided string - - Args: - string (str): String to be hashed - - Returns: - int: hash code - """ - hash_ = 0 - for char in string: - hash_ = int((((31 * hash_ + ord(char)) ^ 0x80000000) & 0xFFFFFFFF) - 0x80000000) - return hash_ - - -def dict_copy_with(old_dict: dict, **kwargs) -> dict: - """Perform a deep copy a dictionary and adds additional key word arguments - - Args: - old_dict (dict): The old dictionary to be copied - - Returns: - dict: New dictionary - """ - new_dict = copy.deepcopy(old_dict) - for key, value in kwargs.items(): - new_dict[key] = value - return new_dict - - -def datetime_gen(dt_: datetime, - time_delta: timedelta, - end_dt: datetime = None, - max_num: int = 100) -> datetime: - """Create a date/time sequence generator, given a starting date/time and a time stride - - Args: - dt_ (datetime): Starting date/time, will be the first date/time made available - time_delta (timedelta): Time delta, can be either positive or negative - end_dt (datetime, optional): Ending date/time, will be the last. Defaults to None. - max_num (int, optional): Max number of date/times that generator will return. - Defaults to 100. - - Yields: - datetime: Next date/time in sequence - """ - if end_dt: - dt_cnt = int((end_dt-dt_)/time_delta)+1 - max_num = min(max_num, dt_cnt) if max_num else dt_cnt - - for i in range(0, max_num): - logger.debug('dt generator %d/%d', i, max_num) - yield dt_ + time_delta * i diff --git a/python/idsse_common/dist/idsse-1.0-py3.11.egg b/python/idsse_common/dist/idsse-1.0-py3.11.egg deleted file mode 100644 index b786ede4..00000000 Binary files a/python/idsse_common/dist/idsse-1.0-py3.11.egg and /dev/null differ diff --git a/python/idsse_common/dist/idsse-1.0-py3.9.egg b/python/idsse_common/dist/idsse-1.0-py3.9.egg deleted file mode 100644 index ec58dbc4..00000000 Binary files a/python/idsse_common/dist/idsse-1.0-py3.9.egg and /dev/null differ diff --git a/python/idsse_common/idsse.egg-info/PKG-INFO b/python/idsse_common/idsse.egg-info/PKG-INFO deleted file mode 100644 index e06abc7f..00000000 --- a/python/idsse_common/idsse.egg-info/PKG-INFO +++ /dev/null @@ -1,10 +0,0 @@ -Metadata-Version: 1.0 -Name: idsse -Version: 1.0 -Summary: IDSSe Common -Home-page: UNKNOWN -Author: WIDS -Author-email: @noaa.gov -License: MIT -Description: UNKNOWN -Platform: UNKNOWN diff --git a/python/idsse_common/idsse.egg-info/SOURCES.txt b/python/idsse_common/idsse.egg-info/SOURCES.txt deleted file mode 100644 index 4647dd57..00000000 --- a/python/idsse_common/idsse.egg-info/SOURCES.txt +++ /dev/null @@ -1,18 +0,0 @@ -README.md -setup.py -idsse.egg-info/PKG-INFO -idsse.egg-info/SOURCES.txt -idsse.egg-info/dependency_links.txt -idsse.egg-info/not-zip-safe -idsse.egg-info/requires.txt -idsse.egg-info/top_level.txt -idsse/common/__init__.py -idsse/common/aws_utils.py -idsse/common/config.py -idsse/common/json_message.py -idsse/common/log_util.py -idsse/common/path_builder.py -idsse/common/publish_confirm.py -idsse/common/utils.py -test/test_config.py -test/test_utils.py \ No newline at end of file diff --git a/python/idsse_common/idsse.egg-info/dependency_links.txt b/python/idsse_common/idsse.egg-info/dependency_links.txt deleted file mode 100644 index 8b137891..00000000 --- a/python/idsse_common/idsse.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/python/idsse_common/idsse.egg-info/not-zip-safe b/python/idsse_common/idsse.egg-info/not-zip-safe deleted file mode 100644 index 8b137891..00000000 --- a/python/idsse_common/idsse.egg-info/not-zip-safe +++ /dev/null @@ -1 +0,0 @@ - diff --git a/python/idsse_common/idsse.egg-info/requires.txt b/python/idsse_common/idsse.egg-info/requires.txt deleted file mode 100644 index 56ac7c0f..00000000 --- a/python/idsse_common/idsse.egg-info/requires.txt +++ /dev/null @@ -1,2 +0,0 @@ -pint -importlib_metadata diff --git a/python/idsse_common/idsse.egg-info/top_level.txt b/python/idsse_common/idsse.egg-info/top_level.txt deleted file mode 100644 index 32ddaad3..00000000 --- a/python/idsse_common/idsse.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -idsse