pingintel-api 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pingintel_api-0.1.0/.gitignore +162 -0
- pingintel_api-0.1.0/LICENSE +21 -0
- pingintel_api-0.1.0/PKG-INFO +25 -0
- pingintel_api-0.1.0/README.md +11 -0
- pingintel_api-0.1.0/justfile +5 -0
- pingintel_api-0.1.0/pyproject.toml +28 -0
- pingintel_api-0.1.0/requirements.txt +2 -0
- pingintel_api-0.1.0/src/pingintel_api/__about__.py +1 -0
- pingintel_api-0.1.0/src/pingintel_api/__init__.py +2 -0
- pingintel_api-0.1.0/src/pingintel_api/constants.py +30 -0
- pingintel_api-0.1.0/src/pingintel_api/sov_fixer_api_client.py +356 -0
- pingintel_api-0.1.0/src/pingintel_api/sovfixerapi_cmd.py +159 -0
- pingintel_api-0.1.0/tests/__init__.py +0 -0
- pingintel_api-0.1.0/tests/tests.py +3 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# poetry
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
102
|
+
#poetry.lock
|
|
103
|
+
|
|
104
|
+
# pdm
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
106
|
+
#pdm.lock
|
|
107
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
108
|
+
# in version control.
|
|
109
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
110
|
+
.pdm.toml
|
|
111
|
+
|
|
112
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
113
|
+
__pypackages__/
|
|
114
|
+
|
|
115
|
+
# Celery stuff
|
|
116
|
+
celerybeat-schedule
|
|
117
|
+
celerybeat.pid
|
|
118
|
+
|
|
119
|
+
# SageMath parsed files
|
|
120
|
+
*.sage.py
|
|
121
|
+
|
|
122
|
+
# Environments
|
|
123
|
+
.env
|
|
124
|
+
.venv
|
|
125
|
+
env/
|
|
126
|
+
venv/
|
|
127
|
+
ENV/
|
|
128
|
+
env.bak/
|
|
129
|
+
venv.bak/
|
|
130
|
+
|
|
131
|
+
# Spyder project settings
|
|
132
|
+
.spyderproject
|
|
133
|
+
.spyproject
|
|
134
|
+
|
|
135
|
+
# Rope project settings
|
|
136
|
+
.ropeproject
|
|
137
|
+
|
|
138
|
+
# mkdocs documentation
|
|
139
|
+
/site
|
|
140
|
+
|
|
141
|
+
# mypy
|
|
142
|
+
.mypy_cache/
|
|
143
|
+
.dmypy.json
|
|
144
|
+
dmypy.json
|
|
145
|
+
|
|
146
|
+
# Pyre type checker
|
|
147
|
+
.pyre/
|
|
148
|
+
|
|
149
|
+
# pytype static type analyzer
|
|
150
|
+
.pytype/
|
|
151
|
+
|
|
152
|
+
# Cython debug symbols
|
|
153
|
+
cython_debug/
|
|
154
|
+
|
|
155
|
+
# PyCharm
|
|
156
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
157
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
158
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
159
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
160
|
+
#.idea/
|
|
161
|
+
|
|
162
|
+
release.bat
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Ping Data Intelligence
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pingintel-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python-based API for Ping Data Intelligence APIs
|
|
5
|
+
Project-URL: Homepage, https://github.com/pingintel/pingintel-api
|
|
6
|
+
Project-URL: Issues, https://github.com/pingintel/pingintel-api
|
|
7
|
+
Author-email: Scott Stafford <scott@pingintel.com>
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# pingintel-api
|
|
16
|
+
Python-based API for Ping Data Technology products.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from pingintel_api import PingSOVFixerAPIClient
|
|
23
|
+
|
|
24
|
+
PingSOVFixerAPIClient(
|
|
25
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pingintel-api"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Scott Stafford", email="scott@pingintel.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "Python-based API for Ping Data Intelligence APIs"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/pingintel/pingintel-api"
|
|
22
|
+
Issues = "https://github.com/pingintel/pingintel-api"
|
|
23
|
+
|
|
24
|
+
[tool.hatch.version]
|
|
25
|
+
path = "src/pingintel_api/__about__.py"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
sovfixerapi = "pingintel_api.sovfixerapi_cmd:main"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Copyright 2021-2024 Ping Data Intelligence
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
|
|
5
|
+
DEFAULT_TIMEOUT_SECONDS = 5.0
|
|
6
|
+
WAIT_AN_EXTRA_FEW_MINUTES_FOR_AOA_TO_TIMEOUT_IF_I_HAVE_TO = 60.0 * 3
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SOV_STATUS(str, enum.Enum):
|
|
10
|
+
PENDING = "PENDING"
|
|
11
|
+
IN_PROGRESS = "IN_PROGRESS"
|
|
12
|
+
ENRICHING = "ENRICHING"
|
|
13
|
+
REENRICHING = "REENRICHING"
|
|
14
|
+
COMPLETE = "COMPLETE"
|
|
15
|
+
FAILED = "FAILED"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
INCOMPLETE_STATUSES = [
|
|
19
|
+
SOV_STATUS.PENDING,
|
|
20
|
+
SOV_STATUS.IN_PROGRESS,
|
|
21
|
+
SOV_STATUS.ENRICHING,
|
|
22
|
+
SOV_STATUS.REENRICHING,
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SOV_RESULT_STATUS(str, enum.Enum):
|
|
27
|
+
SUCCESS = "SUCCESS"
|
|
28
|
+
FAILED_TO_READ = "FAILED_TO_READ"
|
|
29
|
+
FAILED_TO_PARSE = "FAILED_TO_PARSE"
|
|
30
|
+
FAILED_TO_PROCESS = "FAILED_TO_PROCESS"
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
# Copyright 2021-2024 Ping Data Intelligence
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import pprint
|
|
8
|
+
import time
|
|
9
|
+
from timeit import default_timer as timer
|
|
10
|
+
from typing import overload, IO, TypedDict, NotRequired
|
|
11
|
+
import click
|
|
12
|
+
import requests
|
|
13
|
+
from requests.exceptions import HTTPError
|
|
14
|
+
from . import constants as c
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
global start_time
|
|
20
|
+
start_time = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def log(msg):
|
|
24
|
+
global start_time
|
|
25
|
+
if start_time is None:
|
|
26
|
+
start_time = timer()
|
|
27
|
+
elapsed = timer() - start_time
|
|
28
|
+
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
29
|
+
click.echo(f"[{timestamp} T+{elapsed:.1f}s] {msg}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def raise_for_status(response: requests.Response):
|
|
33
|
+
if response.ok:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
error_msg = response.text
|
|
37
|
+
log(f"{response.status_code} {response.reason}: {error_msg}")
|
|
38
|
+
|
|
39
|
+
raise HTTPError(error_msg, response=response)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_fileobj(source):
|
|
43
|
+
return hasattr(source, "read")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class FixSOVResponseRequest(TypedDict):
|
|
47
|
+
status: c.SOV_STATUS
|
|
48
|
+
requested_at: str
|
|
49
|
+
progress_started_at: str
|
|
50
|
+
completed_at: str | None
|
|
51
|
+
last_health_check_time: str
|
|
52
|
+
last_health_status: str
|
|
53
|
+
pct_complete: int
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FixSOVResponseResultOutput(TypedDict):
|
|
57
|
+
url: str
|
|
58
|
+
description: str
|
|
59
|
+
filename: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FixSOVResponseResult(TypedDict):
|
|
63
|
+
message: str
|
|
64
|
+
status: c.SOV_RESULT_STATUS
|
|
65
|
+
outputs: list[FixSOVResponseResultOutput]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class FixSOVResponse(TypedDict):
|
|
69
|
+
request: FixSOVResponseRequest
|
|
70
|
+
result: NotRequired[FixSOVResponseResult]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SOVFixerAPIClient:
|
|
74
|
+
SOV_STATUS = c.SOV_STATUS
|
|
75
|
+
SOV_RESULT_STATUS = c.SOV_RESULT_STATUS
|
|
76
|
+
|
|
77
|
+
@overload
|
|
78
|
+
def __init__(self, api_url: str, token=None) -> None: ...
|
|
79
|
+
|
|
80
|
+
@overload
|
|
81
|
+
def __init__(self, environment: str = "prod", token=None) -> None: ...
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
api_url: str | None = None,
|
|
86
|
+
environment: str | None = "prod",
|
|
87
|
+
token=None,
|
|
88
|
+
):
|
|
89
|
+
if api_url is None:
|
|
90
|
+
assert environment, "Need either api_url or environment."
|
|
91
|
+
if environment == "prod":
|
|
92
|
+
api_url = "https://api.sovfixer.com"
|
|
93
|
+
elif environment == "prod2":
|
|
94
|
+
api_url = "https://api2.sovfixer.com"
|
|
95
|
+
elif environment == "prodeu":
|
|
96
|
+
api_url = "https://api.eu.sovfixer.com"
|
|
97
|
+
elif environment == "local":
|
|
98
|
+
api_url = "http://api-local.sovfixer.com"
|
|
99
|
+
elif environment == "local2":
|
|
100
|
+
api_url = "http://localhost:8000"
|
|
101
|
+
else:
|
|
102
|
+
api_url = f"https://api-{environment}.sovfixer.com"
|
|
103
|
+
|
|
104
|
+
if token is None:
|
|
105
|
+
if environment in ["staging", "staging2"]:
|
|
106
|
+
serverspace = "stg"
|
|
107
|
+
elif environment in ["prod", "prod2"]:
|
|
108
|
+
serverspace = "prd"
|
|
109
|
+
elif environment in ["prodeu", "prodeu2"]:
|
|
110
|
+
serverspace = "prdeu"
|
|
111
|
+
elif environment in ["dev", "dev2"]:
|
|
112
|
+
serverspace = "dev"
|
|
113
|
+
elif environment in ["local", "local2"]:
|
|
114
|
+
serverspace = "local"
|
|
115
|
+
else:
|
|
116
|
+
raise ValueError("Unknown environment and missing token.")
|
|
117
|
+
token = os.environ.get(f"PING_{serverspace}_AUTH_TOKEN".upper())
|
|
118
|
+
|
|
119
|
+
if token is None:
|
|
120
|
+
token = os.environ.get("SOVFIXER_AUTH_TOKEN")
|
|
121
|
+
if token is None:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
"Need --auth-token or SOVFIXER_AUTH_TOKEN environment variable set."
|
|
124
|
+
)
|
|
125
|
+
assert api_url
|
|
126
|
+
self.api_url = api_url
|
|
127
|
+
self.token = token
|
|
128
|
+
self.headers = {"Authorization": f"Token {token}"}
|
|
129
|
+
self.session = requests.Session()
|
|
130
|
+
self.session.headers = {
|
|
131
|
+
"Authorization": f"Token {self.token}",
|
|
132
|
+
"Accept-Encoding": "gzip",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def fix_sov_async_start(
|
|
136
|
+
self,
|
|
137
|
+
file: IO[bytes] | str,
|
|
138
|
+
document_type,
|
|
139
|
+
filename=None,
|
|
140
|
+
callback_url=None,
|
|
141
|
+
output_formats=None,
|
|
142
|
+
client_ref=None,
|
|
143
|
+
integrations=None,
|
|
144
|
+
delegate_to: str | None = None,
|
|
145
|
+
):
|
|
146
|
+
url = self.api_url + "/api/v1/sov"
|
|
147
|
+
|
|
148
|
+
if is_fileobj(file):
|
|
149
|
+
if filename is None:
|
|
150
|
+
raise ValueError("Need filename if file is a file object.")
|
|
151
|
+
|
|
152
|
+
files = {"file": (filename, file)}
|
|
153
|
+
else:
|
|
154
|
+
if not os.path.exists(file):
|
|
155
|
+
raise click.ClickException(f"Path {file} does not exist.")
|
|
156
|
+
|
|
157
|
+
files = {"file": open(file, "rb")}
|
|
158
|
+
|
|
159
|
+
data = {}
|
|
160
|
+
if callback_url:
|
|
161
|
+
data["callback_url"] = callback_url
|
|
162
|
+
if document_type:
|
|
163
|
+
data["document_type"] = document_type
|
|
164
|
+
if output_formats:
|
|
165
|
+
data["output_formats"] = output_formats
|
|
166
|
+
if client_ref:
|
|
167
|
+
data["client_ref"] = client_ref
|
|
168
|
+
if integrations is not None:
|
|
169
|
+
data["integrations"] = integrations
|
|
170
|
+
if delegate_to is not None:
|
|
171
|
+
data["delegate_to"] = delegate_to
|
|
172
|
+
|
|
173
|
+
response = self.session.post(url, files=files, data=data)
|
|
174
|
+
if response.status_code == 200:
|
|
175
|
+
pprint.pprint(response.json())
|
|
176
|
+
else:
|
|
177
|
+
pprint.pprint(response.text)
|
|
178
|
+
|
|
179
|
+
raise_for_status(response)
|
|
180
|
+
|
|
181
|
+
response_data = response.json()
|
|
182
|
+
sov_id = response_data["id"]
|
|
183
|
+
message = response_data["message"]
|
|
184
|
+
status_url = self.api_url + f"/api/v1/sov/{sov_id}?include_progress=True"
|
|
185
|
+
log(
|
|
186
|
+
f"+ Dispatched {sov_id}: {message}. Now, polling for results at {status_url}."
|
|
187
|
+
)
|
|
188
|
+
return response_data
|
|
189
|
+
|
|
190
|
+
def fix_sov_async_check_progress(self, sovid_or_start_ret) -> FixSOVResponse:
|
|
191
|
+
if isinstance(sovid_or_start_ret, dict):
|
|
192
|
+
sov_id = sovid_or_start_ret["id"]
|
|
193
|
+
else:
|
|
194
|
+
sov_id = sovid_or_start_ret
|
|
195
|
+
|
|
196
|
+
status_url = self.api_url + f"/api/v1/sov/{sov_id}?include_progress=True"
|
|
197
|
+
# params = {"id": sov_id}
|
|
198
|
+
|
|
199
|
+
response = self.session.get(status_url)
|
|
200
|
+
# pprint.pprint(response.json())
|
|
201
|
+
raise_for_status(response)
|
|
202
|
+
|
|
203
|
+
response_data: FixSOVResponse = response.json()
|
|
204
|
+
# request_status = response_data["request"]["status"]
|
|
205
|
+
return response_data
|
|
206
|
+
|
|
207
|
+
def fix_sov_download(
|
|
208
|
+
self, output_ret, actually_write=False, output_path=None, environment=None
|
|
209
|
+
):
|
|
210
|
+
return download_output(
|
|
211
|
+
output_ret,
|
|
212
|
+
session=self.session,
|
|
213
|
+
actually_write=actually_write,
|
|
214
|
+
output_path=output_path,
|
|
215
|
+
environment=environment,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def download_output(
|
|
220
|
+
output,
|
|
221
|
+
environment=None,
|
|
222
|
+
session=None,
|
|
223
|
+
auth_token=None,
|
|
224
|
+
actually_write=False,
|
|
225
|
+
output_path=None,
|
|
226
|
+
):
|
|
227
|
+
if session is None:
|
|
228
|
+
session = requests.Session()
|
|
229
|
+
session.headers = {
|
|
230
|
+
"Authorization": f"Token {auth_token}",
|
|
231
|
+
"Accept-Encoding": "gzip",
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
output_url = output["url"]
|
|
235
|
+
if (
|
|
236
|
+
environment
|
|
237
|
+
and environment == "local2"
|
|
238
|
+
and "api-local.sovfixer.com" in output_url
|
|
239
|
+
):
|
|
240
|
+
output_url = output_url.replace("api-local.sovfixer.com", "localhost:8000")
|
|
241
|
+
|
|
242
|
+
output_description = output["description"]
|
|
243
|
+
output_filename = output["filename"]
|
|
244
|
+
if output_path is None:
|
|
245
|
+
output_path = output_filename
|
|
246
|
+
|
|
247
|
+
log(f"Requesting output from {output_url}...")
|
|
248
|
+
with session.get(output_url, stream=True) as response:
|
|
249
|
+
raise_for_status(response)
|
|
250
|
+
filesize_mb = int(response.headers.get("content-length", 0)) / 1024 / 1024
|
|
251
|
+
pprint.pprint(dict(response.headers))
|
|
252
|
+
log(f" - Streaming {output_description} output ({filesize_mb:.2f} MB)...")
|
|
253
|
+
|
|
254
|
+
if actually_write:
|
|
255
|
+
with open(output_path, "wb") as fd:
|
|
256
|
+
for chunk in response.iter_content(chunk_size=1024 * 1024):
|
|
257
|
+
fd.write(chunk)
|
|
258
|
+
log(f" - Downloaded {output_description} output: {output_path}.")
|
|
259
|
+
return output_path if actually_write else None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def secure_filename(s):
|
|
263
|
+
return s
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def fix_sov(
|
|
267
|
+
API_URL,
|
|
268
|
+
filename,
|
|
269
|
+
document_type,
|
|
270
|
+
auth_token,
|
|
271
|
+
environment=None,
|
|
272
|
+
callback_url=None,
|
|
273
|
+
actually_write=False,
|
|
274
|
+
output_formats=None,
|
|
275
|
+
session=None,
|
|
276
|
+
client_ref=None,
|
|
277
|
+
):
|
|
278
|
+
if auth_token is None:
|
|
279
|
+
auth_token = os.environ.get("SOVFIXER_AUTH_TOKEN")
|
|
280
|
+
if auth_token is None:
|
|
281
|
+
raise click.ClickException(
|
|
282
|
+
"Need --auth-token or SOVFIXER_AUTH_TOKEN environment variable set."
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if not os.path.exists(filename):
|
|
286
|
+
raise click.ClickException("Path does not exist.")
|
|
287
|
+
|
|
288
|
+
sov_fixer_client = SOVFixerAPIClient(API_URL, auth_token)
|
|
289
|
+
start_response = sov_fixer_client.fix_sov_async_start(
|
|
290
|
+
filename,
|
|
291
|
+
document_type=document_type,
|
|
292
|
+
callback_url=callback_url,
|
|
293
|
+
output_formats=output_formats,
|
|
294
|
+
client_ref=client_ref,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
while 1:
|
|
298
|
+
response_data = sov_fixer_client.fix_sov_async_check_progress(start_response)
|
|
299
|
+
# raise_for_status(response_data)
|
|
300
|
+
# pprint.pprint(response_data)
|
|
301
|
+
|
|
302
|
+
request_status = response_data["request"]["status"]
|
|
303
|
+
|
|
304
|
+
POLL_SECS = 2.5
|
|
305
|
+
if request_status == "PENDING":
|
|
306
|
+
log(
|
|
307
|
+
f" - Has not yet been queued for processing, checking progress in {POLL_SECS}s."
|
|
308
|
+
)
|
|
309
|
+
time.sleep(POLL_SECS)
|
|
310
|
+
elif request_status == "IN_PROGRESS":
|
|
311
|
+
log(f" - Still in progress, checking progress in {POLL_SECS}s.")
|
|
312
|
+
time.sleep(POLL_SECS)
|
|
313
|
+
else:
|
|
314
|
+
break
|
|
315
|
+
|
|
316
|
+
result_status = response_data["result"]["status"]
|
|
317
|
+
result_message = response_data["result"]["message"]
|
|
318
|
+
log(f"+ Finished with result {result_status}: {result_message}")
|
|
319
|
+
|
|
320
|
+
if result_status == "SUCCESS":
|
|
321
|
+
log("Complete! Fetching outputs.")
|
|
322
|
+
for output in response_data["result"]["outputs"]:
|
|
323
|
+
output_url = output["url"]
|
|
324
|
+
if (
|
|
325
|
+
environment
|
|
326
|
+
and environment == "local2"
|
|
327
|
+
and "api-local.sovfixer.com" in output_url
|
|
328
|
+
):
|
|
329
|
+
output_url = output_url.replace(
|
|
330
|
+
"api-local.sovfixer.com", "localhost:8000"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
output_filename = output["filename"]
|
|
334
|
+
|
|
335
|
+
output_path = output_filename
|
|
336
|
+
|
|
337
|
+
if actually_write:
|
|
338
|
+
if os.path.exists(output_path):
|
|
339
|
+
yesno = input(
|
|
340
|
+
f"Do you want to overwrite the existing file {output_path} [y/N]? "
|
|
341
|
+
)
|
|
342
|
+
if yesno.lower() != "y":
|
|
343
|
+
continue
|
|
344
|
+
|
|
345
|
+
response = sov_fixer_client.fix_sov_download(
|
|
346
|
+
output,
|
|
347
|
+
actually_write=actually_write,
|
|
348
|
+
output_path=output_path,
|
|
349
|
+
environment=environment,
|
|
350
|
+
)
|
|
351
|
+
raise_for_status(response)
|
|
352
|
+
return True
|
|
353
|
+
else:
|
|
354
|
+
log("* Parsing failed! Raw API output:")
|
|
355
|
+
log(response_data)
|
|
356
|
+
return False
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
# Copyright 2021-2024 Ping Data Intelligence
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import enum
|
|
7
|
+
import gzip
|
|
8
|
+
import hashlib
|
|
9
|
+
import io
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import pathlib
|
|
14
|
+
import pprint
|
|
15
|
+
import random
|
|
16
|
+
import time
|
|
17
|
+
import zipfile
|
|
18
|
+
from timeit import default_timer as timer
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
import requests
|
|
22
|
+
from requests.exceptions import HTTPError
|
|
23
|
+
|
|
24
|
+
from pingintel_api.sov_fixer_api_client import fix_sov
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
global start_time
|
|
30
|
+
start_time = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
sovfixerapi.py
|
|
35
|
+
|
|
36
|
+
Example Python commandline script for using the Ping Data Technologies sovfixer API to process SOVs.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def log(msg):
|
|
41
|
+
global start_time
|
|
42
|
+
if start_time is None:
|
|
43
|
+
start_time = timer()
|
|
44
|
+
elapsed = timer() - start_time
|
|
45
|
+
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
46
|
+
click.echo(f"[{timestamp} T+{elapsed:.1f}s] {msg}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@click.command()
|
|
50
|
+
@click.argument(
|
|
51
|
+
"filename", nargs=-1, required=True, type=click.Path(exists=True, dir_okay=False)
|
|
52
|
+
)
|
|
53
|
+
@click.option(
|
|
54
|
+
"-e",
|
|
55
|
+
"--environment",
|
|
56
|
+
type=click.Choice(
|
|
57
|
+
[
|
|
58
|
+
"staging",
|
|
59
|
+
"staging2",
|
|
60
|
+
"prod",
|
|
61
|
+
"prod2",
|
|
62
|
+
"prodeu",
|
|
63
|
+
"prodeu2",
|
|
64
|
+
"local",
|
|
65
|
+
"local2",
|
|
66
|
+
"dev",
|
|
67
|
+
"dev2",
|
|
68
|
+
],
|
|
69
|
+
case_sensitive=False,
|
|
70
|
+
),
|
|
71
|
+
default="staging",
|
|
72
|
+
)
|
|
73
|
+
@click.option(
|
|
74
|
+
"-d",
|
|
75
|
+
"--document-type",
|
|
76
|
+
type=click.Choice(
|
|
77
|
+
["SOV", "PREM_BDX", "CLAIM_BDX", "SOV_BDX", "ACORD"], case_sensitive=False
|
|
78
|
+
),
|
|
79
|
+
default="SOV",
|
|
80
|
+
help="Identify `filename` document type. Defaults to SOV.",
|
|
81
|
+
)
|
|
82
|
+
@click.option(
|
|
83
|
+
"--auth-token",
|
|
84
|
+
help="Provide auth token via --auth-token or SOVFIXER_AUTH_TOKEN environment variable.",
|
|
85
|
+
)
|
|
86
|
+
@click.option(
|
|
87
|
+
"--callback-url", help="(Optional) Provide a URL to which results should be POSTed."
|
|
88
|
+
)
|
|
89
|
+
@click.option(
|
|
90
|
+
"-o",
|
|
91
|
+
"--output-format",
|
|
92
|
+
multiple=True,
|
|
93
|
+
help="Select output format.",
|
|
94
|
+
)
|
|
95
|
+
@click.option("--client-ref")
|
|
96
|
+
@click.option(
|
|
97
|
+
"--write",
|
|
98
|
+
"--no-write",
|
|
99
|
+
is_flag=True,
|
|
100
|
+
default=False,
|
|
101
|
+
help="If set, actually write the output. Otherwise, download as a test but do not write.",
|
|
102
|
+
)
|
|
103
|
+
def main(
|
|
104
|
+
filename,
|
|
105
|
+
environment,
|
|
106
|
+
document_type,
|
|
107
|
+
auth_token,
|
|
108
|
+
callback_url,
|
|
109
|
+
output_format,
|
|
110
|
+
client_ref,
|
|
111
|
+
write,
|
|
112
|
+
):
|
|
113
|
+
if environment == "prod":
|
|
114
|
+
API_URL = "https://api.sovfixer.com"
|
|
115
|
+
elif environment == "prod2":
|
|
116
|
+
API_URL = "https://api2.sovfixer.com"
|
|
117
|
+
elif environment == "prodeu":
|
|
118
|
+
API_URL = "https://api.eu.sovfixer.com"
|
|
119
|
+
elif environment == "local":
|
|
120
|
+
API_URL = "http://api-local.sovfixer.com"
|
|
121
|
+
elif environment == "local2":
|
|
122
|
+
API_URL = "http://localhost:8000"
|
|
123
|
+
else:
|
|
124
|
+
API_URL = f"https://api-{environment}.sovfixer.com"
|
|
125
|
+
|
|
126
|
+
if auth_token is None:
|
|
127
|
+
if environment in ["staging", "staging2"]:
|
|
128
|
+
serverspace = "stg"
|
|
129
|
+
elif environment in ["prod", "prod2"]:
|
|
130
|
+
serverspace = "prd"
|
|
131
|
+
elif environment in ["prodeu", "prodeu2"]:
|
|
132
|
+
serverspace = "prdeu"
|
|
133
|
+
elif environment in ["dev", "dev2"]:
|
|
134
|
+
serverspace = "dev"
|
|
135
|
+
elif environment in ["local", "local2"]:
|
|
136
|
+
serverspace = "local"
|
|
137
|
+
else:
|
|
138
|
+
raise NotImplementedError()
|
|
139
|
+
auth_token = os.environ.get(f"PING_{serverspace}_AUTH_TOKEN".upper())
|
|
140
|
+
|
|
141
|
+
if isinstance(filename, pathlib.PosixPath):
|
|
142
|
+
filename = [str(filename)]
|
|
143
|
+
|
|
144
|
+
for fn in filename:
|
|
145
|
+
fix_sov(
|
|
146
|
+
API_URL,
|
|
147
|
+
fn,
|
|
148
|
+
document_type,
|
|
149
|
+
auth_token,
|
|
150
|
+
environment,
|
|
151
|
+
callback_url,
|
|
152
|
+
actually_write=write,
|
|
153
|
+
output_formats=output_format,
|
|
154
|
+
client_ref=client_ref,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
main()
|
|
File without changes
|