Skip to content

Commit

Permalink
Merge pull request #27 from ikalnytskyi/chore/housekeeping
Browse files Browse the repository at this point in the history
Housekeeping
  • Loading branch information
ikalnytskyi authored May 7, 2024
2 parents 170d3eb + 2259ce9 commit 35a5ace
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 188 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up sources
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Publish to PyPI
env:
Expand Down
21 changes: 15 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,36 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up sources
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Run ruff
run: pipx run tox -e lint
env:
RUFF_OUTPUT_FORMAT: github

test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-latest]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
include:
- os: macos-latest
python-version: "3.12"
- os: windows-latest
python-version: "3.12"

runs-on: ${{ matrix.os }}
steps:
- name: Set up sources
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

Expand All @@ -44,4 +53,4 @@ jobs:
if: matrix.os == 'macos-latest'

- name: Run pytest
run: pipx run tox -e py3
run: pipx run tox -e test
36 changes: 29 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "httpie-credential-store"
version = "3.1.0"
Expand All @@ -14,19 +18,37 @@ python = "^3.8"
httpie = "^3.1"
keyring = ">= 23.5"

[tool.poetry.dev-dependencies]
[tool.poetry.group.lint]
optional = true

[tool.poetry.group.lint.dependencies]
ruff = "^0.4.2"

[tool.poetry.group.test]
optional = true

[tool.poetry.group.test.dependencies]
pytest = "^7.1"
responses = "^0.20"
mock = "^4.0"
pytest-github-actions-annotate-failures = "*"

[tool.poetry.plugins."httpie.plugins.auth.v1"]
credential-store = "httpie_credential_store:CredentialStoreAuthPlugin"
creds = "httpie_credential_store:CredsAuthPlugin"

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

[tool.ruff]
line-length = 88
line-length = 100
target-version = "py38"

[tool.ruff.lint]
select = ["ALL"]
ignore = ["ANN", "D", "PTH", "PLR", "PT005", "ISC001", "INP001", "S603", "S607", "COM812"]

[tool.ruff.lint.per-file-ignores]
"src/httpie_credential_store/_keychain.py" = ["S602"]
"tests/*" = ["S101", "INP001"]

[tool.ruff.lint.isort]
known-first-party = ["httpie_credential_store"]
lines-after-imports = 2
lines-between-types = 1
24 changes: 12 additions & 12 deletions src/httpie_credential_store/_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class HTTPBasicAuth(requests.auth.HTTPBasicAuth, AuthProvider):
name = "basic"

def __init__(self, *, username, password):
super(HTTPBasicAuth, self).__init__(username, get_secret(password))
super().__init__(username, get_secret(password))


class HTTPDigestAuth(requests.auth.HTTPDigestAuth, AuthProvider):
Expand All @@ -54,7 +54,7 @@ class HTTPDigestAuth(requests.auth.HTTPDigestAuth, AuthProvider):
name = "digest"

def __init__(self, *, username, password):
super(HTTPDigestAuth, self).__init__(username, get_secret(password))
super().__init__(username, get_secret(password))


class HTTPHeaderAuth(requests.auth.AuthBase, AuthProvider):
Expand All @@ -67,18 +67,20 @@ def __init__(self, *, name, value):
self._value = get_secret(value)

