diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..9045ed9 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,16 @@ +name: Documentation +on: + push: + branches: + - main + +jobs: + mkdocs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.7 + - run: pip install mkdocs-material + - run: mkdocs gh-deploy --force diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..b7c3e8b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,45 @@ +name: Tests + +on: + push: + paths-ignore: + - 'docs/**' + branches: [ main ] + pull_request: + paths-ignore: + - 'docs/**' + branches: + - '**' + +jobs: + pre-commit-checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Linting + run: | + pip install pre-commit + pre-commit run --all-files + + tests: + needs: pre-commit-checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + - uses: webfactory/ssh-agent@v0.4.1 + with: + ssh-private-key: ${{ secrets.CHIMEFRB_BOT_SSH_PRIVATE_KEY }} + - name: Installation + run: | + pip install --upgrade pip + pip install poetry + poetry install + - name: Run Tests + run: | + poetry run pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0253a56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,136 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Docker +*.TempDockerfile + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +requirements.txt + +# 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/ + +# 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 +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Ignore dynaconf secret files +.secrets.* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d5c6448 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,62 @@ +repos: + + - repo: https://github.com/pycqa/isort + rev: 5.5.4 + hooks: + - id: isort + args: [ --multi-line=3, --trailing-comma, --force-grid-wrap=0, --use-parentheses, --line-width=88] + + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.4 + hooks: + - id: flake8 + additional_dependencies: ["flake8-eradicate==0.4.0"] + args: [--max-line-length=89] + + + - repo: https://github.com/asottile/pyupgrade + rev: v2.7.2 + hooks: + - id: pyupgrade + args: [--py36-plus] + + - repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.782 + hooks: + - id: mypy + args: [--ignore-missing-imports] + + - repo: https://github.com/pycqa/pydocstyle + rev: 4.0.0 # pick a git hash / tag to point to + hooks: + - id: pydocstyle + args: [--convention=numpy, --add-ignore=D104] + + - repo: https://github.com/asottile/blacken-docs + rev: v1.8.0 + hooks: + - id: blacken-docs + additional_dependencies: [black==20.8b1] + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + exclude: ^docs/* + - id: end-of-file-fixer + exclude: ^docs/* + - id: debug-statements + - id: check-case-conflict + - id: check-json + - id: check-yaml + - id: mixed-line-ending + - id: check-toml + - id: pretty-format-json + - id: check-docstring-first + - id: check-symlinks + - id: detect-private-key diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7b607ff --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 CHIME/FRB Collaboration + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f3f642 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# subpulse-analysis + +Welcome to subpulse-analysis. For more information, see documentation [subpulse-analysis](chimefrb.github.io/subpulse-analysis) diff --git a/backend.yml b/backend.yml new file mode 100644 index 0000000..7dd4e36 --- /dev/null +++ b/backend.yml @@ -0,0 +1,7 @@ +# Defaults +default: + backend: + name: /subpulse + components: + blueprints: + - subpulse.backend.rest diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..83c1798 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,3 @@ +# subpulse-analysis + +Welcome to subpulse-analysis. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..4b43417 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,66 @@ +site_name: subpulse-analysis +site_author: Shiny Brar +site_description: "Maestro Backend :: /subpulse" +repo_name: chimefrb/subpulse-analysis +repo_url: "https://github.com/chimefrb/subpulse-analysis" +copyright: Copyright © 2016 - 2020 CHIME/FRB Collaboration + +theme: + name: material + palette: + scheme: default + primary: indigo + accent: indigo + font: + text: Roboto + code: Roboto Mono + language: en + features: + - tabs + +plugins: + - search + +# Extensions +markdown_extensions: + - admonition + - abbr + - attr_list + - def_list + - footnotes + - meta + - toc: + permalink: true + - pymdownx.arithmatex: + generic: true + - pymdownx.betterem: + smart_enable: all + - pymdownx.caret + - pymdownx.critic + - pymdownx.details + - pymdownx.emoji + - pymdownx.highlight: + use_pygments: true + linenums_style: pymdownx.inline + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.magiclink: + repo_url_shorthand: true + user: squidfunk + repo: mkdocs-material + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.snippets: + check_paths: true + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: "!!python/name:pymdownx.superfences.fence_code_format" + - pymdownx.tabbed + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde + +nav: + - Home: index.md diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ab27d5e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[tool.poetry] +name = "subpulse" +version = "2021.05-alpha.0" +description = "CHIME/FRB Backend - subpulse" +authors = ["Shiny Brar "] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.7" +click = "^7.1.0" +maestro = {git = "ssh://git@github.com/CHIMEFRB/maestro.git", branch="main"} + +[tool.poetry.dev-dependencies] +pytest = "^6.0.1" +mkdocs-material = "^5.5.3" +pre-commit = "^2.6.0" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/subpulse/__init__.py b/subpulse/__init__.py new file mode 100644 index 0000000..8efc662 --- /dev/null +++ b/subpulse/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +from . import analysis # noqa: F401 +from . import backend # noqa: F401 +from . import pipelines # noqa: F401 +from . import routines # noqa: F401 +from . import utilities # noqa: F401 diff --git a/subpulse/analysis/__init__.py b/subpulse/analysis/__init__.py new file mode 100644 index 0000000..9e67788 --- /dev/null +++ b/subpulse/analysis/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +""" +Analysis consists of general purpose functions. + +Note +---- +Function from either alpha or beta are not imported here, since they are not +required by default. +""" + +from . import seed # noqa: F401 diff --git a/subpulse/analysis/example/__init__.py b/subpulse/analysis/example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/subpulse/analysis/example/alpha.py b/subpulse/analysis/example/alpha.py new file mode 100644 index 0000000..f03bfa9 --- /dev/null +++ b/subpulse/analysis/example/alpha.py @@ -0,0 +1,22 @@ +"""Analysis functions for Example.""" +from random import randint + + +def randomized(minimum: int = 0, maximum: int = 100) -> int: + """Random integer generator. + + Parameters + ---------- + minimum : int, optional + Minimum value, by default 0 + maximum : int, optional + Maximum value, by default 100 + + Returns + ------- + int + Random integer in range [minimum, maxumim]. + """ + isinstance(minimum, int) + isinstance(maximum, int) + return randint(minimum, maximum) diff --git a/subpulse/analysis/example/beta.py b/subpulse/analysis/example/beta.py new file mode 100644 index 0000000..f93c417 --- /dev/null +++ b/subpulse/analysis/example/beta.py @@ -0,0 +1,19 @@ +"""Example Analysis Functions.""" +from random import random + + +def randomized(scale: float) -> float: + """Random value. + + Parameters + ---------- + scale : float + Scale. + + Returns + ------- + float + A random number. + """ + isinstance(scale, float) + return scale * random() diff --git a/subpulse/analysis/seed.py b/subpulse/analysis/seed.py new file mode 100644 index 0000000..7f0b7d0 --- /dev/null +++ b/subpulse/analysis/seed.py @@ -0,0 +1,39 @@ +""" +Core Analysis Functions. + +Note +---- +Analysis functions at the top-level are methods core to the repository, +commonly used by multiple routines and required by default. For simpler projects, +all analysis functions could be core. +""" +import uuid +from typing import Union + + +def get_uuid(flavor: str = "str") -> Union[str, bytes]: + """Generate a Universally Unique Identifier. + + Parameters + ---------- + flavor : str, optional + Type of uuid, can be str, hex or bytes, by default str + + Returns + ------- + Union[str, bytes] + uuid + + Raises + ------ + ValueError + Raised when uuid flavor misconfigured. + """ + if flavor == "str": + return str(uuid.uuid1()) + elif flavor == "hex": + return uuid.uuid1().hex + elif flavor == "bytes": + return uuid.uuid1().bytes + else: + raise ValueError("undefined flavor") diff --git a/subpulse/backend/__init__.py b/subpulse/backend/__init__.py new file mode 100644 index 0000000..4265cc3 --- /dev/null +++ b/subpulse/backend/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python diff --git a/subpulse/backend/rest.py b/subpulse/backend/rest.py new file mode 100644 index 0000000..4c1389d --- /dev/null +++ b/subpulse/backend/rest.py @@ -0,0 +1,63 @@ +"""Sample RESTful Framework.""" +from sanic import Blueprint +from sanic.request import Request +from sanic.response import HTTPResponse, json +from sanic_openapi import doc + +from subpulse.routines import composite, simple + +# NOTE: The URL Prefix for your backend has to be the name of the backend +blueprint = Blueprint("subpulse Backend", url_prefix="/") + + +@doc.summary("Hello from /subpulse!") +@blueprint.get("/") +async def hello(request: Request) -> HTTPResponse: + """Hello World. + + Parameters + ---------- + request : Request + Request object from sanic app + + Returns + ------- + HTTPResponse + """ + return json("Hello from /subpulse 🦧") + + +@doc.summary("Seeder Routine") +@blueprint.get("/simple") +async def get_seeder(request: Request) -> HTTPResponse: + """Run Seeder Routine. + + Parameters + ---------- + request : Request + Request object from sanic app + + Returns + ------- + HTTPResponse + """ + example = simple.Simple("hex") + return json(example.seedling()) + + +@doc.summary("Composite Routine") +@blueprint.get("/composite") +async def get_composite(request: Request) -> HTTPResponse: + """Run Composite Routine. + + Parameters + ---------- + request : Request + Request object from sanic app + + Returns + ------- + HTTPResponse + """ + example = composite.Composite(1.0, 10.0, "hex") + return json(example.get_random_integer()) diff --git a/subpulse/pipelines/__init__.py b/subpulse/pipelines/__init__.py new file mode 100644 index 0000000..4265cc3 --- /dev/null +++ b/subpulse/pipelines/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python diff --git a/subpulse/pipelines/pipeline.py b/subpulse/pipelines/pipeline.py new file mode 100644 index 0000000..168236a --- /dev/null +++ b/subpulse/pipelines/pipeline.py @@ -0,0 +1,18 @@ +"""Sample Pipeline.""" +import click + +from subpulse.routines import composite + + +@click.command() +@click.option("--minimum", default=0.1) +@click.option("--maximum", default=10.0) +def run(minimum, maximum): + """Execute Analysis Routine.""" + analysis = composite.Composite(maximum=maximum, minimum=minimum) + print(analysis.seedling()) + print(analysis.get_alpha()) + + +if __name__ == "__main__": + run() diff --git a/subpulse/routines/__init__.py b/subpulse/routines/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/subpulse/routines/composite.py b/subpulse/routines/composite.py new file mode 100644 index 0000000..6d4f399 --- /dev/null +++ b/subpulse/routines/composite.py @@ -0,0 +1,36 @@ +"""Sample routine, using functions from multiple analysis locations.""" +from math import ceil, floor + +from subpulse.analysis.example import alpha + + +class Composite: + """A composite routine based on two different analysis. + + Example + ------- + >>> from subpulse.routines import composite + >>> example = composite.Composite(minimum=1.5, maximum=5.0) + >>> print(example.get_alpha()) + >>> print(example.get_seed()) + """ + + def __init__(self, minimum: float, maximum: float, flavor: str): + """Intialization. + + Parameters + ---------- + minimum : float + maximum : float + """ + self.minimum = minimum + self.maximum = maximum + + def get_random_integer(self) -> float: + """Get random integer. + + Returns + ------- + float + """ + return alpha.randomized(floor(self.minimum), ceil(self.maximum)) diff --git a/subpulse/routines/compound.py b/subpulse/routines/compound.py new file mode 100644 index 0000000..b1ab4fe --- /dev/null +++ b/subpulse/routines/compound.py @@ -0,0 +1,45 @@ +"""Sample Compound Routine.""" +from subpulse.analysis.example import beta +from subpulse.routines import composite, simple + + +class Compound(composite.Composite, simple.Simple): + """A compound routine. + + Note + ---- + This routine is based on two simpler routines and some analysis functions. + + Parameters + ---------- + composite : composite.Composite + Composite Analysis Routine + simple : simple.Simple + Simple Analysis Routine + + Example + ------- + >>> from subpulse.routines import compound + >>> analysis = compound.Compound(maximum=11.0, minimum=3.0, flavor="hex") + >>> print(analysis.get_alpha()) + >>> print(analysis.get_beta()) + >>> print(analysis.seedling()) + """ + + def __init__(self, maximum: float, minimum: float, flavor: str): + """Initialization. + + Parameters + ---------- + maximum : float + minimum : float + flavor : str + Flavor of seed, valid values are [str, hex, bytes] + """ + self.minimum = minimum + self.maximum = maximum + self.flavor = flavor + + def get_beta(self): + """Get random scaled value.""" + return beta.randomized(scale=self.maximum) diff --git a/subpulse/routines/simple.py b/subpulse/routines/simple.py new file mode 100644 index 0000000..1b1ce8f --- /dev/null +++ b/subpulse/routines/simple.py @@ -0,0 +1,35 @@ +"""Example of a simple routine, as it only depends on core analysis functions.""" +from typing import Union + +from subpulse.analysis import seed + + +class Simple: + """Simple Seeder. + + Example + ------- + >>> from subpulse.routines import simple + >>> seeds = simple.Simple(flavor="str") + >>> print(seeds.seedling()) + """ + + def __init__(self, flavor: str): + """Seeder Class. + + Parameters + ---------- + flavor : str + Flavor or seed to get, + """ + self.flavor = flavor + + def seedling(self) -> Union[str, bytes]: + """Get random seedling. + + Returns + ------- + str + Random alpha-numeric seed. + """ + return seed.get_uuid(flavor=self.flavor) diff --git a/subpulse/utilities/__init__.py b/subpulse/utilities/__init__.py new file mode 100644 index 0000000..4265cc3 --- /dev/null +++ b/subpulse/utilities/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python diff --git a/subpulse/utilities/convert.py b/subpulse/utilities/convert.py new file mode 100644 index 0000000..8d6205f --- /dev/null +++ b/subpulse/utilities/convert.py @@ -0,0 +1,20 @@ +"""Conversion Utilities.""" +from typing import Any + + +def list_to_dict(data: list, value: Any = {}) -> dict: + """Convert list to a dictionary. + + Parameters + ---------- + data: list + Data type to convert + value: typing.Any + Default value for the dict keys + + Returns + ------- + dictionary : dict + Dictionary of the input data + """ + return {item: value for item in data} diff --git a/tests/test_import.py b/tests/test_import.py new file mode 100644 index 0000000..7b6762e --- /dev/null +++ b/tests/test_import.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +"""Generic tests for frb-datatrails.""" +import subpulse + + +def test_project_import(): + """Simple check to test for package importability.""" + assert isinstance(subpulse.__file__, str) + + +def test_analysis_function(): + """Check if the seed function works.""" + flavor = "str" + uuid = subpulse.analysis.seed.get_uuid(flavor=flavor) + assert isinstance(uuid, str)