Skip to content

Commit 819ef39

Browse files
authored
Merge pull request #250 from statisticsnorway/reset_auth
2 parents 309f052 + 3261868 commit 819ef39

13 files changed

Lines changed: 588 additions & 150 deletions

.pre-commit-config.yaml

Lines changed: 20 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,41 @@
11
default_language_version:
2-
python: python3.10
2+
python: python3.11
33
repos:
4-
- repo: local
4+
# Pre-commit maintained utility hooks
5+
- repo: https://github.com/pre-commit/pre-commit-hooks
6+
rev: v4.6.0
57
hooks:
6-
# General file checkers
78
- id: check-added-large-files
8-
name: Check added large files
9-
entry: check-added-large-files
10-
language: system
119
- id: check-merge-conflict
12-
name: Check for merge conflict strings
13-
entry: check-merge-conflict
14-
language: system
1510
- id: check-case-conflict
16-
name: Check for problematic file names
17-
entry: check-case-conflict
18-
language: system
19-
20-
# General text checkers
2111
- id: end-of-file-fixer
22-
name: Fix End of Files
23-
entry: end-of-file-fixer
24-
language: system
2512
types: [text]
2613
- id: trailing-whitespace
27-
name: Trim Trailing Whitespace
28-
entry: trailing-whitespace-fixer
29-
language: system
3014
types: [text]
3115
- id: mixed-line-ending
32-
name: Mixed Line Ending
33-
entry: mixed-line-ending
34-
language: system
3516
types: [text]
36-
37-
# Serialization format checkers
3817
- id: check-yaml
39-
name: Check Yaml
40-
entry: check-yaml
41-
language: system
42-
types: [yaml]
4318
- id: check-json
44-
name: Check JSON
45-
entry: check-json
46-
language: system
47-
types: [json]
4819
- id: check-toml
49-
name: Check Toml
50-
entry: check-toml
51-
language: system
52-
types: [toml]
53-
54-
# Python tools
5520
- id: check-ast
56-
name: Check Python syntax
57-
entry: check-ast
58-
language: system
59-
types: [python]
21+
22+
# Darglint docstring checker
23+
- repo: https://github.com/terrencepreilly/darglint
24+
rev: v1.8.1
25+
hooks:
6026
- id: darglint
61-
name: darglint
62-
entry: darglint
63-
language: system
64-
types: [python]
27+
28+
# Ruff linter
29+
- repo: https://github.com/astral-sh/ruff-pre-commit
30+
rev: v0.6.9
31+
hooks:
6532
- id: ruff
66-
name: ruff
67-
entry: ruff check --fix --exit-non-zero-on-fix
68-
language: system
69-
types_or: [python, jupyter]
33+
args: ["--fix", "--exit-non-zero-on-fix"]
7034
require_serial: true
35+
36+
# Black formatter
37+
- repo: https://github.com/psf/black
38+
rev: 24.10.0
39+
hooks:
7140
- id: black
72-
name: black
73-
entry: black
74-
language: system
75-
types_or: [python, jupyter]
7641
require_serial: true

CONTRIBUTING.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,68 @@ To run linting and code formatting checks before committing your change, you can
119119
nox --session=pre-commit -- install
120120
```
121121

122+
123+
124+
125+
126+
## Project setup
127+
128+
### Poetry venv
129+
Installing the dev-dependencies in a new environment can be done with the following command:
130+
```bash
131+
poetry install --with dev
132+
```
133+
134+
Or if you want to call nox with poetry and pass pytest flags through...
135+
```bash
136+
poetry run nox -p 3.11
137+
```
138+
139+
The marker "integration_dapla" is for running specific tests locally on a dapla. They are placed in tests/test_integration_dapla.py .\
140+
Use this flag in the command-line call to avoid running them on other platforms.
141+
142+
143+
### Pytest coverage
144+
```bash
145+
poetry run nox -s tests ypy -p 3.11
146+
```
147+
Run this when developing tests.
148+
If you achieve a higher testing coverage make sure to increase the threshold in the workflow.
149+
.github/workflows/tests.yml
150+
(at the bottom)
151+
152+
### Running the pre-commit hooks locally
153+
```bash
154+
poetry run nox -s pre-commit -p 3.11
155+
```
156+
Several of the pre-commit hooks will try to modify the files on a fail. Re-running the command might therefore result in a different result the second time.
157+
158+
### Type-checking with Mypy
159+
```bash
160+
poetry run nox -s mypy -p 3.11
161+
```
162+
163+
## Publish new version of package to Pypi
164+
165+
The action to publish to Pypi is connected with a workflow to releases from Github.
166+
So to publish to Pypi, make sure everything is done, and in the main branch, bump the version in pyproject.toml, add a tag and release on Github.
167+
168+
## Rebuild docs
169+
Necessary if you have changed the package at the submodule-level.
170+
171+
The content of the {eval-rst} block below is generated by the command:
172+
```bash
173+
poetry run sphinx-apidoc -T -f -t ./docs/templates -o ./docs ./src
174+
```
175+
from the root directory.
176+
177+
178+
179+
### Bump version
180+
```bash
181+
poetry version patch
182+
```
183+
122184
It is recommended to open an issue before starting work on anything.
123185
This will allow a chance to talk it over with the owners and validate your approach.
124186

DEV_README.md

Lines changed: 0 additions & 60 deletions
This file was deleted.

demo/01_enkleste_bruk.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# extension: .py
77
# format_name: percent
88
# format_version: '1.3'
9-
# jupytext_version: 1.15.2
9+
# jupytext_version: 1.18.1
1010
# kernelspec:
1111
# display_name: dapla-statbank-client
1212
# language: python

docs/reference.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,6 @@ statbank.globals module
6464
:undoc-members:
6565
:show-inheritance:
6666
67-
statbank.netrc module
68-
---------------------
69-
70-
.. automodule:: statbank.netrc
71-
:members:
72-
:undoc-members:
73-
:show-inheritance:
74-
7567
statbank.response\_to\_pandas module
7668
------------------------------------
7769
@@ -111,3 +103,14 @@ statbank.uttrekk\_validations module
111103
:members:
112104
:undoc-members:
113105
:show-inheritance:
106+
107+
statbank.writable\_netrc module
108+
-------------------------------
109+
110+
.. automodule:: statbank.writable_netrc
111+
:members:
112+
:undoc-members:
113+
:show-inheritance:
114+
115+
116+
```

