dgc-core 1.0.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.
- dgc_core-1.0.0/MANIFEST.in +2 -0
- dgc_core-1.0.0/Makefile +33 -0
- dgc_core-1.0.0/PKG-INFO +118 -0
- dgc_core-1.0.0/README.md +91 -0
- dgc_core-1.0.0/dgc_core/__init__.py +82 -0
- dgc_core-1.0.0/dgc_core/auth.py +35 -0
- dgc_core-1.0.0/dgc_core/constants/__init__.py +0 -0
- dgc_core-1.0.0/dgc_core/constants/card.py +14 -0
- dgc_core-1.0.0/dgc_core/enums/__init__.py +0 -0
- dgc_core-1.0.0/dgc_core/enums/card.py +93 -0
- dgc_core-1.0.0/dgc_core/enums/submission.py +37 -0
- dgc_core-1.0.0/dgc_core/enums/user.py +8 -0
- dgc_core-1.0.0/dgc_core/env.py +35 -0
- dgc_core-1.0.0/dgc_core/fetch.py +124 -0
- dgc_core-1.0.0/dgc_core/logger.py +33 -0
- dgc_core-1.0.0/dgc_core/merge.py +235 -0
- dgc_core-1.0.0/dgc_core/mongo_cache.py +113 -0
- dgc_core-1.0.0/dgc_core/normalize.py +27 -0
- dgc_core-1.0.0/dgc_core/pricecharting.py +65 -0
- dgc_core-1.0.0/dgc_core/pub.py +41 -0
- dgc_core-1.0.0/dgc_core/regex.py +17 -0
- dgc_core-1.0.0/dgc_core/task.py +123 -0
- dgc_core-1.0.0/dgc_core.egg-info/PKG-INFO +118 -0
- dgc_core-1.0.0/dgc_core.egg-info/SOURCES.txt +44 -0
- dgc_core-1.0.0/dgc_core.egg-info/dependency_links.txt +1 -0
- dgc_core-1.0.0/dgc_core.egg-info/requires.txt +18 -0
- dgc_core-1.0.0/dgc_core.egg-info/top_level.txt +1 -0
- dgc_core-1.0.0/pyproject.toml +52 -0
- dgc_core-1.0.0/requirements-dev.txt +1 -0
- dgc_core-1.0.0/requirements.txt +1 -0
- dgc_core-1.0.0/scripts/release.py +67 -0
- dgc_core-1.0.0/setup.cfg +4 -0
- dgc_core-1.0.0/tests/test_auth.py +79 -0
- dgc_core-1.0.0/tests/test_env.py +44 -0
- dgc_core-1.0.0/tests/test_fetch.py +136 -0
- dgc_core-1.0.0/tests/test_logger.py +94 -0
- dgc_core-1.0.0/tests/test_merge.py +500 -0
- dgc_core-1.0.0/tests/test_mongo_cache.py +332 -0
- dgc_core-1.0.0/tests/test_normalize.py +74 -0
- dgc_core-1.0.0/tests/test_pricecharting.py +230 -0
- dgc_core-1.0.0/tests/test_pub.py +101 -0
- dgc_core-1.0.0/tests/test_public_api.py +12 -0
- dgc_core-1.0.0/tests/test_regex.py +64 -0
- dgc_core-1.0.0/tests/test_release.py +149 -0
- dgc_core-1.0.0/tests/test_task.py +178 -0
- dgc_core-1.0.0/tests/test_types_contract.py +218 -0
dgc_core-1.0.0/Makefile
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
VENV ?= venv
|
|
2
|
+
PYTHON ?= python
|
|
3
|
+
|
|
4
|
+
ifeq ($(OS),Windows_NT)
|
|
5
|
+
VENV_PYTHON := $(VENV)/Scripts/python.exe
|
|
6
|
+
else
|
|
7
|
+
VENV_PYTHON := $(VENV)/bin/python
|
|
8
|
+
endif
|
|
9
|
+
|
|
10
|
+
.PHONY: venv test coverage-html build check publish bump-patch
|
|
11
|
+
|
|
12
|
+
venv:
|
|
13
|
+
$(PYTHON) -m venv $(VENV)
|
|
14
|
+
$(VENV_PYTHON) -m pip install -r requirements-dev.txt
|
|
15
|
+
|
|
16
|
+
test: venv
|
|
17
|
+
$(VENV_PYTHON) -m pytest --cov=dgc_core --cov-report=term-missing
|
|
18
|
+
|
|
19
|
+
coverage-html: test
|
|
20
|
+
$(VENV_PYTHON) -m coverage html
|
|
21
|
+
|
|
22
|
+
build: venv
|
|
23
|
+
$(PYTHON) scripts/release.py clean-build
|
|
24
|
+
$(VENV_PYTHON) -m build
|
|
25
|
+
|
|
26
|
+
check: build
|
|
27
|
+
$(VENV_PYTHON) -m twine check dist/*
|
|
28
|
+
|
|
29
|
+
publish: check
|
|
30
|
+
$(VENV_PYTHON) -m twine upload dist/*
|
|
31
|
+
|
|
32
|
+
bump-patch:
|
|
33
|
+
$(PYTHON) scripts/release.py bump-patch
|
dgc_core-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dgc-core
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Shared Python utilities and domain types for DGC services
|
|
5
|
+
Author-email: velopace <velopace7@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/velopace739/dgc-core-python
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: functions-framework>=3.10.1
|
|
11
|
+
Requires-Dist: flask>=3.1.3
|
|
12
|
+
Requires-Dist: firebase-admin>=7.4.0
|
|
13
|
+
Requires-Dist: google-api-core>=2.30.3
|
|
14
|
+
Requires-Dist: google-auth>=2.49.2
|
|
15
|
+
Requires-Dist: google-auth-oauthlib>=1.3.1
|
|
16
|
+
Requires-Dist: google-cloud-pubsub>=2.37.0
|
|
17
|
+
Requires-Dist: google-cloud-tasks>=2.22.0
|
|
18
|
+
Requires-Dist: protobuf>=7.34.1
|
|
19
|
+
Requires-Dist: pydantic>=2.13.3
|
|
20
|
+
Requires-Dist: pymongo>=4.17.0
|
|
21
|
+
Requires-Dist: requests>=2.32.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build>=1.3.0; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest>=8.4.1; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-cov>=6.2.1; extra == "dev"
|
|
26
|
+
Requires-Dist: twine>=6.1.0; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# dgc-core
|
|
29
|
+
|
|
30
|
+
Shared Python utilities and domain types for DGC services. The distribution is
|
|
31
|
+
named `dgc-core`; the Python package is `dgc_core`. Requires Python 3.10 or later.
|
|
32
|
+
|
|
33
|
+
This repository combines `opticedge-cloud-utils-python` (`opticedge_cloud_utils`
|
|
34
|
+
1.0.33) and `opticedge-types-python` (`opticedge_types` 1.0.53). Existing utility
|
|
35
|
+
behavior, public exports, enum values, and eligibility constants are preserved.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
Install from this checkout during development:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
python -m pip install -e .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
After version 1.0.0 is published to your package index, consumers can use
|
|
46
|
+
`dgc-core==1.0.0` in their requirements.
|
|
47
|
+
|
|
48
|
+
## Imports
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from dgc_core import ApprovalStatus, GradingType, LogLevel, MongoCache, log, require_env
|
|
52
|
+
from dgc_core.pub import publish_message
|
|
53
|
+
from dgc_core.enums.card import CardCategory
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Utilities, enums, and constants are exported together from `dgc_core`. Utility
|
|
57
|
+
modules live directly in the package, alongside `enums/` and `constants/`.
|
|
58
|
+
Helpers cover authentication, configuration, HTTP fetching, logging, merging,
|
|
59
|
+
MongoDB cache, normalization, PriceCharting parsing, Pub/Sub, regex, and Cloud
|
|
60
|
+
Tasks. Domain definitions cover cards, users, submissions, and grading eligibility.
|
|
61
|
+
|
|
62
|
+
## Migrating existing consumers
|
|
63
|
+
|
|
64
|
+
Replace the `opticedge-cloud-utils` and `opticedge-types` requirements with the
|
|
65
|
+
single `dgc-core` requirement after publication, and update imports to `dgc_core`:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
python -m pip install dgc-core==1.0.0
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| Previous import | New import |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `from opticedge_cloud_utils import log` | `from dgc_core import log` |
|
|
74
|
+
| `from opticedge_types import GradingType` | `from dgc_core import GradingType` |
|
|
75
|
+
| `from opticedge_cloud_utils.pub import publish_message` | `from dgc_core.pub import publish_message` |
|
|
76
|
+
| `from opticedge_types.enums.card import GradingType` | `from dgc_core.enums.card import GradingType` |
|
|
77
|
+
| `from opticedge_types.constants.card import ON_DEMAND_ELIGIBLE_GRADING_TYPES` | `from dgc_core.constants.card import ON_DEMAND_ELIGIBLE_GRADING_TYPES` |
|
|
78
|
+
|
|
79
|
+
Update mock and patch paths in consumer tests using the same mapping. The
|
|
80
|
+
distribution provides only the `dgc_core` package.
|
|
81
|
+
|
|
82
|
+
Service requirements are not changed by this repository merge. Services pinned
|
|
83
|
+
to older releases should run their own tests when adopting the combined release.
|
|
84
|
+
|
|
85
|
+
## Development and verification
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
make test
|
|
89
|
+
make check
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Without Make, create a virtual environment and activate it, then run:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
python -m pip install -r requirements-dev.txt
|
|
96
|
+
python -m pytest --cov=dgc_core --cov-report=term-missing
|
|
97
|
+
python -m build
|
|
98
|
+
python -m twine check dist/*
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The tests mock external calls. `make build` clears old `build/` and `dist/` output
|
|
102
|
+
and produces a wheel and source distribution for the current version.
|
|
103
|
+
|
|
104
|
+
## Releasing
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
make bump-patch
|
|
108
|
+
make publish
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`make bump-patch` increments `[project].version` in `pyproject.toml` (for example,
|
|
112
|
+
`1.0.0` to `1.0.1`) and preserves the rest of the file's formatting. It uses the
|
|
113
|
+
base Python interpreter and works before creating the virtual environment.
|
|
114
|
+
|
|
115
|
+
`make publish` builds fresh artifacts, checks them with Twine, and uploads them
|
|
116
|
+
to your configured package index (PyPI by default), using your existing Twine
|
|
117
|
+
credentials or configuration. Publishing uses the current version; it does not
|
|
118
|
+
bump the version automatically.
|
dgc_core-1.0.0/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# dgc-core
|
|
2
|
+
|
|
3
|
+
Shared Python utilities and domain types for DGC services. The distribution is
|
|
4
|
+
named `dgc-core`; the Python package is `dgc_core`. Requires Python 3.10 or later.
|
|
5
|
+
|
|
6
|
+
This repository combines `opticedge-cloud-utils-python` (`opticedge_cloud_utils`
|
|
7
|
+
1.0.33) and `opticedge-types-python` (`opticedge_types` 1.0.53). Existing utility
|
|
8
|
+
behavior, public exports, enum values, and eligibility constants are preserved.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Install from this checkout during development:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
python -m pip install -e .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
After version 1.0.0 is published to your package index, consumers can use
|
|
19
|
+
`dgc-core==1.0.0` in their requirements.
|
|
20
|
+
|
|
21
|
+
## Imports
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from dgc_core import ApprovalStatus, GradingType, LogLevel, MongoCache, log, require_env
|
|
25
|
+
from dgc_core.pub import publish_message
|
|
26
|
+
from dgc_core.enums.card import CardCategory
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Utilities, enums, and constants are exported together from `dgc_core`. Utility
|
|
30
|
+
modules live directly in the package, alongside `enums/` and `constants/`.
|
|
31
|
+
Helpers cover authentication, configuration, HTTP fetching, logging, merging,
|
|
32
|
+
MongoDB cache, normalization, PriceCharting parsing, Pub/Sub, regex, and Cloud
|
|
33
|
+
Tasks. Domain definitions cover cards, users, submissions, and grading eligibility.
|
|
34
|
+
|
|
35
|
+
## Migrating existing consumers
|
|
36
|
+
|
|
37
|
+
Replace the `opticedge-cloud-utils` and `opticedge-types` requirements with the
|
|
38
|
+
single `dgc-core` requirement after publication, and update imports to `dgc_core`:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
python -m pip install dgc-core==1.0.0
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
| Previous import | New import |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `from opticedge_cloud_utils import log` | `from dgc_core import log` |
|
|
47
|
+
| `from opticedge_types import GradingType` | `from dgc_core import GradingType` |
|
|
48
|
+
| `from opticedge_cloud_utils.pub import publish_message` | `from dgc_core.pub import publish_message` |
|
|
49
|
+
| `from opticedge_types.enums.card import GradingType` | `from dgc_core.enums.card import GradingType` |
|
|
50
|
+
| `from opticedge_types.constants.card import ON_DEMAND_ELIGIBLE_GRADING_TYPES` | `from dgc_core.constants.card import ON_DEMAND_ELIGIBLE_GRADING_TYPES` |
|
|
51
|
+
|
|
52
|
+
Update mock and patch paths in consumer tests using the same mapping. The
|
|
53
|
+
distribution provides only the `dgc_core` package.
|
|
54
|
+
|
|
55
|
+
Service requirements are not changed by this repository merge. Services pinned
|
|
56
|
+
to older releases should run their own tests when adopting the combined release.
|
|
57
|
+
|
|
58
|
+
## Development and verification
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
make test
|
|
62
|
+
make check
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Without Make, create a virtual environment and activate it, then run:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
python -m pip install -r requirements-dev.txt
|
|
69
|
+
python -m pytest --cov=dgc_core --cov-report=term-missing
|
|
70
|
+
python -m build
|
|
71
|
+
python -m twine check dist/*
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The tests mock external calls. `make build` clears old `build/` and `dist/` output
|
|
75
|
+
and produces a wheel and source distribution for the current version.
|
|
76
|
+
|
|
77
|
+
## Releasing
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
make bump-patch
|
|
81
|
+
make publish
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`make bump-patch` increments `[project].version` in `pyproject.toml` (for example,
|
|
85
|
+
`1.0.0` to `1.0.1`) and preserves the rest of the file's formatting. It uses the
|
|
86
|
+
base Python interpreter and works before creating the virtual environment.
|
|
87
|
+
|
|
88
|
+
`make publish` builds fresh artifacts, checks them with Twine, and uploads them
|
|
89
|
+
to your configured package index (PyPI by default), using your existing Twine
|
|
90
|
+
credentials or configuration. Publishing uses the current version; it does not
|
|
91
|
+
bump the version automatically.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Shared utilities, enums, and constants for DGC services."""
|
|
2
|
+
|
|
3
|
+
from .auth import verify_request
|
|
4
|
+
from .env import require_env
|
|
5
|
+
from .fetch import fetch_url, fetch_image
|
|
6
|
+
from .logger import log, LogLevel
|
|
7
|
+
from .merge import (
|
|
8
|
+
deep_merge,
|
|
9
|
+
detect_field_changes,
|
|
10
|
+
safe_any_changed,
|
|
11
|
+
prune_conflicting_set_unset,
|
|
12
|
+
)
|
|
13
|
+
from .mongo_cache import MongoCache
|
|
14
|
+
from .normalize import normalize_enum, normalize_object_id
|
|
15
|
+
from .pricecharting import split_name_and_number
|
|
16
|
+
from .pub import publish_message
|
|
17
|
+
from .regex import escape_for_regex
|
|
18
|
+
from .task import create_task
|
|
19
|
+
from .constants.card import (
|
|
20
|
+
ON_DEMAND_ELIGIBLE_GRADING_TYPES,
|
|
21
|
+
POST_GRADING_ELIGIBLE_GRADING_TYPES,
|
|
22
|
+
)
|
|
23
|
+
from .enums.card import (
|
|
24
|
+
ApprovalStatus,
|
|
25
|
+
RejectType,
|
|
26
|
+
RescanType,
|
|
27
|
+
GradingType,
|
|
28
|
+
InstantGradeType,
|
|
29
|
+
GradingField,
|
|
30
|
+
CardType,
|
|
31
|
+
TradingCardType,
|
|
32
|
+
SportsCardType,
|
|
33
|
+
CardCategory,
|
|
34
|
+
CardVersions,
|
|
35
|
+
)
|
|
36
|
+
from .enums.user import CollectorType
|
|
37
|
+
from .enums.submission import (
|
|
38
|
+
SubmissionStatus,
|
|
39
|
+
SubmissionSessionStatus,
|
|
40
|
+
SubmissionGradingLevel,
|
|
41
|
+
ReturnedVerificationStatus,
|
|
42
|
+
ReturnCreditSessionStatus,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"verify_request",
|
|
47
|
+
"require_env",
|
|
48
|
+
"fetch_url",
|
|
49
|
+
"fetch_image",
|
|
50
|
+
"log",
|
|
51
|
+
"LogLevel",
|
|
52
|
+
"deep_merge",
|
|
53
|
+
"detect_field_changes",
|
|
54
|
+
"safe_any_changed",
|
|
55
|
+
"prune_conflicting_set_unset",
|
|
56
|
+
"MongoCache",
|
|
57
|
+
"normalize_enum",
|
|
58
|
+
"normalize_object_id",
|
|
59
|
+
"split_name_and_number",
|
|
60
|
+
"publish_message",
|
|
61
|
+
"escape_for_regex",
|
|
62
|
+
"create_task",
|
|
63
|
+
"ON_DEMAND_ELIGIBLE_GRADING_TYPES",
|
|
64
|
+
"POST_GRADING_ELIGIBLE_GRADING_TYPES",
|
|
65
|
+
"ApprovalStatus",
|
|
66
|
+
"RejectType",
|
|
67
|
+
"RescanType",
|
|
68
|
+
"GradingType",
|
|
69
|
+
"InstantGradeType",
|
|
70
|
+
"GradingField",
|
|
71
|
+
"CardType",
|
|
72
|
+
"TradingCardType",
|
|
73
|
+
"SportsCardType",
|
|
74
|
+
"CollectorType",
|
|
75
|
+
"CardCategory",
|
|
76
|
+
"CardVersions",
|
|
77
|
+
"SubmissionStatus",
|
|
78
|
+
"SubmissionSessionStatus",
|
|
79
|
+
"SubmissionGradingLevel",
|
|
80
|
+
"ReturnedVerificationStatus",
|
|
81
|
+
"ReturnCreditSessionStatus",
|
|
82
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import google.auth.transport.requests
|
|
2
|
+
import google.oauth2.id_token
|
|
3
|
+
|
|
4
|
+
def verify_request(request, allowed_service_account: str, allowed_audience: str) -> bool:
|
|
5
|
+
"""
|
|
6
|
+
Verifies that an incoming request is from a trusted source (e.g., Cloud Tasks)
|
|
7
|
+
using ID tokens.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
request: A Flask/Django-like request object with headers.
|
|
11
|
+
allowed_service_account: The expected service account email.
|
|
12
|
+
allowed_audience: The expected audience.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
bool: True if the request is verified, False otherwise.
|
|
16
|
+
"""
|
|
17
|
+
auth_header = request.headers.get("Authorization")
|
|
18
|
+
if not auth_header or not auth_header.startswith("Bearer "):
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
token = auth_header.split(" ")[1]
|
|
22
|
+
request_adapter = google.auth.transport.requests.Request()
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
decoded_token = google.oauth2.id_token.verify_oauth2_token(
|
|
26
|
+
token,
|
|
27
|
+
request_adapter,
|
|
28
|
+
audience=allowed_audience
|
|
29
|
+
)
|
|
30
|
+
if decoded_token.get("email") != allowed_service_account:
|
|
31
|
+
return False
|
|
32
|
+
return True
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"Auth error: {e}")
|
|
35
|
+
return False
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from dgc_core.enums.card import GradingType
|
|
2
|
+
|
|
3
|
+
ON_DEMAND_ELIGIBLE_GRADING_TYPES = {
|
|
4
|
+
GradingType.DEEP_2024,
|
|
5
|
+
GradingType.DEEP,
|
|
6
|
+
GradingType.INSTANT,
|
|
7
|
+
GradingType.DEEP_INSTANT
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
POST_GRADING_ELIGIBLE_GRADING_TYPES = {
|
|
11
|
+
GradingType.DEEP,
|
|
12
|
+
GradingType.INSTANT,
|
|
13
|
+
GradingType.DEEP_INSTANT
|
|
14
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from enum import IntEnum, Enum
|
|
2
|
+
from typing import TypeAlias
|
|
3
|
+
|
|
4
|
+
class ApprovalStatus(IntEnum):
|
|
5
|
+
RESCAN = -2
|
|
6
|
+
PENDING = -1
|
|
7
|
+
FAILED = 0
|
|
8
|
+
APPROVED = 1
|
|
9
|
+
PENDING_INSTANT = 3
|
|
10
|
+
APPROVING = 999
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RejectType(Enum):
|
|
14
|
+
INVALID_CARD = "Invalid card"
|
|
15
|
+
FAKE_HIGHRISK = "Fake - High Risk"
|
|
16
|
+
REPEAT = "Repeat"
|
|
17
|
+
MODEL_FAILURE = "Model failure"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RescanType(Enum):
|
|
21
|
+
ENVIRONMENT_LIGHTING = "Environment - Lighting",
|
|
22
|
+
ENVIRONMENT_ARTEFACTS = "Environment - Artefacts",
|
|
23
|
+
CARD_IN_SLAB_SLEEVE = "Card in Slab/Sleeve",
|
|
24
|
+
CARD_OBSTRUCTION = "Card - Obstruction",
|
|
25
|
+
CARD_ARTIFACTS = "CARD-ARTIFACTS",
|
|
26
|
+
SCAN_PROCESS_FAILURE = "Scan Process Failure"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GradingType(IntEnum):
|
|
30
|
+
DEEP_2024 = 0
|
|
31
|
+
DEEP = 1
|
|
32
|
+
ADDED = 2
|
|
33
|
+
SOLD = 3
|
|
34
|
+
INSTANT = 4
|
|
35
|
+
DEEP_INSTANT = 5
|
|
36
|
+
CARD_CENTERING = 6
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class InstantGradeType(IntEnum):
|
|
40
|
+
OVERALL = 0
|
|
41
|
+
SUBGRADES = 1
|
|
42
|
+
OVERALL_TO_SUBGRADES = 2
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class GradingField(Enum):
|
|
46
|
+
GRADING = "grading"
|
|
47
|
+
CENTER = "centering_grading"
|
|
48
|
+
SURFACE = "surface_grading"
|
|
49
|
+
EDGE = "edge_grading"
|
|
50
|
+
CORNER = "corner_grading"
|
|
51
|
+
ENVIRONMENT = "grading_environment"
|
|
52
|
+
CONFIDENCE = "grading_confidence"
|
|
53
|
+
STRUCTURE = "card_structure"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TradingCardType(str, Enum):
|
|
57
|
+
POKEMON = "pokemon"
|
|
58
|
+
LORCANA = "lorcana"
|
|
59
|
+
MAGIC = "magic"
|
|
60
|
+
YUGIOH = "yugioh"
|
|
61
|
+
DIGIMON = "digimon"
|
|
62
|
+
DRAGON_BALL = "dragon_ball"
|
|
63
|
+
ONE_PIECE = "one_piece"
|
|
64
|
+
STAR_WARS = 'star_wars'
|
|
65
|
+
MARVEL = 'marvel'
|
|
66
|
+
GARBAGE_PAIL = 'garbage_pail'
|
|
67
|
+
OTHER_TCG = 'other_tcg'
|
|
68
|
+
OTHER_NON_TCG = 'other'
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class SportsCardType(str, Enum):
|
|
72
|
+
BASEBALL = "baseball"
|
|
73
|
+
BASKETBALL = "basketball"
|
|
74
|
+
FOOTBALL = "football"
|
|
75
|
+
HOCKEY = "hockey"
|
|
76
|
+
SOCCER = "soccer"
|
|
77
|
+
RACING = "racing"
|
|
78
|
+
WRESTLING = "wrestling"
|
|
79
|
+
UFC = "ufc"
|
|
80
|
+
BOXING = "boxing"
|
|
81
|
+
GOLF = "golf"
|
|
82
|
+
TENNIS = "tennis"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
CardType: TypeAlias = TradingCardType | SportsCardType
|
|
86
|
+
|
|
87
|
+
class CardCategory(str, Enum):
|
|
88
|
+
TRADING = "trading"
|
|
89
|
+
SPORTS = "sports"
|
|
90
|
+
|
|
91
|
+
class CardVersions(str, Enum):
|
|
92
|
+
DEFAULT = '0.0',
|
|
93
|
+
VERSION_1_0 = '1.0'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from enum import IntEnum, Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SubmissionStatus(IntEnum):
|
|
5
|
+
READY = 0
|
|
6
|
+
PROGRESS = 1
|
|
7
|
+
LEVEL = 2
|
|
8
|
+
VERIFY = 3
|
|
9
|
+
SUBMITTED = 4
|
|
10
|
+
RETURNED = 5
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SubmissionSessionStatus(str, Enum):
|
|
14
|
+
PENDING="pending"
|
|
15
|
+
PROCESSING="processing"
|
|
16
|
+
RETURNED="returned"
|
|
17
|
+
FAILED="failed"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SubmissionGradingLevel(IntEnum):
|
|
21
|
+
VALUE_BULK=0
|
|
22
|
+
VALUE=1
|
|
23
|
+
VALUE_PLUS=2
|
|
24
|
+
REGULAR=3
|
|
25
|
+
EXPRESS=4
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ReturnedVerificationStatus(IntEnum):
|
|
29
|
+
PENDING = -1
|
|
30
|
+
FAILED = 0
|
|
31
|
+
VERIFIED = 1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ReturnCreditSessionStatus(str, Enum):
|
|
35
|
+
PENDING="pending"
|
|
36
|
+
SUCCESS="success"
|
|
37
|
+
FAILED="failed"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# dgc_core/env.py
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def require_env(name: str, default: Optional[str] = None) -> str:
|
|
8
|
+
"""
|
|
9
|
+
Retrieve an environment variable.
|
|
10
|
+
|
|
11
|
+
Rules:
|
|
12
|
+
- If variable exists and is a non-empty string → return it.
|
|
13
|
+
- If missing or empty and default provided → return default.
|
|
14
|
+
- If missing or empty and no default → raise RuntimeError.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
name: Environment variable name.
|
|
18
|
+
default: Optional fallback value.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
str
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
RuntimeError
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
value = os.getenv(name)
|
|
28
|
+
|
|
29
|
+
# Treat both None and "" as invalid/missing
|
|
30
|
+
if value is None or value == "":
|
|
31
|
+
if default is not None:
|
|
32
|
+
return default
|
|
33
|
+
raise RuntimeError(f"{name} env var must be set")
|
|
34
|
+
|
|
35
|
+
return value
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# dgc_core/fetch.py (updated)
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal, Optional
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import requests
|
|
6
|
+
import random
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
DEFAULT_RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}
|
|
10
|
+
DEFAULT_USER_AGENT = (
|
|
11
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
12
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
13
|
+
"Chrome/124.0.0.0 Safari/537.36"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ImageResponse:
|
|
18
|
+
content: bytes
|
|
19
|
+
content_type: str
|
|
20
|
+
content_length: int | None
|
|
21
|
+
status_code: int
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class URLResponse:
|
|
25
|
+
status_code: int
|
|
26
|
+
headers: dict
|
|
27
|
+
content: Any
|
|
28
|
+
|
|
29
|
+
def fetch_url(
|
|
30
|
+
url: str,
|
|
31
|
+
params: Optional[dict] = None,
|
|
32
|
+
retries: int = 4,
|
|
33
|
+
timeout: int = 20,
|
|
34
|
+
extra_retryable_status_codes: set[int] | None = None,
|
|
35
|
+
user_agent: str = DEFAULT_USER_AGENT,
|
|
36
|
+
base_delay: float = 0.5,
|
|
37
|
+
max_delay: float = 8.0,
|
|
38
|
+
response_type: Literal["text", "json"] = "text",
|
|
39
|
+
) -> URLResponse:
|
|
40
|
+
retryable_status_codes = set(DEFAULT_RETRYABLE_STATUS_CODES)
|
|
41
|
+
if extra_retryable_status_codes:
|
|
42
|
+
retryable_status_codes.update(extra_retryable_status_codes)
|
|
43
|
+
|
|
44
|
+
headers = {
|
|
45
|
+
"User-Agent": user_agent,
|
|
46
|
+
"Accept": (
|
|
47
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,"
|
|
48
|
+
"image/avif,image/webp,*/*;q=0.8"
|
|
49
|
+
),
|
|
50
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
51
|
+
"Referer": "https://www.google.com/",
|
|
52
|
+
"Connection": "keep-alive",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
with requests.Session() as session:
|
|
56
|
+
for attempt in range(retries + 1):
|
|
57
|
+
try:
|
|
58
|
+
response = session.get(url, headers=headers, timeout=timeout, params=params)
|
|
59
|
+
if response.status_code in retryable_status_codes:
|
|
60
|
+
raise requests.HTTPError(f"retryable status {response.status_code}", response=response)
|
|
61
|
+
response.raise_for_status()
|
|
62
|
+
|
|
63
|
+
content = response.json() if response_type == "json" else response.text
|
|
64
|
+
return URLResponse(status_code=response.status_code, headers=dict(response.headers), content=content)
|
|
65
|
+
|
|
66
|
+
except requests.RequestException as exc:
|
|
67
|
+
retryable = exc.response is not None and exc.response.status_code in retryable_status_codes
|
|
68
|
+
if attempt >= retries or not retryable:
|
|
69
|
+
raise RuntimeError(
|
|
70
|
+
f"Request failed after {attempt + 1} attempt(s). Retryable={retryable}. Error: {exc}"
|
|
71
|
+
) from exc
|
|
72
|
+
delay = random.uniform(0, min(max_delay, base_delay * (2 ** attempt)))
|
|
73
|
+
time.sleep(delay)
|
|
74
|
+
|
|
75
|
+
raise RuntimeError("unreachable")
|
|
76
|
+
|
|
77
|
+
def fetch_image(
|
|
78
|
+
url: str,
|
|
79
|
+
params: Optional[dict] = None,
|
|
80
|
+
retries: int = 4,
|
|
81
|
+
timeout: int = 20,
|
|
82
|
+
extra_retryable_status_codes: set[int] | None = None,
|
|
83
|
+
user_agent: str = DEFAULT_USER_AGENT,
|
|
84
|
+
base_delay: float = 0.5,
|
|
85
|
+
max_delay: float = 8.0,
|
|
86
|
+
) -> ImageResponse:
|
|
87
|
+
retryable_status_codes = set(DEFAULT_RETRYABLE_STATUS_CODES)
|
|
88
|
+
if extra_retryable_status_codes:
|
|
89
|
+
retryable_status_codes.update(extra_retryable_status_codes)
|
|
90
|
+
|
|
91
|
+
headers = {
|
|
92
|
+
"User-Agent": user_agent,
|
|
93
|
+
"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
|
|
94
|
+
"Referer": "https://www.google.com/",
|
|
95
|
+
"Connection": "keep-alive",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
with requests.Session() as session:
|
|
99
|
+
for attempt in range(retries + 1):
|
|
100
|
+
try:
|
|
101
|
+
response = session.get(url, headers=headers, timeout=timeout, params=params, stream=True)
|
|
102
|
+
if response.status_code in retryable_status_codes:
|
|
103
|
+
raise requests.HTTPError(f"retryable status {response.status_code}", response=response)
|
|
104
|
+
response.raise_for_status()
|
|
105
|
+
|
|
106
|
+
content_type = response.headers.get("Content-Type", "application/octet-stream")
|
|
107
|
+
content_length = response.headers.get("Content-Length")
|
|
108
|
+
return ImageResponse(
|
|
109
|
+
content=response.content,
|
|
110
|
+
content_type=content_type,
|
|
111
|
+
content_length=int(content_length) if content_length else None,
|
|
112
|
+
status_code=response.status_code,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
except requests.RequestException as exc:
|
|
116
|
+
retryable = exc.response is not None and exc.response.status_code in retryable_status_codes
|
|
117
|
+
if attempt >= retries or not retryable:
|
|
118
|
+
raise RuntimeError(
|
|
119
|
+
f"Image request failed after {attempt + 1} attempt(s). Retryable={retryable}. Error: {exc}"
|
|
120
|
+
) from exc
|
|
121
|
+
delay = random.uniform(0, min(max_delay, base_delay * (2 ** attempt)))
|
|
122
|
+
time.sleep(delay)
|
|
123
|
+
|
|
124
|
+
raise RuntimeError("unreachable")
|