diff --git a/.github/workflows/sphinx-build.yml b/.github/workflows/sphinx-build.yml new file mode 100644 index 0000000..f0de21b --- /dev/null +++ b/.github/workflows/sphinx-build.yml @@ -0,0 +1,52 @@ +# Syntax reference for this file: +# https://help.github.com/en/articles/workflow-syntax-for-github-actions + +name: Sphinx Documentation Builder +on: [push, pull_request] + +# https://gist.github.com/c-bata/ed5e7b7f8015502ee5092a3e77937c99 +jobs: + build-and-delpoy: + name: Build + runs-on: ubuntu-latest + steps: + # https://github.com/marketplace/actions/checkout + - uses: actions/checkout@v2 + # https://github.com/marketplace/actions/setup-python + # ^-- This gives info on matrix testing. + - name: Install Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + # I don't know where the "run" thing is documented. + - name: Install dependencies + run: | + pip install -r docsource/sphinxrequires.txt + # Only uses the latest version of 'comtrade' available from PyPI + - name: Build Sphinx docs + if: success() + run: | + pip install comtrade + sphinx-build -M html docsource docs + + # https://github.com/marketplace/actions/github-pages + #- if: success() + # uses: crazy-max/ghaction-github-pages@master + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # with: + # target_branch: gh-pages + # build_dir: _build/html/ + + # https://github.com/peaceiris/actions-gh-pages + - name: Deploy + if: success() + uses: peaceiris/actions-gh-pages@v3 + with: + publish_branch: gh-pages + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/html/ + + +# This action probably does everything for you: +# https://github.com/marketplace/actions/sphinx-build diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml new file mode 100644 index 0000000..102c842 --- /dev/null +++ b/.github/workflows/unittest.yml @@ -0,0 +1,27 @@ +name: Unit Test + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + if [ -f test/testrequires.txt ]; then pip install -r test/testrequires.txt; fi + python3 -c "import comtrade; print('comtrade.__file__')" + - name: Test with Python unittest + run: | + python3 -m unittest tests/tests.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6769e21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/LICENSE.md b/LICENSE similarity index 100% rename from LICENSE.md rename to LICENSE diff --git a/README.md b/README.md index 039779a..593591b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,12 @@ # Python Comtrade -__Python Comtrade__ is a module for Python 3 designed to read _Common Format for Transient Data Exchange_ (COMTRADE) files. These consists of oscillography data recorded during power system outages, control systems tests, validation and tests of field equipment, protective relaying logs, etc. The COMTRADE format is defined by IEEE Standards, summarized in the table below. Some equipment vendors put additional information in proprietary versions of it. This module aims IEEE definitions but may support those proprietary versions. - +A Python 3 module designed to read _Common Format for Transient Data Exchange_ +(COMTRADE) files. These consists of oscillography data recorded during power +system outages, control systems tests, validation and tests of field equipment, +protective relaying logs, etc. The COMTRADE format is defined by IEEE Standards, +summarized in the table below. Some equipment vendors put additional information +in proprietary versions of it. This module aims IEEE definitions but may support +those proprietary versions. | Standard | Revision | |:---------------------------------------|:--------:| @@ -9,27 +14,26 @@ __Python Comtrade__ is a module for Python 3 designed to read _Common Format for | IEEE C37.111(TM)-1999 | 1999 | | IEEE C37.111(TM)-2013 / IEC 60255-24 | 2013 | - ## Installation -``` -pip install comtrade +```shell +pip install python-comtrade ``` Or just copy `comtrade.py` from this repository. - ## How to Use -The examples below shows how to open both CFG and DAT files or the new CFF file to plot (using `pyplot`) analog channel oscillography. - - +The examples below shows how to open both CFG and DAT files or the new CFF file +to plot (using `pyplot`) analog channel oscillography. ### CFG and DAT files (all revisions) -Comtrade files separated in CFG and DAT formats can also be read with `Comtrade.load`. A `CFG` file path must be passed as an argument and, optionaly, a `DAT` file path too (if the file name is not equal of the CFG file). +Comtrade files separated in CFG and DAT formats can also be read with +`Comtrade.load`. A `CFG` file path must be passed as an argument and, optionaly, +a `DAT` file path too (if the file name is not equal of the CFG file). -```py +```python import matplotlib.pyplot as plt from comtrade import Comtrade @@ -44,13 +48,13 @@ plt.legend([rec.analog_channel_ids[0], rec.analog_channel_ids[1]]) plt.show() ``` -It will read the contents of additional header (`*.hdr`) and information (`*.inf`) files. -Their contents are available through `Comtrade.hdr` and `Comtrade.inf` properties. - +It will read the contents of additional header (`*.hdr`) and information +(`*.inf`) files. Their contents are available through `Comtrade.hdr` and +`Comtrade.inf` properties. ### CFF files (2013 revision) -```py +```python import matplotlib.pyplot as plt from comtrade import Comtrade @@ -65,20 +69,29 @@ plt.legend([rec.analog_channel_ids[0], rec.analog_channel_ids[1]]) plt.show() ``` -A `Comtrade` class must be instantiated and the method `load` called with the `CFF` file path. - -`Comtrade.analog` and `Comtrade.status` lists stores analog and status channel sample lists respectively. These can be accessed through zero-based indexes, i.e., `Comtrade.analog[0]`. The list `Comtrade.time` stores each sample time in seconds. +A `Comtrade` class must be instantiated and the method `load` called with the +`CFF` file path. -More information can be accessed through `Comtrade.cfg` object, which stores data such as detailed channel information. +`Comtrade.analog` and `Comtrade.status` lists stores analog and status channel +sample lists respectively. These can be accessed through zero-based indexes, +i.e., `Comtrade.analog[0]`. The list `Comtrade.time` stores each sample time in +seconds. -Data of additional sections, such as HDR and INF, can be accessed through `hdr` and `inf` properties, respectively. +More information can be accessed through `Comtrade.cfg` object, which stores +data such as detailed channel information. +Data of additional sections, such as HDR and INF, can be accessed through `hdr` +and `inf` properties, respectively. ## Features -This module implements some of the functionality described in each of the Standard revisions. The tables below lists some features and file formats and which revision supports it. It also shows whether this module support the feature or the format. +This module implements some of the functionality described in each of the +Standard revisions. The tables below lists some features and file formats and +which revision supports it. It also shows whether this module support the +feature or the format. -Feel free to pull requests implementing one of these unsupported features or fixing bugs. +Feel free to pull requests implementing one of these unsupported features or +fixing bugs. | Formats | 1991 | 1999 | 2013 | Module Support | |:------------------------------------------------------|:----:|:-----:|:----:|:---------------:| @@ -93,7 +106,6 @@ Feel free to pull requests implementing one of these unsupported features or fix | Float32 data file format | | | x | x | | Schema for phasor data | | | x | no | - | Features | 1991 | 1999 | 2013 | Module Support | |:------------------------------------------------------|:----:|:-----:|:----:|:---------------:| | COMTRADE standard revision | | x | x | x | @@ -106,7 +118,6 @@ Feel free to pull requests implementing one of these unsupported features or fix | Multiple sample rates | x | x | x | Partial | | Nanoseconds scale | | | x | x | - ### Unsupported features * Nanoseconds time base within Python's `datetime` objects (such as `start_timestamp` and `trigger_timestamp` properties). It warns the user but doesn't use it, truncating the numbers. @@ -114,13 +125,12 @@ Feel free to pull requests implementing one of these unsupported features or fix * Null fields in ASCII data (blank columns). * Missing data fields in binary data (`0xFFFF...`) are treated as any other value. - ### Additional settings -#### Numpy arrays as data structures +#### NumPy arrays as data structures -The use of `numpy.array` as a data structure to hold time, analog and status data can be enforced -in `Comtrade` object constructor: +The use of `numpy.array` as a data structure to hold time, analog and status +data can be enforced in `Comtrade` object constructor: ```python obj = Comtrade(use_numpy_arrays=True) @@ -128,40 +138,43 @@ obj = Comtrade(use_numpy_arrays=True) It may improve performance for computations after loading data. - #### File encodings -Specify the `encoding` as a keyword argument on all load methods as you'd specify for common file loading: +Specify the `encoding` as a keyword argument on all load methods as you'd +specify for common file loading: ```python rec = Comtrade() rec.load("sample_files/sample_ascii.cff", encoding="iso-8859-1") ``` - ## Documentation https://github.com/dparrini/python-comtrade ## Support -Feel free to report any bugs you find. You are welcome to fork and submit pull requests. +Feel free to report any bugs you find. You are welcome to fork and submit pull +requests. ## Development -To run tests, use Python's `unittest`. From a clone of the GitHub repository, run the command: +To run tests, use Python's `unittest`. From a clone of the GitHub repository, +run the command: -#### On Windows -``` +### On Windows + +```shell python -m unittest tests\tests.py ``` -#### On Linux: -``` +### On Linux + +```shell python3 -m unittest ./tests/tests.py ``` ## License -The module is available at [GitHub](https://github.com/dparrini/python-comtrade) under the MIT license. - +The module is available at [GitHub](https://github.com/dparrini/python-comtrade) +under the MIT license. diff --git a/comtrade.py b/comtrade.py index ace6134..9bf7ce1 100644 --- a/comtrade.py +++ b/comtrade.py @@ -39,6 +39,7 @@ except ModuleNotFoundError: HAS_NUMPY = False +__version__ = "0.0.11" # COMTRADE standard revisions REV_1991 = "1991" @@ -184,15 +185,18 @@ def _read_timestamp(timestamp_line: str, rev_year: str, ignore_warnings: bool = def _file_is_utf8(file_path): if os.path.exists(file_path): - with open(file_path, "r") as file: - return _stream_is_utf8(file) + try: + with open(file_path, "r", encoding="utf-8") as file: + return _stream_is_utf8(file) + except UnicodeDecodeError: + return False return False def _stream_is_utf8(stream): try: - contents = stream.readlines() - except UnicodeDecodeError as exception: + _ = stream.readlines() + except UnicodeDecodeError: return True return False @@ -208,10 +212,11 @@ def __init__(self, **kwargs): Cfg object constructor. Keyword arguments: - ignore_warnings -- whether warnings are displayed in stdout + ignore_warnings -- whether warnings are displayed in stdout (default: False) """ self.filename = "" + self.filepath = None # implicit data self._time_base = self.TIME_BASE_MICROSEC @@ -249,7 +254,7 @@ def __init__(self, **kwargs): def station_name(self) -> str: """Return the recording device's station name.""" return self._station_name - + @property def rec_dev_id(self) -> str: """Return the recording device id.""" @@ -274,17 +279,17 @@ def analog_channels(self) -> list: def status_channels(self) -> list: """Return the status channels list with complete channel description.""" return self._status_channels - + @property def analog_count(self) -> int: """Return the number of analog channels.""" return self._analog_count - + @property def status_count(self) -> int: """Return the number of status channels.""" return self._status_count - + @property def time_base(self) -> float: """Return the time base.""" @@ -294,12 +299,12 @@ def time_base(self) -> float: def frequency(self) -> float: """Return the measured line frequency in Hertz.""" return self._frequency - + @property def ft(self) -> str: """Return the expected DAT file format.""" return self._ft - + @property def timemult(self) -> float: """Return the DAT time multiplier (Default = 1).""" @@ -310,34 +315,34 @@ def timestamp_critical(self) -> bool: """Returns whether the DAT file must contain non-zero timestamp values.""" return self._timestamp_critical - + @property def start_timestamp(self) -> dt.datetime: """Return the recording start time stamp as a datetime object.""" return self._start_timestamp - + @property def trigger_timestamp(self) -> dt.datetime: """Return the trigger time stamp as a datetime object.""" return self._trigger_timestamp - + @property def nrates(self) -> int: """Return the number of different sample rates within the DAT file.""" return self._nrates - + @property def sample_rates(self) -> list: """ Return a list with pairs describing the number of samples for a given sample rate. - """ + """ return self._sample_rates # Deprecated properties - Changed "Digital" for "Status" @property def digital_channels(self) -> list: - """Returns the status channels bidimensional values list.""" + """Returns the status channels bi-dimensional values list.""" if not self.ignore_warnings: warnings.warn(FutureWarning("digital_channels is deprecated, " "use status_channels instead.")) @@ -364,8 +369,11 @@ def load(self, filepath, **user_kwargs): with open(self.filepath, "r", **kwargs) as cfg: self._read_io(cfg) else: - raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), - self.filepath) + raise FileNotFoundError( + errno.ENOENT, + os.strerror(errno.ENOENT), + self.filepath + ) def read(self, cfg_lines): """Read CFG-format data of a FileIO or StringIO object.""" @@ -386,7 +394,8 @@ def _read_io(self, cfg): line = cfg.readline() # station, device, and comtrade standard revision information packed = _read_sep_values(line) - if 3 == len(packed): + pack_len = len(packed) + if 3 == pack_len: # only 1999 revision and above has the standard revision year self._station_name, self._rec_dev_id, self._rev_year = packed self._rev_year = self._rev_year.strip() @@ -395,9 +404,21 @@ def _read_io(self, cfg): if not self.ignore_warnings: msg = WARNING_UNKNOWN_REVISION.format(self._rev_year) warnings.warn(Warning(msg)) - else: + elif pack_len == 2: self._station_name, self._rec_dev_id = packed self._rev_year = REV_1991 + else: + warnings.warn( + ( + f"COMTRADE CFG file presents a heading with an {pack_len} " + "fields, more than the expected 3. This may be indication " + "of an invalid COMTRADE file format." + ), + UserWarning + ) + self._station_name = packed[0] + self._rev_year = packed[-1] + self._rec_dev_id = ",".join(packed[1:-1]) line_count = line_count + 1 # Second line @@ -460,7 +481,7 @@ def _read_io(self, cfg): self._timestamp_critical = False line_count = line_count + 1 - for inrate in range(self._nrates): + for _ in range(self._nrates): line = cfg.readline() # each sample rate samp, endsamp = _read_sep_values(line) @@ -520,7 +541,7 @@ def _read_io(self, cfg): def _get_time_base(self, using_nanoseconds: bool): """ - Return the time base, which is based on the fractionary part of the + Return the time base, which is based on the fractionary part of the seconds in a timestamp (00.XXXXX). """ if using_nanoseconds: @@ -538,7 +559,7 @@ class Comtrade: EXT_HDR = "hdr" # format specific ASCII_SEPARATOR = "," - + def __init__(self, **kwargs): """ Comtrade object constructor. @@ -557,6 +578,7 @@ def __init__(self, **kwargs): self._status_channel_ids = [] self._status_phases = [] self._timestamp_critical = False + self._total_samples = 0 # Data types if "use_numpy_arrays" in kwargs: @@ -602,7 +624,7 @@ def cfg(self) -> Cfg: def hdr(self): """Return the HDR file contents.""" return self._hdr - + @property def inf(self): """Return the INF file contents.""" @@ -612,17 +634,17 @@ def inf(self): def analog_channel_ids(self) -> list: """Returns the analog channels name list.""" return self._analog_channel_ids - + @property def analog_phases(self) -> list: """Returns the analog phase name list.""" return self._analog_phases - + @property def status_channel_ids(self) -> list: """Returns the status channels name list.""" return self._status_channel_ids - + @property def status_phases(self) -> list: """Returns the status phase name list.""" @@ -635,12 +657,12 @@ def time(self) -> list: @property def analog(self) -> list: - """Return the analog channel values bidimensional list.""" + """Return the analog channel values bi-dimensional list.""" return self._analog_values - + @property def status(self) -> list: - """Return the status channel values bidimensional list.""" + """Return the status channel values bi-dimensional list.""" return self._status_values @property @@ -702,21 +724,32 @@ def ft(self) -> str: def digital_channel_ids(self) -> list: """Returns the status channels name list.""" if not self.ignore_warnings: - warnings.warn(FutureWarning("digital_channel_ids is deprecated, use status_channel_ids instead.")) + warnings.warn( + FutureWarning( + "digital_channel_ids is deprecated, use status_channel_ids " + "instead." + ) + ) return self._status_channel_ids @property def digital(self) -> list: - """Returns the status channels bidimensional values list.""" + """Returns the status channels bi-dimensional values list.""" if not self.ignore_warnings: - warnings.warn(FutureWarning("digital is deprecated, use status instead.")) + warnings.warn( + FutureWarning("digital is deprecated, use status instead.") + ) return self._status_values @property def digital_count(self) -> int: """Returns the number of status channels.""" if not self.ignore_warnings: - warnings.warn(FutureWarning("digital_count is deprecated, use status_count instead.")) + warnings.warn( + FutureWarning( + "digital_count is deprecated, use status_count instead." + ) + ) return self._cfg.status_count def _get_dat_reader(self): @@ -734,7 +767,9 @@ def _get_dat_reader(self): dat = Float32DatReader(**dat_kwargs) else: dat = None - raise Exception("Not supported data file format: {}".format(self.ft)) + raise Exception( + f"Not supported data file format: {self.ft}" + ) return dat def read(self, cfg_lines, dat_lines_or_bytes) -> None: @@ -745,7 +780,7 @@ def read(self, cfg_lines, dat_lines_or_bytes) -> None: # channel ids self._cfg_extract_channels_ids(self._cfg) - + # channel phases self._cfg_extract_phases(self._cfg) @@ -758,7 +793,7 @@ def read(self, cfg_lines, dat_lines_or_bytes) -> None: def _cfg_extract_channels_ids(self, cfg) -> None: self._analog_channel_ids = [channel.name for channel in cfg.analog_channels] self._status_channel_ids = [channel.name for channel in cfg.status_channels] - + def _cfg_extract_phases(self, cfg) -> None: self._analog_phases = [channel.ph for channel in cfg.analog_channels] self._status_phases = [channel.ph for channel in cfg.status_channels] @@ -775,7 +810,7 @@ def load(self, cfg_file, dat_file = None, **kwargs) -> None: object. dat_file, inf_file, and hdr_file are optional (Default: None). cfg_file is the cfg file path, including its extension. - dat_file is optional, and may be set if the DAT file name differs from + dat_file is optional, and may be set if the DAT file name differs from the CFG file name. Keyword arguments: @@ -820,14 +855,16 @@ def load(self, cfg_file, dat_file = None, **kwargs) -> None: # check if the CFF file exists self._load_cff(cfg_file) else: - raise Exception(r"Expected CFG file path, got intead \"{}\".".format(cfg_file)) + raise FileNotFoundError( + r"Expected CFG file path, got instead \"{}\".".format(cfg_file) + ) def _load_cfg_dat(self, cfg_filepath, dat_filepath, **kwargs): self._cfg.load(cfg_filepath, **kwargs) # channel ids self._cfg_extract_channels_ids(self._cfg) - + # channel phases self._cfg_extract_phases(self._cfg) @@ -866,13 +903,13 @@ def _load_cff(self, cff_file_path: str, **kwargs): hdr_lines = [] inf_lines = [] # file type: CFG, HDR, INF, DAT - ftype = None + file_type = None # file format: ASCII, BINARY, BINARY32, FLOAT32 - fformat = None + file_format = None if "encoding" not in kwargs and _file_is_utf8(cff_file_path): kwargs["encoding"] = "utf-8" # Number of bytes for binary/float dat - fbytes = 0 + file_bytes = 0 with open(cff_file_path, "r", **kwargs) as file: header_re = re.compile(CFF_HEADER_REXP) last_match = None @@ -884,39 +921,42 @@ def _load_cff(self, cff_file_path: str, **kwargs): if mobj is not None: last_match = mobj groups = last_match.groups() - ftype = groups[0] + file_type = groups[0] if len(groups) > 1: - fformat = last_match.groups()[1] + file_format = last_match.groups()[1] fbytes_obj = last_match.groups()[2] - fbytes = int(fbytes_obj) if fbytes_obj is not None else 0 + if fbytes_obj is not None: + file_bytes = int(fbytes_obj) + else: + file_bytes = 0 - elif last_match is not None and ftype == "CFG": + elif last_match is not None and file_type == "CFG": cfg_lines.append(line.strip()) - elif last_match is not None and ftype == "DAT": - if fformat == TYPE_ASCII: + elif last_match is not None and file_type == "DAT": + if file_format == TYPE_ASCII: dat_lines.append(line.strip()) else: break - elif last_match is not None and ftype == "HDR": + elif last_match is not None and file_type == "HDR": hdr_lines.append(line.strip()) - elif last_match is not None and ftype == "INF": + elif last_match is not None and file_type == "INF": inf_lines.append(line.strip()) line = file.readline() - if fformat == TYPE_ASCII: + if file_format == TYPE_ASCII: # process ASCII CFF data self.read("\n".join(cfg_lines), "\n".join(dat_lines)) else: # read dat bytes total_bytes = os.path.getsize(cff_file_path) - cff_bytes_read = total_bytes - fbytes + cff_bytes_read = total_bytes - file_bytes with open(cff_file_path, "rb") as file: file.read(cff_bytes_read) - dat_bytes = file.read(fbytes) + dat_bytes = file.read(file_bytes) self.read("\n".join(cfg_lines), dat_bytes) # stores additional data @@ -935,9 +975,14 @@ def cfg_summary(self): interval_line = "From {} to {} with time mult. = {}" format_line = "{} format" - lines = [header_line.format(self.analog_count, self.status_count, - self.channels_count), - "Line frequency: {} Hz".format(self.frequency)] + lines = [ + header_line.format( + self.analog_count, + self.status_count, + self.channels_count + ), + f"Line frequency: {self.frequency} Hz" + ] for i in range(self._cfg.nrates): rate, points = self._cfg.sample_rates[i] lines.append(sample_line.format(rate, points)) @@ -975,6 +1020,7 @@ def __init__(self, n: int, name='', ph='', ccbm='', y=0): def __str__(self): fields = [str(self.n), self.name, self.ph, self.ccbm, str(self.y)] + return ','.join(fields) class AnalogChannel(Channel): @@ -1183,8 +1229,7 @@ def __init__(self, **kwargs): self.STRUCT_FORMAT_STATUS_ONLY = "II {dcount:d}H" def get_reader_format(self, analog_channels, status_bytes): - # Number of status fields of 2 bytes based on the total number of - # bytes. + # Number of status fields of 2 bytes based on the total number of bytes. dcount = math.floor(status_bytes / 2) # Check the file configuration @@ -1219,7 +1264,6 @@ def parse(self, contents): row_reader = struct.Struct(self.get_reader_format(achannels, dbytes)) # Row reading function. - next_row = None if isinstance(contents, io.TextIOBase) or \ isinstance(contents, io.BufferedIOBase): # Read all buffer contents diff --git a/docsource/_templates/base.rst b/docsource/_templates/base.rst new file mode 100644 index 0000000..278387b --- /dev/null +++ b/docsource/_templates/base.rst @@ -0,0 +1,5 @@ +{{ fullname | escape | underline}} + + + +.. auto{{ objtype }}:: {{ objname }} \ No newline at end of file diff --git a/docsource/_templates/class.rst b/docsource/_templates/class.rst new file mode 100644 index 0000000..85105fa --- /dev/null +++ b/docsource/_templates/class.rst @@ -0,0 +1,65 @@ +{% if referencefile %} +.. include:: {{ referencefile }} +{% endif %} + +{{ objname }} +{{ underline }} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :show-inheritance: + + {% if '__init__' in methods %} + {% set caught_result = methods.remove('__init__') %} + {% endif %} + + {% block attributes_summary %} + {% if attributes %} + + .. rubric:: Attributes Summary + + .. autosummary:: + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + + {% endif %} + {% endblock %} + + {% block methods_summary %} + {% if methods %} + + .. rubric:: Methods Summary + + .. autosummary:: + {% for item in methods %} + ~{{ name }}.{{ item }} + {%- endfor %} + + {% endif %} + {% endblock %} + + {% block attributes_documentation %} + {% if attributes %} + + .. rubric:: Attributes Documentation + + {% for item in attributes %} + .. autoattribute:: {{ item }} + {%- endfor %} + + {% endif %} + {% endblock %} + + {% block methods_documentation %} + {% if methods %} + + .. rubric:: Methods Documentation + + {% for item in methods %} + .. automethod:: {{ item }} + {%- endfor %} + + {% endif %} + {% endblock %} diff --git a/docsource/_templates/module.rst b/docsource/_templates/module.rst new file mode 100644 index 0000000..d952d0b --- /dev/null +++ b/docsource/_templates/module.rst @@ -0,0 +1,20 @@ +{{ fullname | escape | underline }} + +.. rubric:: Description + +.. automodule:: {{ fullname }} + +.. currentmodule:: {{ fullname }} + + + +{% if functions %} +.. rubric:: Functions + +.. autosummary:: + :toctree: . + {% for function in functions %} + {{ function }} + {% endfor %} + +{% endif %} \ No newline at end of file diff --git a/docsource/comtrade.rst b/docsource/comtrade.rst new file mode 100644 index 0000000..b7d1ffd --- /dev/null +++ b/docsource/comtrade.rst @@ -0,0 +1,19 @@ +Cfg Class Reference +=================== + +.. autoclass:: comtrade.Cfg + :members: + + +Comtrade Class Reference +======================== + +.. autoclass:: comtrade.Comtrade + :members: + + +Additional Package Functions +============================ + +.. autofunction:: comtrade.fill_with_zeros_to_the_right + diff --git a/docsource/conf.py b/docsource/conf.py new file mode 100644 index 0000000..81e3e81 --- /dev/null +++ b/docsource/conf.py @@ -0,0 +1,95 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import re +import sys + + +# Verify Import +try: + import comtrade +except ImportError: + print("Couldn't import `comtrade` module!") + sys.exit(9) + + +# -- Project information ----------------------------------------------------- + +project = 'python-comtrade' +copyright = '2018, David Rodrigues Parrini' +author = 'David Rodrigues Parrini' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + 'sphinx.ext.autosummary', + 'numpydoc', + 'sphinx_sitemap', + 'm2r2', +] +autosummary_generate = True +numpydoc_show_class_members = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'classic' +html_title = 'python-comtrade' +# html_theme_options = { + # 'rightsidebar': 'false', + # 'stickysidebar': 'false', + # 'collapsiblesidebar': 'false', + # 'externalrefs': 'false', + # 'footerbgcolor': '#08385D', + # 'footertextcolor': '#ffffff', + # 'sidebarbgcolor': '#08385D', + # # 'sidebartextcolor': , + # 'relbarbgcolor': '#08385D', + # 'relbartextcolor': '#ffffff', + # # 'relbarlinkcolor': '#3432D8', + # 'bgcolor': '#ffffff', + # # 'textcolor': , + # 'linkcolor': '#3432D8', + # # 'visitedlinkcolor': , + # 'headbgcolor': '#C1C1C1', + # 'headtextcolor': '#08385D', + # 'headlinkcolor': '#3432D8', + # # 'codebgcolor': , + # # 'codetextcolor': , + # # 'bodyfont': , + # # 'headfont': , +# } + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['static'] + +# END diff --git a/docsource/index.rst b/docsource/index.rst new file mode 100644 index 0000000..1526cea --- /dev/null +++ b/docsource/index.rst @@ -0,0 +1,22 @@ +.. python-comtrade documentation master file + +Module Documentation +==================== + +.. toctree:: + :maxdepth: 1 + + comtrade + support + + + +.. mdinclude:: ../README.md + + +Source Repository and Package Release (PyPI): +============================================= + +- GitHub: https://github.com/dparrini/python-comtrade +- PyPI: https://pypi.org/project/comtrade/ + diff --git a/docsource/sphinxrequires.txt b/docsource/sphinxrequires.txt new file mode 100644 index 0000000..09f1f66 --- /dev/null +++ b/docsource/sphinxrequires.txt @@ -0,0 +1,5 @@ +wheel +sphinx +m2r2 +numpydoc +sphinx-sitemap \ No newline at end of file diff --git a/docsource/support.rst b/docsource/support.rst new file mode 100644 index 0000000..2631484 --- /dev/null +++ b/docsource/support.rst @@ -0,0 +1,31 @@ +Channel Object References +========================= + +.. autoclass:: comtrade.Channel + :members: + +.. autoclass:: comtrade.StatusChannel + :members: + +.. autoclass:: comtrade.AnalogChannel + :members: + + +DatReader Object Reference +========================== + +.. autoclass:: comtrade.DatReader + :members: + +.. autoclass:: comtrade.AsciiDatReader + :members: + +.. autoclass:: comtrade.BinaryDatReader + :members: + +.. autoclass:: comtrade.Binary32DatReader + :members: + +.. autoclass:: comtrade.Float32DatReader + :members: + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..cddea90 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "python-comtrade" +authors = [ + {name = "David Parrini"}, + {name = "Joe Stanley", email = "engineerjoe440@yahoo.com"} +] +maintainers = [ + {name = "David Parrini"}, +] +description = "A Python 3 module designed to read Common Format for Transient Data Exchange (COMTRADE) files." +readme = "README.md" +license = {file = "LICENSE"} +classifiers = [ + "License :: OSI Approved :: MIT License" +] +dynamic = ["version"] + +[project.urls] +Home = "https://github.com/engineerjoe440/python-comtrade" + +[tool.flit.module] +name = "comtrade" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fc1f76c --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() \ No newline at end of file