Compare commits

...

4 Commits

9 changed files with 3115 additions and 11 deletions

79
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,79 @@
---
# All of the pre-commit hooks here actually use the `pytyhon` pre-commit language
# setting. However, for the python language setting, pre-commit will create and manage
# a cached virtual environment for each hook ID and do a bare `pip install <repo>` into
# the venv to setup the hook. This can result in conflicting dependency versions between
# the version installed to the pre-commit venv and the version installed to the Poetry
# venv specified in the lockfile.
#
# The solution is to specify `language: system` for all hooks and then install the
# required dependencies to the Poetry venv. The `system` language skips the isolated
# venv creation and looks for the entrypoint specified by the hook in the global
# environment which, if running in the Poetry venv, will find the entrypoint provided
# by the Poetry-managed dependency.
#
repos:
- repo: local
hooks:
- id: end-of-file-fixer
name: end-of-file-fixer
entry: end-of-file-fixer
language: system
types:
- text
- id: fix-encoding-pragma
name: fix-encoding-pragma
entry: fix-encoding-pragma
language: system
args:
- "--remove"
types:
- python
- id: trailing-whitespace-fixer
name: trailing-whitespace-fixer
entry: trailing-whitespace-fixer
language: system
types:
- text
- id: check-merge-conflict
name: check-merge-conflict
entry: check-merge-conflict
language: system
types:
- text
- id: reorder-python-imports
name: reorder-python-imports
entry: reorder-python-imports
language: system
args:
- "--unclassifiable-application-module=tox_poetry_installer"
types:
- python
- id: black
name: black
entry: black
language: system
types:
- python
- id: blacken-docs
name: blacken-docs
entry: blacken-docs
language: system
types:
- text
- id: mdformat
name: mdformat
entry: mdformat
language: system
args:
- "--number"
- "--wrap=90"
types:
- markdown

View File

@ -11,7 +11,6 @@
# --disable=W"
disable=logging-fstring-interpolation
,logging-format-interpolation
,bad-continuation
,line-too-long
,ungrouped-imports
,typecheck

View File

@ -27,9 +27,10 @@ Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address, without their
explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
- Publishing others' private information, such as a physical or email address, without
their explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional
setting
## Enforcement Responsibilities
@ -52,8 +53,8 @@ offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the
community leaders responsible for enforcement at \[INSERT CONTACT METHOD\]. All
complaints will be reviewed and investigated promptly and fairly.
community leaders responsible for enforcement at \[INSERT CONTACT METHOD\]. All complaints
will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of
any incident.
@ -105,8 +106,8 @@ toward or disparagement of classes of individuals.
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at

40
Makefile Normal file
View File