src/statbank/api_exceptions.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,37 @@
1+
from typing import Any
2+
3+
import requests
4+
5+
6+
class StatbankAuthError(requests.HTTPError):
7+
"""Raised when authentication with Statbank fails.
8+
9+
Extends ``requests.HTTPError`` with an optional ``response_content`` attribute
10+
that can hold parsed JSON from the response.
11+
12+
Args:
13+
*args: Positional arguments forwarded to ``requests.HTTPError``.
14+
response_content: Optional JSON-decoded payload to attach.
15+
**kwargs: Keyword arguments forwarded to ``requests.HTTPError``.
16+
"""
17+
18+
def __init__(
19+
self,
20+
*args: Any,
21+
response_content: dict[str, Any] | None = None,
22+
**kwargs: Any,
23+
) -> None:
24+
"""Initialize the Statbank authentication error.
25+
26+
Args:
27+
*args: Positional arguments forwarded to ``requests.HTTPError``.
28+
response_content: Optional JSON-decoded payload to attach.
29+
**kwargs: Keyword arguments forwarded to ``requests.HTTPError``.
30+
"""
31+
super().__init__(*args, **kwargs)
32+
self.response_content: dict[str, Any] | None = response_content
33+
34+
135
class StatbankApiError(Exception):
236
"""Base class for Statbank Api Errors."""
337

src/statbank/auth.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from dapla_auth_client import AuthClient
1919
from furl import furl
2020

21+
from .api_exceptions import StatbankAuthError
2122
from .globals import DaplaEnvironment
2223
from .globals import DaplaRegion
2324
from .globals import UseDb
@@ -175,7 +176,7 @@ def _build_headers(self: Self) -> dict[str, str]:
175176
}
176177

177178
def _get_auth(self) -> requests.auth.AuthBase:
178-
host = cast(str, self._config.endpoint_base.host) # noqa: TC006
179+
host = cast(str, self._config.endpoint_base.host)
179180

180181
with Netrc(self._config.netrc_path) as authfile:
181182
auth_record = authfile[host]
@@ -196,6 +197,50 @@ def _get_auth(self) -> requests.auth.AuthBase:
196197
), # Can be None in Python 3.10
197198
)
198199

200+
def reset_auth(self) -> None:
201+
"""Reset the auth by removing the entry from the .netrc-file first, then asking to enter the username and password again."""
202+
self._cleanup_netrc()
203+
self._auth = self._get_auth()
204+
205+
def _cleanup_netrc(self) -> None:
206+
host = cast("str", self._config.endpoint_base.host)
207+
with Netrc(self._config.netrc_path) as authfile:
208+
if host in authfile:
209+
del authfile[host]
210+
211+
def _react_to_httperror_should_retry(
212+
self,
213+
e: requests.HTTPError | StatbankAuthError,
214+
) -> bool:
215+
default_err_msg = """Got an http-error, but it does not look like an account-lock or invalid password/username,
216+
is this something we should program for?
217+
If you want to reset the auth manually call StatbankClient().reset_auth() or
218+
StatbankAuth().reset_auth(). Alternatively edit the .netrc file directly.""".replace(
219+
"\n",
220+
"",
221+
)
222+
223+
if hasattr(e, "response_content") and e.response_content:
224+
if "ORA-28000" in e.response_content.get("ExceptionMessage", ""):
225+
err_msg = f'Your account has been locked. Contact kundeservice@ssb.no to unlock. Resetting auth. Errortext: {e.response_content.get("ExceptionMessage", "")}'
226+
logger.error(err_msg)
227+
new_err = StatbankAuthError(err_msg)
228+
self._cleanup_netrc()
229+
raise new_err
230+
if "ORA-01017" in e.response_content.get("ExceptionMessage", ""):
231+
logger.error(
232+
f"TYPE CAREFULLY - The username and password you used may have been wrong. Resetting Auth. Errortext: {e.response_content.get('ExceptionMessage', '')}",
233+
)
234+
self.reset_auth()
235+
return True
236+
logger.error(
237+
f"{default_err_msg}{e.response_content.get('Exception_message', '')} - {e} ",
238+
)
239+
else:
240+
logger.error(f"{default_err_msg}{e}.")
241+
242+
raise e
243+
199244
def _encrypt_password(self: Self, password: str) -> str:
200245
pat = None
201246

@@ -207,16 +252,16 @@ def _encrypt_password(self: Self, password: str) -> str:
207252

208253
auth = TokenAuth(pat) if pat is not None else None
209254

210-
repsonse = r.post(
255+
response = r.post(
211256
self._config.encrypt_url.url,
212257
auth=auth,
213258
json={"message": password},
214259
timeout=30,
215260
)
216261

217-
repsonse.raise_for_status()
262+
response.raise_for_status()
218263

219-
data = cast(dict[Literal["message"], str], repsonse.json()) # noqa: TC006
264+
data = cast('dict[Literal["message"], str]', response.json())
220265

221266
return data["message"]
222267

0 commit comments

Comments
 (0)