if not is_legal_header_name(self._name):
raise ValueError(
error_message = (
f"HTTP header authentication provider received invalid "
f"header name: {self._name!r}. Please remove illegal "
f"characters and try again."
)
raise ValueError(error_message)

if is_illegal_header_value(self._value):
raise ValueError(
error_message = (
f"HTTP header authentication provider received invalid "
f"header value: {self._value!r}. Please remove illegal "
f"characters and try again."
)
raise ValueError(error_message)

def __call__(self, request):
request.headers[self._name] = self._value
Expand All @@ -95,18 +97,20 @@ def __init__(self, *, token, scheme="Bearer"):
self._token = get_secret(token)

if is_illegal_header_value(self._scheme):
raise ValueError(
error_message = (
f"HTTP token authentication provider received scheme that "
f"contains illegal characters: {self._scheme!r}. Please "
f"remove these characters and try again."
)
raise ValueError(error_message)

if is_illegal_header_value(self._token):
raise ValueError(
error_message = (
f"HTTP token authentication provider received token that "
f"contains illegal characters: {self._token!r}. Please "
f"remove these characters and try again."
)
raise ValueError(error_message)

def __call__(self, request):
request.headers["Authorization"] = f"{self._scheme} {self._token}"
Expand All @@ -119,19 +123,15 @@ class HTTPMultipleAuth(requests.auth.AuthBase, AuthProvider):
name = "multiple"

def __init__(self, *, providers):
self._providers = [
get_auth(provider.pop("provider"), **provider) for provider in providers
]
self._providers = [get_auth(provider.pop("provider"), **provider) for provider in providers]

def __call__(self, request):
for provider in self._providers:
request = provider(request)
return request


_PROVIDERS = {
provider_cls.name: provider_cls for provider_cls in AuthProvider.__subclasses__()
}
_PROVIDERS = {provider_cls.name: provider_cls for provider_cls in AuthProvider.__subclasses__()}


def get_auth(provider, **kwargs):
Expand Down
20 changes: 10 additions & 10 deletions src/httpie_credential_store/_keychain.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ class ShellKeychain(KeychainProvider):

def get(self, *, command):
try:
return subprocess.check_output(command, shell=True).decode("UTF-8")
return subprocess.check_output(command, shell=True, text=True)
except subprocess.CalledProcessError as exc:
raise LookupError(f"No secret found: {exc}")
error_message = f"No secret found: {exc}"
raise LookupError(error_message) from exc


class PasswordStoreKeychain(ShellKeychain):
Expand All @@ -40,10 +41,11 @@ def get(self, *, name):
try:
# password-store may store securely extra information along with a
# password. Nevertheless, a password is always a first line.
text = super(PasswordStoreKeychain, self).get(command=f"pass {name}")
text = subprocess.check_output(["pass", name], text=True)
return text.splitlines()[0]
except LookupError:
raise LookupError(f"password-store: no secret found: '{name}'")
except subprocess.CalledProcessError as exc:
error_message = f"password-store: no secret found: '{name}'"
raise LookupError(error_message) from exc


class SystemKeychain(KeychainProvider):
Expand All @@ -57,17 +59,15 @@ def __init__(self):
def get(self, *, service, username):
secret = self._keyring.get_password(service, username)
if not secret:
raise LookupError(
error_message = (
f"No secret found for '{service}' service and '{username}' "
f"username in '{self.name}' keychain."
)
raise LookupError(error_message)
return secret


_PROVIDERS = {
provider_cls.name: provider_cls
for provider_cls in KeychainProvider.__subclasses__()
}
_PROVIDERS = {provider_cls.name: provider_cls for provider_cls in KeychainProvider.__subclasses__()}


def get_keychain(provider):
Expand Down
4 changes: 3 additions & 1 deletion src/httpie_credential_store/_plugin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""HTTPie Credential Store Auth Plugin."""

import requests
import httpie.plugins
import requests

from ._store import get_credential_store

Expand All @@ -23,6 +23,8 @@ class CredentialStoreAuthPlugin(httpie.plugins.AuthPlugin):
auth_parse = False # do not parse '-a' content

def get_auth(self, username=None, password=None):
_ = username
_ = password
credential_id = self.raw_auth

class CredentialStoreAuth(requests.auth.AuthBase):
Expand Down
14 changes: 8 additions & 6 deletions src/httpie_credential_store/_store.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Credentials are managed here."""

import io
import json
import os
import re
Expand All @@ -12,7 +11,7 @@
from ._auth import get_auth


class CredentialStore(object):
class CredentialStore:
"""Credential store, manages your credentials."""

def __init__(self, credentials):
Expand Down Expand Up @@ -42,10 +41,11 @@ def get_credential_store(name, directory=httpie.config.DEFAULT_CONFIG_DIR):
credential_file = os.path.join(directory, name)

if not os.path.exists(credential_file):
raise FileNotFoundError(
error_message = (
f"Credentials file '{credential_file}' is not found; "
f"please create one and try again."
)
raise FileNotFoundError(error_message)

mode = stat.S_IMODE(os.stat(credential_file).st_mode)

Expand All @@ -56,20 +56,22 @@ def get_credential_store(name, directory=httpie.config.DEFAULT_CONFIG_DIR):
# ignore this platform for a while.
if sys.platform != "win32":
if mode & 0o077 > 0o000:
raise PermissionError(
error_message = (
f"Permissions '{mode:04o}' for '{credential_file}' are too "
f"open; please ensure your credentials file is NOT accessible "
f"by others."
)
raise PermissionError(error_message)

if mode & 0o400 != 0o400:
raise PermissionError(
error_message = (
f"Permissions '{mode:04o}' for '{credential_file}' are too "
f"close; please ensure your credentials file CAN be read by "
f"you."
)
raise PermissionError(error_message)

with io.open(credential_file, encoding="UTF-8") as f:
with open(credential_file, encoding="UTF-8") as f:
credentials = json.load(f)

return CredentialStore(credentials)
15 changes: 6 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
import os
import tempfile

import mock
import pytest


@pytest.fixture(scope="session", autouse=True)
def _httpie_config_dir():
def _httpie_config_dir(tmp_path_factory: pytest.TempPathFactory):
"""Set path to HTTPie configuration directory."""

# HTTPie can optionally read a path to configuration directory from
# environment variable. In order to avoid messing with user's local
# configuration, HTTPIE_CONFIG_DIR environment variable is patched to point
# to a temporary directory instead. But here's the thing, HTTPie is not ran
# in subprocess in these tests, and so the environment variable is read
# only once on first package import. That's why it must be set before
# just once on first package import. That's why it must be set before
# HTTPie package is imported and that's why the very same value must be
# used for all tests (session scope). Otherwise, tests may fail because
# they will look for credentials file in different directory.
with tempfile.TemporaryDirectory() as tmpdir:
with mock.patch.dict(os.environ, {"HTTPIE_CONFIG_DIR": tmpdir}):
yield tmpdir
with pytest.MonkeyPatch.context() as monkeypatch:
tmp_path = tmp_path_factory.mktemp(".httpie")
monkeypatch.setenv("HTTPIE_CONFIG_DIR", str(tmp_path))
yield tmp_path
Loading

0 comments on commit 35a5ace

Please sign in to comment.