@ -0,0 +1,40 @@
# spectrepy makefile
.PHONY: help
# Put it first so that "make" without argument is like "make help"
# Adapted from:
# https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
help: ## List Makefile targets
$(info Makefile documentation)
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-10s\033[0m %s\n", $$1, $$2}'
clean-tox:
rm --recursive --force ./.mypy_cache
rm --recursive --force ./.tox
rm --recursive --force tests/__pycache__/
rm --recursive --force .pytest_cache/
rm --force .coverage
clean-py:
rm --recursive --force ./dist
rm --recursive --force ./build
rm --recursive --force ./*.egg-info
rm --recursive --force ./**/__pycache__/
clean: clean-tox clean-py; ## Clean temp build/cache files and directories
wheel: ## Build Python binary distribution wheel package
poetry build --format wheel
source: ## Build Python source distribution package
poetry build --format sdist
test: ## Run the project testsuite(s)
poetry run tox --recreate --parallel
dev: ## Create the local dev environment
poetry install --with dev --sync
poetry run pre-commit install
publish: test wheel source ## Build and upload to pypi (requires $PYPI_API_KEY be set)
@poetry publish --username __token__ --password $(PYPI_API_KEY)

2720
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,64 @@
[tool.poetry]
name = "spectrepy"
version = "0.1.0"
license = "MIT"
description = "Python bindings for the Spectre Password Manager"
authors = ["Ethan Paul <24588726+enpaul@users.noreply.github.com>"]
license = "MIT"
repository = "https://github.com/enpaul/spectrepy/"
packages = [
{include = "spectrepy.py"},
{include = "tests/*.py", format = "sdist"}
]
keywords = ["spectre", "mpw", "password", "manager", "masterpassword"]
readme = "README.md"
classifiers = [
"Development Status :: 1 - Planning",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Operating System :: POSIX",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: Implementation :: CPython"
]
[tool.poetry.dependencies]
python = "^3.7"
[tool.poetry.group.dev.dependencies]
black = {version = "^23.3.0", python = "^3.10"}
blacken-docs = {version = "^1.13.0", python = "^3.10"}
ipython = {version = "^8.12.0", python = "^3.10"}
mdformat = {version = "^0.7.16", python = "^3.10"}
mdformat-gfm = {version = "^0.3.5", python = "^3.10"}
mypy = {version = "^1.1.1", python = "^3.10"}
pre-commit = {version = "^3.2.1", python = "^3.10"}
pre-commit-hooks = {version = "^4.4.0", python = "^3.10"}
pylint = {version = "^2.17.1", python = "^3.10"}
reorder-python-imports = {version = "^3.9.0", python = "^3.10"}
types-toml = {version = "^0.10.8.6", python = "^3.10"}
toml = {version = "^0.10.2", python = "^3.10"}
[tool.poetry.group.test.dependencies]
pytest = "^7.2.2"
pytest-cov = "^4.0.0"
toml = "^0.10.2"
typing-extensions = {version = "^4.5.0", python = "^3.8"}
[tool.poetry.group.ci.dependencies]
tox = "^3.20"
tox-poetry-installer = {extras = ["poetry"], version = "^0.10.2"}
[tool.poetry.group.security.dependencies]
bandit = {version = "^1.7.5", python = "^3.10"}
[build-system]
requires = ["poetry-core"]

119
spectrepy.py Normal file
View File

@ -0,0 +1,119 @@
"""Python library bindings for the Spectre password manager"""
import enum
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
from typing import Union
__title__ = "spectrepy"
__summary__ = "Python bindings for the Spectre Password Manager"
__version__ = "0.1.0"
__url__ = "https://github.com/enpaul/spectrepy/"
__license__ = "MIT"
__authors__ = ["Ethan Paul <24588726+enpaul@users.noreply.github.com>"]
__all__ = [
"__title__",
"__summary__",
"__version__",
"__url__",
"__license__",
"__authors__",
"SpectrePasswordType",
"SpectreAlgorithmVersion",
"SpectrePassword",
"Spectre",
"generate",
"get_version",
]
class SpectrePasswordType(enum.Enum):
X = "maximum"
L = "long"
M = "medium"
B = "basic"
S = "short"
I = "pin"
N = "name"
P = "phrase"
K = "key"
class SpectreAlgorithmVersion(enum.Enum):
V0 = 0
V1 = 1
V2 = 2
V3 = 3
@dataclass(frozen=True)
class SpectrePassword:
site: str
identicon: str
password_type: SpectrePasswordType
username: str
algorithm_version: SpectreAlgorithmVersion
context: str
def generate(*args, **kwargs) -> SpectrePassword:
return Spectre(*args, **kwargs).generate()
def get_version(*args, **kwargs) -> str:
return Spectre(*args, **kwargs).version
def get_path(*args, **kwargs) -> Path:
return Spectre(*args, **kwargs).path
class Spectre:
def __init__(self, binary: Optional[Union[str, Path]] = None):
self._path: Path
self._version: str
self._path = (
Path(binary).resolve() if binary is not None else self._find_binary()
)
self._version = self._fetch_version()
@property
def version(self) -> str:
return self._version
@property
def program(self) -> str:
return self._program
@property
def path(self) -> Path:
return self._path
@staticmethod
def _find_binary() -> Path:
binary = shutil.which("spectre") or shutil.which("mpw")
if not binary:
raise RuntimeError
return Path(binary)
def generate(self) -> SpectrePassword:
pass
def _call(self, *args, raise_errors: bool = True) -> subprocess.CompletedProcess:
result = subprocess.run([str(self._path)] + [*args], capture_output=True)
if result.returncode != 0 and raise_errors:
raise RuntimeError
return result
def _fetch_version(self) -> str:
out = self._call("-h")
raw = out.stderr.decode().split("\n")[0].strip().split(" ")
version = raw[-1]
return version

37
tests/test_metadata.py Normal file
View File

@ -0,0 +1,37 @@
"""Ensure that the pyproject and module metadata never drift out of sync
The next best thing to having one source of truth is having a way to ensure all of your
sources of truth agree with each other.
"""
from pathlib import Path
import toml
import spectrepy
def test_metadata():
"""Test that module metadata matches pyproject poetry metadata"""
with (Path(__file__).resolve().parent / ".." / "pyproject.toml").open() as infile:
pyproject = toml.load(infile, _dict=dict)
assert pyproject["tool"]["poetry"]["name"] == spectrepy.__title__
assert pyproject["tool"]["poetry"]["version"] == spectrepy.__version__
assert pyproject["tool"]["poetry"]["license"] == spectrepy.__license__
assert pyproject["tool"]["poetry"]["description"] == spectrepy.__summary__
assert pyproject["tool"]["poetry"]["repository"] == spectrepy.__url__
assert (
all(
item in spectrepy.__authors__
for item in pyproject["tool"]["poetry"]["authors"]
)
is True
)
assert (
all(
item in pyproject["tool"]["poetry"]["authors"]
for item in spectrepy.__authors__
)
is True
)

63
tox.ini Normal file
View File

@ -0,0 +1,63 @@
[tox]
envlist = py3{7,8,9,10,11}, static, static-tests, security
isolated_build = true
skip_missing_interpreters = true
[testenv]
description = Run the tests
require_locked_deps = true
require_poetry = true
poetry_dep_groups =
test
commands =
pytest {toxinidir}/tests/ \
--cov spectrepy \
--cov-config {toxinidir}/.coveragerc \
--cov-report term-missing
[testenv:static]
description = Static formatting and quality enforcement
basepython = python3.10
platform = linux
ignore_errors = true
poetry_dep_groups =
dev
commands =
pre-commit run \
--all-files
pylint {toxinidir}/spectrepy.py \
--rcfile {toxinidir}/.pylintrc
mypy {toxinidir}/spectrepy.py \
--ignore-missing-imports \
--no-strict-optional
[testenv:static-tests]
description = Static formatting and quality enforcement for the tests
basepython = python3.10
platform = linux
ignore_errors = true
poetry_dep_groups =
dev
commands =
pylint {toxinidir}/tests/ \
--rcfile {toxinidir}/.pylintrc
mypy {toxinidir}/tests/ \
--ignore-missing-imports \
--no-strict-optional
[testenv:security]
description = Security checks
basepython = python3.10
platform = linux
ignore_errors = true
skip_install = true
poetry_dep_groups =
security
commands =
bandit {toxinidir}/spectrepy.py \
--recursive \
--quiet
bandit {toxinidir}/tests/ \
--recursive \
--quiet \
--skip B101