opengamedata-api-utils 0.0.1__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.
- opengamedata_api_utils-0.0.1/LICENSE +21 -0
- opengamedata_api_utils-0.0.1/PKG-INFO +69 -0
- opengamedata_api_utils-0.0.1/README.md +50 -0
- opengamedata_api_utils-0.0.1/pyproject.toml +37 -0
- opengamedata_api_utils-0.0.1/requirements.txt +5 -0
- opengamedata_api_utils-0.0.1/setup.cfg +4 -0
- opengamedata_api_utils-0.0.1/src/ogd/apis/HelloAPI.py +48 -0
- opengamedata_api_utils-0.0.1/src/ogd/apis/__init__.py +0 -0
- opengamedata_api_utils-0.0.1/src/ogd/apis/configs/ServerConfig.py +137 -0
- opengamedata_api_utils-0.0.1/src/ogd/apis/configs/__init__.py +0 -0
- opengamedata_api_utils-0.0.1/src/ogd/apis/models/APIRequest.py +101 -0
- opengamedata_api_utils-0.0.1/src/ogd/apis/models/APIResponse.py +173 -0
- opengamedata_api_utils-0.0.1/src/ogd/apis/models/__init__.py +0 -0
- opengamedata_api_utils-0.0.1/src/ogd/apis/models/enums/RESTType.py +8 -0
- opengamedata_api_utils-0.0.1/src/ogd/apis/models/enums/ResponseStatus.py +117 -0
- opengamedata_api_utils-0.0.1/src/ogd/apis/models/enums/__init__.py +0 -0
- opengamedata_api_utils-0.0.1/src/ogd/apis/resources/Hello.py +29 -0
- opengamedata_api_utils-0.0.1/src/ogd/apis/resources/ParamHello.py +29 -0
- opengamedata_api_utils-0.0.1/src/ogd/apis/utils/APIUtils.py +128 -0
- opengamedata_api_utils-0.0.1/src/ogd/apis/utils/__init__.py +0 -0
- opengamedata_api_utils-0.0.1/src/opengamedata_api_utils.egg-info/PKG-INFO +69 -0
- opengamedata_api_utils-0.0.1/src/opengamedata_api_utils.egg-info/SOURCES.txt +23 -0
- opengamedata_api_utils-0.0.1/src/opengamedata_api_utils.egg-info/dependency_links.txt +1 -0
- opengamedata_api_utils-0.0.1/src/opengamedata_api_utils.egg-info/requires.txt +4 -0
- opengamedata_api_utils-0.0.1/src/opengamedata_api_utils.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 opengamedata
|
|
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,69 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: opengamedata-api-utils
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Package of utilities for server-side scripts in OpenGameData.
|
|
5
|
+
Author: Ryan Wilkinson, Glenn Palmer, Daus Husaini
|
|
6
|
+
Author-email: Luke Swanson <superscription58@gmail.com>
|
|
7
|
+
Project-URL: Homepage, https://github.com/opengamedata/opengamedata-api-utils
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/opengamedata/opengamedata-api-utils/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: flask==2.3.3
|
|
16
|
+
Requires-Dist: flask-restful==0.3.10
|
|
17
|
+
Requires-Dist: requests==2.32.*
|
|
18
|
+
Requires-Dist: opengamedata-common>=2.0.0b6
|
|
19
|
+
|
|
20
|
+
# opengamedata-api-utils
|
|
21
|
+
|
|
22
|
+
Repository for utility server-side script and classes used by OpenGameData APIs.
|
|
23
|
+
|
|
24
|
+
## Contents
|
|
25
|
+
|
|
26
|
+
### Python Package
|
|
27
|
+
|
|
28
|
+
The `utils` repository contains a package that can be installed from `pip`, and places its classes under the `ogd.apis` namespace.
|
|
29
|
+
The available modules include:
|
|
30
|
+
|
|
31
|
+
* `HelloAPI.py` : Class for creating a "Hello, World" API to aid in testing deploys of other APIs.
|
|
32
|
+
* `schemas` : Contains a base class for server config schemas to handle version and debug-level config items
|
|
33
|
+
* `utils` : Contains helper classes for API requests and responses, as well as general-purpose parsing and setup functions.
|
|
34
|
+
|
|
35
|
+
### Data Store Utilities
|
|
36
|
+
|
|
37
|
+
The `store` directory contains a script for reindexing a folder of OGD datasets after new datasets have been added.
|
|
38
|
+
This index is used by the FileAPI to check what datasets are available on the server.
|
|
39
|
+
|
|
40
|
+
## Getting Started
|
|
41
|
+
|
|
42
|
+
### Hello World of Flask
|
|
43
|
+
|
|
44
|
+
Steps to run:
|
|
45
|
+
|
|
46
|
+
1. Check out latest `opengamedata-server`.
|
|
47
|
+
2. Run `pip install -r requirements.txt` to ensure you've got flask.
|
|
48
|
+
3. Run `flask run`.
|
|
49
|
+
4. Open localhost:5000 or localhost:5000/hello to see some really basic text output from the Flask server.
|
|
50
|
+
|
|
51
|
+
If Flask doesn't run, it's possible you'd need to first export FLASK_APP as an environment variable, set to "wsgi" (so in Bash, export FLASK_APP=wsgi).
|
|
52
|
+
However, the script is named wsgi.py specifically because Flask is supposed to auto-detect it. So if this issue ever did come up, please ping Luke so he can look into it.
|
|
53
|
+
|
|
54
|
+
## Testing in VS Code
|
|
55
|
+
|
|
56
|
+
Be sure to add the following to `.vscode/settings.json` in your local checkout:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
"python.testing.unittestEnabled": true,
|
|
60
|
+
"python.testing.unittestArgs": [
|
|
61
|
+
"-v",
|
|
62
|
+
"-s",
|
|
63
|
+
"./tests/cases",
|
|
64
|
+
"-t",
|
|
65
|
+
"./",
|
|
66
|
+
"-p",
|
|
67
|
+
"t_*.py"
|
|
68
|
+
],
|
|
69
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# opengamedata-api-utils
|
|
2
|
+
|
|
3
|
+
Repository for utility server-side script and classes used by OpenGameData APIs.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
### Python Package
|
|
8
|
+
|
|
9
|
+
The `utils` repository contains a package that can be installed from `pip`, and places its classes under the `ogd.apis` namespace.
|
|
10
|
+
The available modules include:
|
|
11
|
+
|
|
12
|
+
* `HelloAPI.py` : Class for creating a "Hello, World" API to aid in testing deploys of other APIs.
|
|
13
|
+
* `schemas` : Contains a base class for server config schemas to handle version and debug-level config items
|
|
14
|
+
* `utils` : Contains helper classes for API requests and responses, as well as general-purpose parsing and setup functions.
|
|
15
|
+
|
|
16
|
+
### Data Store Utilities
|
|
17
|
+
|
|
18
|
+
The `store` directory contains a script for reindexing a folder of OGD datasets after new datasets have been added.
|
|
19
|
+
This index is used by the FileAPI to check what datasets are available on the server.
|
|
20
|
+
|
|
21
|
+
## Getting Started
|
|
22
|
+
|
|
23
|
+
### Hello World of Flask
|
|
24
|
+
|
|
25
|
+
Steps to run:
|
|
26
|
+
|
|
27
|
+
1. Check out latest `opengamedata-server`.
|
|
28
|
+
2. Run `pip install -r requirements.txt` to ensure you've got flask.
|
|
29
|
+
3. Run `flask run`.
|
|
30
|
+
4. Open localhost:5000 or localhost:5000/hello to see some really basic text output from the Flask server.
|
|
31
|
+
|
|
32
|
+
If Flask doesn't run, it's possible you'd need to first export FLASK_APP as an environment variable, set to "wsgi" (so in Bash, export FLASK_APP=wsgi).
|
|
33
|
+
However, the script is named wsgi.py specifically because Flask is supposed to auto-detect it. So if this issue ever did come up, please ping Luke so he can look into it.
|
|
34
|
+
|
|
35
|
+
## Testing in VS Code
|
|
36
|
+
|
|
37
|
+
Be sure to add the following to `.vscode/settings.json` in your local checkout:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
"python.testing.unittestEnabled": true,
|
|
41
|
+
"python.testing.unittestArgs": [
|
|
42
|
+
"-v",
|
|
43
|
+
"-s",
|
|
44
|
+
"./tests/cases",
|
|
45
|
+
"-t",
|
|
46
|
+
"./",
|
|
47
|
+
"-p",
|
|
48
|
+
"t_*.py"
|
|
49
|
+
],
|
|
50
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"build == 1.2.*",
|
|
4
|
+
"setuptools == 74.1.*",
|
|
5
|
+
"setuptools-git-versioning == 2.0.*",
|
|
6
|
+
"twine == 5.1.*",
|
|
7
|
+
"wheel == 0.44.*"
|
|
8
|
+
]
|
|
9
|
+
build-backend = "setuptools.build_meta"
|
|
10
|
+
|
|
11
|
+
[tool.setuptools-git-versioning]
|
|
12
|
+
enabled = true
|
|
13
|
+
|
|
14
|
+
[tool.setuptools.dynamic]
|
|
15
|
+
dependencies = {file = ["requirements.txt"]}
|
|
16
|
+
|
|
17
|
+
[project]
|
|
18
|
+
name = "opengamedata-api-utils"
|
|
19
|
+
dynamic = ["version", "dependencies"]
|
|
20
|
+
authors = [
|
|
21
|
+
{ name="Luke Swanson", email="superscription58@gmail.com" },
|
|
22
|
+
{ name="Ryan Wilkinson" },
|
|
23
|
+
{ name="Glenn Palmer" },
|
|
24
|
+
{ name="Daus Husaini" }
|
|
25
|
+
]
|
|
26
|
+
description = "Package of utilities for server-side scripts in OpenGameData."
|
|
27
|
+
readme = "README.md"
|
|
28
|
+
requires-python = ">=3.12"
|
|
29
|
+
classifiers = [
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"License :: OSI Approved :: MIT License",
|
|
32
|
+
"Operating System :: OS Independent",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
"Homepage" = "https://github.com/opengamedata/opengamedata-api-utils"
|
|
37
|
+
"Bug Tracker" = "https://github.com/opengamedata/opengamedata-api-utils/issues"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HelloAPI
|
|
3
|
+
|
|
4
|
+
Contains the HelloAPI class, which we register to all API apps as a way to test that the app is active.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# import libraries
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
# import 3rd-party libraries
|
|
12
|
+
from flask import Flask
|
|
13
|
+
from flask_restful import Resource, Api
|
|
14
|
+
|
|
15
|
+
# import OGD libraries
|
|
16
|
+
|
|
17
|
+
# import locals
|
|
18
|
+
from ogd.apis.resources.Hello import Hello
|
|
19
|
+
from ogd.apis.resources.ParamHello import ParamHello
|
|
20
|
+
from ogd.apis.configs.ServerConfig import ServerConfig
|
|
21
|
+
from ogd.apis.models.enums.ResponseStatus import ResponseStatus
|
|
22
|
+
from ogd.apis.models.enums.RESTType import RESTType
|
|
23
|
+
from ogd.apis.models.APIResponse import APIResponse
|
|
24
|
+
|
|
25
|
+
class HelloAPI:
|
|
26
|
+
class ENDPOINTS(StrEnum):
|
|
27
|
+
HELLO = 'hello'
|
|
28
|
+
VERSION = 'version'
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def register(app:Flask, server_config:ServerConfig, root_endpoint:Optional[ENDPOINTS | str]=None):
|
|
32
|
+
api = Api(app)
|
|
33
|
+
urls = ['/hello'] + (['/'] if root_endpoint == HelloAPI.ENDPOINTS.HELLO else [])
|
|
34
|
+
api.add_resource(Hello, *urls)
|
|
35
|
+
api.add_resource(ParamHello, '/p_hello/<name>')
|
|
36
|
+
urls = ['/version'] + (['/'] if root_endpoint == HelloAPI.ENDPOINTS.VERSION else [])
|
|
37
|
+
api.add_resource(HelloAPI.Version, *urls)
|
|
38
|
+
|
|
39
|
+
HelloAPI.server_config = server_config
|
|
40
|
+
|
|
41
|
+
class Version(Resource):
|
|
42
|
+
def get(self):
|
|
43
|
+
ret_val = APIResponse(
|
|
44
|
+
req_type = RESTType.GET,
|
|
45
|
+
val = { "version" : str(HelloAPI.server_config.Version) },
|
|
46
|
+
msg = "Successfully retrieved API version.",
|
|
47
|
+
status = ResponseStatus.OK)
|
|
48
|
+
return ret_val.AsDict
|
|
File without changes
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ServerConfig
|
|
3
|
+
|
|
4
|
+
Contains a Config class for managing config data for server configurations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# import standard libraries
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Dict, Final, Optional, Self
|
|
10
|
+
|
|
11
|
+
# import 3rd-party libraries
|
|
12
|
+
|
|
13
|
+
# import OGD libraries
|
|
14
|
+
from ogd.common.configs.Config import Config
|
|
15
|
+
from ogd.common.models.SemanticVersion import SemanticVersion
|
|
16
|
+
from ogd.common.utils.typing import JSONMap, Map
|
|
17
|
+
from ogd.common.utils.Logger import Logger
|
|
18
|
+
|
|
19
|
+
# import local files
|
|
20
|
+
|
|
21
|
+
class ServerConfig(Config):
|
|
22
|
+
_DEFAULT_DEBUG_LEVEL : Final[int] = logging.INFO
|
|
23
|
+
_DEFAULT_VERSION : Final[str] = "UNKNOWN VERSION"
|
|
24
|
+
|
|
25
|
+
# *** BUILT-INS & PROPERTIES ***
|
|
26
|
+
|
|
27
|
+
def __init__(self, name:str,
|
|
28
|
+
debug_level:Optional[int], version:Optional[SemanticVersion],
|
|
29
|
+
other_elements:Optional[Map]=None):
|
|
30
|
+
|
|
31
|
+
unparsed_elements : Map = other_elements or {}
|
|
32
|
+
|
|
33
|
+
self._dbg_level : int
|
|
34
|
+
self._version : SemanticVersion
|
|
35
|
+
|
|
36
|
+
self._version = version if version is not None else self._parseVersion(unparsed_elements=unparsed_elements, schema_name=name)
|
|
37
|
+
self._dbg_level = debug_level if debug_level is not None else self._parseDebugLevel(unparsed_elements=unparsed_elements, schema_name=name)
|
|
38
|
+
|
|
39
|
+
super().__init__(name=name, other_elements=other_elements)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def DebugLevel(self) -> int:
|
|
43
|
+
return self._dbg_level
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def Version(self) -> SemanticVersion:
|
|
47
|
+
return self._version
|
|
48
|
+
|
|
49
|
+
# *** IMPLEMENT ABSTRACT FUNCTIONS ***
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def AsMarkdown(self) -> str:
|
|
53
|
+
ret_val : str
|
|
54
|
+
|
|
55
|
+
ret_val = f"{self.Name}"
|
|
56
|
+
return ret_val
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def AsDict(self) -> JSONMap:
|
|
60
|
+
return {
|
|
61
|
+
"API_VERSION": str(self.Version),
|
|
62
|
+
"DEBUG_LEVEL": self.DebugLevel
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def Default(cls):
|
|
67
|
+
return ServerConfig(
|
|
68
|
+
name="DefaultServerConfig",
|
|
69
|
+
debug_level=ServerConfig._DEFAULT_DEBUG_LEVEL,
|
|
70
|
+
version=SemanticVersion.FromString("0.0.0-Testing"),
|
|
71
|
+
other_elements={}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def _fromDict(cls, name:str, unparsed_elements:Map,
|
|
76
|
+
key_overrides:Optional[Dict[str, str]]=None,
|
|
77
|
+
default_override:Optional[Self]=None):
|
|
78
|
+
return ServerConfig(name=name, debug_level=None, version=None, other_elements=unparsed_elements)
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _parseDebugLevel(unparsed_elements:Map, schema_name:Optional[str]=None) -> int:
|
|
82
|
+
ret_val : int
|
|
83
|
+
|
|
84
|
+
raw_level : str = ServerConfig.ParseElement(
|
|
85
|
+
unparsed_elements=unparsed_elements,
|
|
86
|
+
valid_keys=["DEBUG_LEVEL"],
|
|
87
|
+
to_type=[int, str],
|
|
88
|
+
default_value=ServerConfig._DEFAULT_DEBUG_LEVEL,
|
|
89
|
+
remove_target=True,
|
|
90
|
+
schema_name=schema_name
|
|
91
|
+
)
|
|
92
|
+
if isinstance(raw_level, int):
|
|
93
|
+
if raw_level in {logging.ERROR, logging.WARN, logging.INFO, logging.DEBUG}:
|
|
94
|
+
ret_val = raw_level
|
|
95
|
+
else:
|
|
96
|
+
ret_val = ServerConfig._DEFAULT_DEBUG_LEVEL
|
|
97
|
+
Logger.Log(f"Debug level had a value of {raw_level}, but this does not correspond to a valid logging level, defaulting to {ret_val}.", logging.WARNING)
|
|
98
|
+
elif isinstance(raw_level, str):
|
|
99
|
+
match raw_level.upper():
|
|
100
|
+
case "ERROR":
|
|
101
|
+
ret_val = logging.ERROR
|
|
102
|
+
case "WARNING" | "WARN":
|
|
103
|
+
ret_val = logging.WARN
|
|
104
|
+
case "INFO":
|
|
105
|
+
ret_val = logging.INFO
|
|
106
|
+
case "DEBUG":
|
|
107
|
+
ret_val = logging.DEBUG
|
|
108
|
+
case _:
|
|
109
|
+
ret_val = ServerConfig._DEFAULT_DEBUG_LEVEL
|
|
110
|
+
Logger.Log(f"Config debug level had unexpected value {raw_level}, defaulting to {ret_val}.", logging.WARNING)
|
|
111
|
+
else:
|
|
112
|
+
ret_val = ServerConfig._DEFAULT_DEBUG_LEVEL
|
|
113
|
+
Logger.Log(f"Config debug level was unexpected type {type(raw_level)}, defaulting to {ret_val}.", logging.WARNING)
|
|
114
|
+
|
|
115
|
+
return ret_val
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _parseVersion(unparsed_elements:Map, schema_name:Optional[str]=None) -> SemanticVersion:
|
|
119
|
+
ret_val : SemanticVersion
|
|
120
|
+
|
|
121
|
+
raw_version = ServerConfig.ParseElement(
|
|
122
|
+
unparsed_elements=unparsed_elements,
|
|
123
|
+
valid_keys=["API_VERSION"],
|
|
124
|
+
to_type=[int, str],
|
|
125
|
+
default_value=ServerConfig._DEFAULT_VERSION,
|
|
126
|
+
remove_target=True,
|
|
127
|
+
schema_name=schema_name
|
|
128
|
+
)
|
|
129
|
+
if isinstance(raw_version, int):
|
|
130
|
+
ret_val = SemanticVersion(major=raw_version)
|
|
131
|
+
elif isinstance(raw_version, str):
|
|
132
|
+
ret_val = SemanticVersion.FromString(semver=raw_version)
|
|
133
|
+
else:
|
|
134
|
+
ret_val = SemanticVersion.FromString(str(raw_version))
|
|
135
|
+
Logger.Log(f"Config version was unexpected type {type(raw_version)}, defaulting to SemanticVersion(str(version))={ret_val}.", logging.WARN)
|
|
136
|
+
|
|
137
|
+
return ret_val
|
|
File without changes
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
from flask import current_app
|
|
6
|
+
|
|
7
|
+
from ogd.apis.models.enums.RESTType import RESTType
|
|
8
|
+
from ogd.apis.models.enums.ResponseStatus import ResponseStatus
|
|
9
|
+
from ogd.apis.models.APIResponse import APIResponse
|
|
10
|
+
|
|
11
|
+
class APIRequest:
|
|
12
|
+
def __init__(self, url:str, request_type:str | RESTType, params:Optional[Dict[str, Any]]=None, body:Optional[Dict[str, Any]]=None, timeout:int=1):
|
|
13
|
+
"""Utility function to make it easier to send requests to a remote server during unit testing.
|
|
14
|
+
|
|
15
|
+
This function does some basic sanity checking of the target URL,
|
|
16
|
+
maps the request type to the appropriate `requests` function call,
|
|
17
|
+
and performs basic error handling to notify what error occurred.
|
|
18
|
+
|
|
19
|
+
:param url: The target URL for the web request
|
|
20
|
+
:type url: str
|
|
21
|
+
:param request: Whether to perform a "GET", "POST", or "PUT" request
|
|
22
|
+
:type request: str
|
|
23
|
+
:param params: A mapping of request parameter names to values. Defaults to {}
|
|
24
|
+
:type params: Dict[str, Any], optional
|
|
25
|
+
:param body: The body of the request to send. Defaults to None
|
|
26
|
+
:type body: Dict[str, Any], optional
|
|
27
|
+
:param logger: A logger to use for debug/error outputs. Defaults to None
|
|
28
|
+
:type logger: logging.Logger, optional
|
|
29
|
+
:raises err: Currently, any exceptions that occur during the request will be raised up.
|
|
30
|
+
If verbose logging is on, a simple debug message indicating the request type and URL is printed first.
|
|
31
|
+
:return: The `Response` object from the request, or None if an error occurred.
|
|
32
|
+
:rtype: requests.Response
|
|
33
|
+
"""
|
|
34
|
+
params = params or {}
|
|
35
|
+
|
|
36
|
+
self._request_type : RESTType
|
|
37
|
+
|
|
38
|
+
if not (url.startswith("http://") or url.startswith("https://")):
|
|
39
|
+
url = f"https://{url}"
|
|
40
|
+
match request_type:
|
|
41
|
+
case RESTType():
|
|
42
|
+
self._request_type = request_type
|
|
43
|
+
case str():
|
|
44
|
+
try:
|
|
45
|
+
self._request_type = RESTType[request_type.upper()]
|
|
46
|
+
except KeyError:
|
|
47
|
+
current_app.logger.warning(f"Bad request type {request_type}, defaulting to GET")
|
|
48
|
+
self._request_type = RESTType.GET
|
|
49
|
+
case _:
|
|
50
|
+
raise TypeError(f"request_type for APIRequest was invalid type {type(request_type)}")
|
|
51
|
+
|
|
52
|
+
self._url = url
|
|
53
|
+
self._params = params
|
|
54
|
+
self._body = body
|
|
55
|
+
self._timeout = timeout
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
return f"Request: {self._request_type} {self._url}"
|
|
59
|
+
|
|
60
|
+
def Execute(self, logger:Optional[logging.Logger]=None, retry:int=0) -> APIResponse:
|
|
61
|
+
ret_val : APIResponse
|
|
62
|
+
|
|
63
|
+
if logger is None and current_app:
|
|
64
|
+
logger = current_app.logger
|
|
65
|
+
|
|
66
|
+
response : requests.Response
|
|
67
|
+
try:
|
|
68
|
+
match (self._request_type):
|
|
69
|
+
case RESTType.GET:
|
|
70
|
+
response = requests.get( self._url, params=self._params, timeout=self._timeout)
|
|
71
|
+
case RESTType.POST:
|
|
72
|
+
response = requests.post(self._url, params=self._params, data=self._body, timeout=self._timeout)
|
|
73
|
+
case RESTType.PUT:
|
|
74
|
+
response = requests.put( self._url, params=self._params, data=self._body, timeout=self._timeout)
|
|
75
|
+
case _:
|
|
76
|
+
if logger:
|
|
77
|
+
logger.warning(f"Bad request type {self._request_type}, defaulting to GET")
|
|
78
|
+
response = requests.get(self._url, params=self._params, timeout=self._timeout)
|
|
79
|
+
except requests.exceptions.ReadTimeout:
|
|
80
|
+
if retry < 5:
|
|
81
|
+
if logger:
|
|
82
|
+
logger.error(f"Timeout error executing {self}, trying again...")
|
|
83
|
+
return self.Execute(logger=logger, retry=retry+1)
|
|
84
|
+
else:
|
|
85
|
+
if logger:
|
|
86
|
+
logger.error(f"Timeout error executing {self}.")
|
|
87
|
+
return APIResponse(req_type=self._request_type, val=None, msg="Could not retrieve results, server timed out!", status=ResponseStatus.GATEWAY_TIMEOUT)
|
|
88
|
+
except Exception as err:
|
|
89
|
+
if logger:
|
|
90
|
+
logger.error(f"Error on {self._request_type} request to {self._url} : {err}")
|
|
91
|
+
return APIResponse(req_type=self._request_type, val=None, msg="Could not retrieve results, encountered an unexpected error while executing request!", status=ResponseStatus.INTERNAL_ERR)
|
|
92
|
+
else:
|
|
93
|
+
ret_val = APIResponse.FromResponse(response)
|
|
94
|
+
if logger:
|
|
95
|
+
out = logger.debug if ret_val.Status == ResponseStatus.OK else logger.warning
|
|
96
|
+
out(f"Request sent to: {self._url}, with params {self._params}")
|
|
97
|
+
out(f"Response received from: {self._url}")
|
|
98
|
+
out(f" Status: {ret_val.Status}")
|
|
99
|
+
out(f" Msg: {ret_val.Message}")
|
|
100
|
+
out(f" Value: {ret_val.Value}")
|
|
101
|
+
return ret_val
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
APIResponse
|
|
3
|
+
|
|
4
|
+
Contains class for representing a response from an OGD API,
|
|
5
|
+
as well as utility enums used by the APIResponse class.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# import standard libraries
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
# import 3rd-party libraries
|
|
14
|
+
import requests
|
|
15
|
+
from flask import Response
|
|
16
|
+
|
|
17
|
+
# import OGD libraries
|
|
18
|
+
from ogd.common.utils.typing import Map
|
|
19
|
+
from ogd.common.utils.Logger import Logger
|
|
20
|
+
|
|
21
|
+
# Import local files
|
|
22
|
+
from ogd.apis.models.enums.RESTType import RESTType
|
|
23
|
+
from ogd.apis.models.enums.ResponseStatus import ResponseStatus
|
|
24
|
+
|
|
25
|
+
class APIResponse:
|
|
26
|
+
def __init__(self, req_type:Optional[RESTType | str], val:Optional[Map], msg:str, status:ResponseStatus):
|
|
27
|
+
self._type : Optional[RESTType]
|
|
28
|
+
self._val : Optional[Map]
|
|
29
|
+
|
|
30
|
+
match req_type:
|
|
31
|
+
case RESTType():
|
|
32
|
+
self._type = req_type
|
|
33
|
+
case str():
|
|
34
|
+
self._type = RESTType[req_type]
|
|
35
|
+
case _:
|
|
36
|
+
self._type = None
|
|
37
|
+
match val:
|
|
38
|
+
case dict() | None:
|
|
39
|
+
self._val = val
|
|
40
|
+
case _:
|
|
41
|
+
try:
|
|
42
|
+
self._val = json.loads(str(val))
|
|
43
|
+
except json.decoder.JSONDecodeError as err:
|
|
44
|
+
abbreviated_val = f"{str(val)[:20]}..." if len(str(val)) > 20 else str(val)
|
|
45
|
+
_msg = f"API response 'value' field contained value '{abbreviated_val}' with invalid type {type(val)}, which could not be converted to a dictionary. Attempting to do so resulted in error:\n{err}\nThe value field will be left blank."
|
|
46
|
+
Logger.Log(_msg, logging.ERROR)
|
|
47
|
+
self._val = None
|
|
48
|
+
self._msg : str = msg
|
|
49
|
+
self._status : ResponseStatus = status
|
|
50
|
+
|
|
51
|
+
def __str__(self):
|
|
52
|
+
return f"{str(self.Type)} request: {self.Status}\n{self.Message}\nValues: {self.Value}"
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def Default(req_type:RESTType):
|
|
56
|
+
return APIResponse(
|
|
57
|
+
req_type=req_type,
|
|
58
|
+
val=None,
|
|
59
|
+
msg="",
|
|
60
|
+
status=ResponseStatus.NONE
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def FromResponse(result:requests.Response) -> "APIResponse":
|
|
65
|
+
ret_val : APIResponse
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
raw = result.json()
|
|
69
|
+
ret_val = APIResponse(req_type=raw.get("type"), val=raw.get("val"), msg=raw.get("msg"), status=ResponseStatus(result.status_code))
|
|
70
|
+
except requests.exceptions.JSONDecodeError:
|
|
71
|
+
ret_val = APIResponse(req_type=None, val=None, msg=result.text, status=ResponseStatus(result.status_code))
|
|
72
|
+
|
|
73
|
+
return ret_val
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def FromDict(all_elements:Dict[str, Any], status:Optional[ResponseStatus]=None) -> Optional["APIResponse"]:
|
|
77
|
+
ret_val : Optional["APIResponse"] = None
|
|
78
|
+
|
|
79
|
+
_type_raw = all_elements.get("type", None)
|
|
80
|
+
_val_raw = all_elements.get("val", None)
|
|
81
|
+
_msg = all_elements.get("msg", "NOT FOUND")
|
|
82
|
+
_status_raw = all_elements.get("status", None)
|
|
83
|
+
try:
|
|
84
|
+
_type = RESTType[str(_type_raw).upper()] if _type_raw else None
|
|
85
|
+
_val = _val_raw if isinstance(_val_raw, dict) else json.loads(str(_val_raw)) if _val_raw is not None else None
|
|
86
|
+
_status = ResponseStatus[str(_status_raw).upper()] if _status_raw else (status or ResponseStatus.NONE)
|
|
87
|
+
except KeyError:
|
|
88
|
+
pass
|
|
89
|
+
else:
|
|
90
|
+
ret_val = APIResponse(req_type=_type, val=_val, msg=_msg, status=_status)
|
|
91
|
+
return ret_val
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def Type(self) -> Optional[RESTType]:
|
|
95
|
+
"""Property for the type of REST request
|
|
96
|
+
|
|
97
|
+
:return: A RESTType representing the type of REST request
|
|
98
|
+
:rtype: _type_
|
|
99
|
+
"""
|
|
100
|
+
return self._type
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def Value(self) -> Optional[Map]:
|
|
104
|
+
"""Property for the value of the request result.
|
|
105
|
+
|
|
106
|
+
:return: Some value, of any type, returned from the request.
|
|
107
|
+
:rtype: Any
|
|
108
|
+
"""
|
|
109
|
+
return self._val
|
|
110
|
+
@Value.setter
|
|
111
|
+
def Value(self, new_val:Optional[Map]):
|
|
112
|
+
self._val = new_val
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def Message(self) -> str:
|
|
117
|
+
"""Property for the message associated with a request result.
|
|
118
|
+
|
|
119
|
+
:return: A string message giving details on the result of the request.
|
|
120
|
+
:rtype: str
|
|
121
|
+
"""
|
|
122
|
+
return self._msg
|
|
123
|
+
@Message.setter
|
|
124
|
+
def Message(self, new_msg:str):
|
|
125
|
+
self._msg = new_msg
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def Status(self) -> ResponseStatus:
|
|
129
|
+
"""Property for the status of the request.
|
|
130
|
+
|
|
131
|
+
:return: A ResponseStatus indicating whether request is/was successful, incomplete, failed, etc.
|
|
132
|
+
:rtype: ResponseStatus
|
|
133
|
+
"""
|
|
134
|
+
return self._status
|
|
135
|
+
@property
|
|
136
|
+
def OK(self) -> bool:
|
|
137
|
+
"""Property indicating whether the APIResponse was successful or not.
|
|
138
|
+
|
|
139
|
+
Technically, this includes both a true "OK" response and any other 200-range response.
|
|
140
|
+
|
|
141
|
+
:return: True if the APIResponse status is in the 200 range, otherwise False.
|
|
142
|
+
:rtype: bool
|
|
143
|
+
"""
|
|
144
|
+
return self.Status in ResponseStatus.SuccessStatuses()
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def AsDict(self):
|
|
148
|
+
return {
|
|
149
|
+
"type" : str(self._type),
|
|
150
|
+
"val" : self._val,
|
|
151
|
+
"msg" : self._msg,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def AsJSON(self):
|
|
156
|
+
return json.dumps(self.AsDict)
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def AsFlaskResponse(self) -> Response:
|
|
160
|
+
return Response(response=self.AsJSON, status=self.Status.value, mimetype='application/json')
|
|
161
|
+
|
|
162
|
+
def RequestErrored(self, msg:str, status:Optional[ResponseStatus]=None):
|
|
163
|
+
self._status = status if status is not None and status in ResponseStatus.ClientErrors() else ResponseStatus.BAD_REQUEST
|
|
164
|
+
self.Message = f"ERROR: {msg}"
|
|
165
|
+
|
|
166
|
+
def ServerErrored(self, msg:str, status:Optional[ResponseStatus]=None):
|
|
167
|
+
self._status = status if status is not None and status in ResponseStatus.ServerErrors() else ResponseStatus.INTERNAL_ERR
|
|
168
|
+
self.Message = f"SERVER ERROR: {msg}"
|
|
169
|
+
|
|
170
|
+
def RequestSucceeded(self, msg:str, val:Optional[Map]):
|
|
171
|
+
self._status = ResponseStatus.OK
|
|
172
|
+
self.Message = f"SUCCESS: {msg}"
|
|
173
|
+
self.Value = val
|
|
File without changes
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
from typing import Set
|
|
3
|
+
|
|
4
|
+
class ResponseStatus(IntEnum):
|
|
5
|
+
"""Enumerated type to track the status of an API request result.
|
|
6
|
+
"""
|
|
7
|
+
NONE = 1
|
|
8
|
+
CONTINUE = 100
|
|
9
|
+
OK = 200
|
|
10
|
+
MULTI_CHOICES = 300
|
|
11
|
+
BAD_REQUEST = 400
|
|
12
|
+
INTERNAL_ERR = 500
|
|
13
|
+
|
|
14
|
+
# 100s
|
|
15
|
+
SWITCHING_PROTOCOLS = 101
|
|
16
|
+
PROCESSING = 102
|
|
17
|
+
EARLY_HINTS = 103
|
|
18
|
+
|
|
19
|
+
# 200s
|
|
20
|
+
CREATED = 201
|
|
21
|
+
ACCEPTED = 202
|
|
22
|
+
NON_AUTHORITATIVE = 203
|
|
23
|
+
NO_CONTENT = 204
|
|
24
|
+
RESET = 205
|
|
25
|
+
PARTIAL = 206
|
|
26
|
+
MULTI_STATUS = 207
|
|
27
|
+
ALREADY_REPORTED = 208
|
|
28
|
+
IM_USED = 226
|
|
29
|
+
|
|
30
|
+
# 300s
|
|
31
|
+
MOVED = 301
|
|
32
|
+
FOUND = 302
|
|
33
|
+
SEE_OTHER = 303
|
|
34
|
+
NOT_MODIFIED = 304
|
|
35
|
+
TEMPORARY_REDIR = 307
|
|
36
|
+
PERMANENT_REDIR = 308
|
|
37
|
+
|
|
38
|
+
# 400s
|
|
39
|
+
UNAUTHORIZED = 401
|
|
40
|
+
PAYMENT_REQUIRED = 402
|
|
41
|
+
FORBIDDEN = 403
|
|
42
|
+
NOT_FOUND = 404
|
|
43
|
+
METHOD_NOT_ALLOWED = 405
|
|
44
|
+
NOT_ACCEPTABLE = 406
|
|
45
|
+
PROXY_AUTH_REQUIRED = 407
|
|
46
|
+
REQUEST_TIMEOUT = 408
|
|
47
|
+
CONFLICT = 409
|
|
48
|
+
GONE = 410
|
|
49
|
+
LENGTH_REQUIRED = 411
|
|
50
|
+
PRECONDITION_FAILED = 412
|
|
51
|
+
CONTENT_TOO_LARGE = 413
|
|
52
|
+
URI_TOO_LONG = 414
|
|
53
|
+
UNSUPPORTED_MEDIA = 415
|
|
54
|
+
RANGE_INVALID = 416
|
|
55
|
+
EXPECTATION_FAIL = 417
|
|
56
|
+
IM_A_TEAPOT = 418
|
|
57
|
+
MISDIRECTED = 421
|
|
58
|
+
TOO_EARLY = 425
|
|
59
|
+
UPGRADE_REQUIRED = 426
|
|
60
|
+
PRECONDITION_REQUIRED = 428
|
|
61
|
+
TOO_MANY_REQUESTS = 429
|
|
62
|
+
HEADERS_TOO_LARGE = 431
|
|
63
|
+
ILLEGAL = 451
|
|
64
|
+
|
|
65
|
+
# 400s WebDAV
|
|
66
|
+
UNPROCESSABLE = 422
|
|
67
|
+
LOCKED = 423
|
|
68
|
+
FAILED_DEPENDENCY = 424
|
|
69
|
+
|
|
70
|
+
# 500s
|
|
71
|
+
NOT_IMPLEMENTED = 501
|
|
72
|
+
BAD_GATEWAY = 502
|
|
73
|
+
UNAVAILABLE = 503
|
|
74
|
+
GATEWAY_TIMEOUT = 504
|
|
75
|
+
UNSUPPORTED_HTTP_VERSION = 505
|
|
76
|
+
VARIANT_NEGOTIATES = 506
|
|
77
|
+
NOT_EXTENDED = 510
|
|
78
|
+
NETWORK_AUTH_REQUIRED = 511
|
|
79
|
+
|
|
80
|
+
# 500s WebDAV
|
|
81
|
+
INSUFFICIENT_STORAGE = 507
|
|
82
|
+
LOOP_DETECTED = 508
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def SuccessStatuses() -> Set["ResponseStatus"]:
|
|
86
|
+
"""Gets the set of valid 400-level "client" error responses.
|
|
87
|
+
|
|
88
|
+
:return: The set of valid 400-level "client" error responses.
|
|
89
|
+
:rtype: Set[ResponseStatus]
|
|
90
|
+
"""
|
|
91
|
+
return {status for status in set(ResponseStatus) if status in range(200, 300)}
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def ClientErrors() -> Set["ResponseStatus"]:
|
|
95
|
+
"""Gets the set of valid 400-level "client" error responses.
|
|
96
|
+
|
|
97
|
+
:return: The set of valid 400-level "client" error responses.
|
|
98
|
+
:rtype: Set[ResponseStatus]
|
|
99
|
+
"""
|
|
100
|
+
return {status for status in set(ResponseStatus) if status in range(400, 500)}
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def ServerErrors() -> Set["ResponseStatus"]:
|
|
104
|
+
"""Gets the set of valid 500-level "server" error responses.
|
|
105
|
+
|
|
106
|
+
:return: The set of valid 500-level "server" error responses.
|
|
107
|
+
:rtype: Set[ResponseStatus]
|
|
108
|
+
"""
|
|
109
|
+
return {status for status in set(ResponseStatus) if status in range(500, 600)}
|
|
110
|
+
|
|
111
|
+
def __str__(self):
|
|
112
|
+
"""Stringify function for ResponseStatus objects.
|
|
113
|
+
|
|
114
|
+
:return: Simple string version of the name of a ResponseStatus
|
|
115
|
+
:rtype: _type_
|
|
116
|
+
"""
|
|
117
|
+
return self.name
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# import 3rd-party libraries
|
|
2
|
+
from flask_restful import Resource
|
|
3
|
+
|
|
4
|
+
from ogd.apis.models.APIResponse import APIResponse, RESTType, ResponseStatus
|
|
5
|
+
|
|
6
|
+
class Hello(Resource):
|
|
7
|
+
def get(self):
|
|
8
|
+
ret_val = APIResponse(
|
|
9
|
+
req_type = RESTType.GET,
|
|
10
|
+
val = None,
|
|
11
|
+
msg = "Hello! You GETted successfully!",
|
|
12
|
+
status = ResponseStatus.OK)
|
|
13
|
+
return ret_val.AsDict
|
|
14
|
+
|
|
15
|
+
def post(self):
|
|
16
|
+
ret_val = APIResponse(
|
|
17
|
+
req_type = RESTType.POST,
|
|
18
|
+
val = None,
|
|
19
|
+
msg = "Hello! You POSTed successfully!",
|
|
20
|
+
status = ResponseStatus.OK)
|
|
21
|
+
return ret_val.AsDict
|
|
22
|
+
|
|
23
|
+
def put(self):
|
|
24
|
+
ret_val = APIResponse(
|
|
25
|
+
req_type = RESTType.PUT,
|
|
26
|
+
val = None,
|
|
27
|
+
msg = "Hello! You PUTted successfully!",
|
|
28
|
+
status = ResponseStatus.OK)
|
|
29
|
+
return ret_val.AsDict
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# import 3rd-party libraries
|
|
2
|
+
from flask_restful import Resource
|
|
3
|
+
|
|
4
|
+
from ogd.apis.models.APIResponse import APIResponse, RESTType, ResponseStatus
|
|
5
|
+
|
|
6
|
+
class ParamHello(Resource):
|
|
7
|
+
def get(self, name):
|
|
8
|
+
ret_val = APIResponse(
|
|
9
|
+
req_type = RESTType.GET,
|
|
10
|
+
val = None,
|
|
11
|
+
msg = f"Hello {name}! You GETted successfully!",
|
|
12
|
+
status = ResponseStatus.OK)
|
|
13
|
+
return ret_val.AsDict
|
|
14
|
+
|
|
15
|
+
def post(self, name):
|
|
16
|
+
ret_val = APIResponse(
|
|
17
|
+
req_type = RESTType.POST,
|
|
18
|
+
val = None,
|
|
19
|
+
msg = f"Hello {name}! You POSTed successfully!",
|
|
20
|
+
status = ResponseStatus.OK)
|
|
21
|
+
return ret_val.AsDict
|
|
22
|
+
|
|
23
|
+
def put(self, name):
|
|
24
|
+
ret_val = APIResponse(
|
|
25
|
+
req_type = RESTType.PUT,
|
|
26
|
+
val = None,
|
|
27
|
+
msg = f"Hello {name}! You PUTted successfully!",
|
|
28
|
+
status = ResponseStatus.OK)
|
|
29
|
+
return ret_val.AsDict
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
APIUtils
|
|
3
|
+
|
|
4
|
+
Contains general utility functions for common tasks when setting up our flask/flask-restful API functions.
|
|
5
|
+
In particular, has functions to assist in parsing certain kinds of data, and for generating OGD-core objects.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# import standard libraries
|
|
9
|
+
import json
|
|
10
|
+
from json.decoder import JSONDecodeError
|
|
11
|
+
from logging import Logger
|
|
12
|
+
from typing import Any, List, Optional
|
|
13
|
+
from urllib import parse
|
|
14
|
+
|
|
15
|
+
def parse_list(list_str:str, logger:Optional[Logger]=None) -> Optional[List[Any]]:
|
|
16
|
+
"""Simple utility to parse a string containing a bracketed list into a Python list.
|
|
17
|
+
Returns None if the list was empty
|
|
18
|
+
|
|
19
|
+
:param list_str: _description_
|
|
20
|
+
:type list_str: str
|
|
21
|
+
:return: A list parsed from the input string, or None if the string list was invalid or empty.
|
|
22
|
+
:rtype: Union[List[Any], None]
|
|
23
|
+
"""
|
|
24
|
+
ret_val : Optional[List[Any]] = None
|
|
25
|
+
try:
|
|
26
|
+
ret_val = json.loads(list_str)
|
|
27
|
+
except JSONDecodeError as e:
|
|
28
|
+
if logger:
|
|
29
|
+
logger.warning(f"Could not parse '{list_str}' as a list, format was not valid!\nGot Error {e}")
|
|
30
|
+
else:
|
|
31
|
+
if ret_val is not None and len(ret_val) == 0:
|
|
32
|
+
# If we had empty list, just treat as null.
|
|
33
|
+
ret_val = None
|
|
34
|
+
return ret_val
|
|
35
|
+
|
|
36
|
+
def urljoin(base:str, url:str, ignore_base_file:bool=False, allow_fragments:bool=True):
|
|
37
|
+
"""Custom variation of the `urllib.parse.urljoin` function provided by Python.
|
|
38
|
+
|
|
39
|
+
By default, this version allows filenames in the base path to remain in the joined path.
|
|
40
|
+
|
|
41
|
+
This is useful for working with Flask apps, particularly on Apache.
|
|
42
|
+
Specifically, unless you alias things in Apache, you'll have `app.py` or `app.wsgi` in the URL.
|
|
43
|
+
For a Flask API, then, you'll likely be joining a base URL like `"https://host.of.app/path/to/app.wsgi"` with an endpoint, call it `"endpoint"`.
|
|
44
|
+
Under `urllib.parse.urljoin`, you'll get `"https://host.of.app/path/to/endpoint"`.
|
|
45
|
+
With _this_ function, setting `ignore_base_file=False`, you'll get `"http://host.of.app/path/to/app.wsgi/endpoint"` as desired.
|
|
46
|
+
|
|
47
|
+
When `ignore_base_file=True`, this function directly falls back to use `urllib.parse.urljoin`.
|
|
48
|
+
|
|
49
|
+
:param base: The base URL, onto which the `url` parameter is joined.
|
|
50
|
+
:type base: str
|
|
51
|
+
:param url: The URL to be joined onto the given base URL.
|
|
52
|
+
:type url: str
|
|
53
|
+
:param ignore_base_file: Whether to ignore filenames in the base URL.
|
|
54
|
+
When True, such filenames are removed from the joined URL.
|
|
55
|
+
For example, joining `https://host.of.app/path/to/app.wsgi` with `endpoint` would yield `https://host.of.app/path/to/endpoint`.
|
|
56
|
+
When False, such filenames are included in the joined URL.
|
|
57
|
+
:type ignore_base_file: bool
|
|
58
|
+
:param allow_fragments: Whether to allow fragment parts in the URLs.
|
|
59
|
+
This is only used when `ignore_base_file=True`, in which case this function falls back on `urllib.parse.urljoin` and `allow_fragments` is passed to that function call.
|
|
60
|
+
:type ignore_base_file: bool
|
|
61
|
+
"""
|
|
62
|
+
if ignore_base_file:
|
|
63
|
+
return parse.urljoin(base=base, url=url, allow_fragments=allow_fragments)
|
|
64
|
+
else:
|
|
65
|
+
# Make sure we have a scheme
|
|
66
|
+
if not (base.startswith("http://") or base.startswith("https://")):
|
|
67
|
+
base = f"https://{base}"
|
|
68
|
+
# If base ends with a /, remove it so we don't double-up when joining
|
|
69
|
+
if base.endswith("/"):
|
|
70
|
+
base = base[:-1]
|
|
71
|
+
# If url starts with a /, remove it so we don't double-up when joining
|
|
72
|
+
if url.startswith("/"):
|
|
73
|
+
url = url[1:]
|
|
74
|
+
return f"{base}/{url}"
|
|
75
|
+
|
|
76
|
+
# def gen_interface(game_id, core_config:ConfigSchema, logger:Optional[Logger]=None) -> Optional[Interface]:
|
|
77
|
+
# """Utility to set up an Interface object for use by the API, given a game_id.
|
|
78
|
+
|
|
79
|
+
# :param game_id: _description_
|
|
80
|
+
# :type game_id: _type_
|
|
81
|
+
# :return: _description_
|
|
82
|
+
# :rtype: _type_
|
|
83
|
+
# """
|
|
84
|
+
# ret_val = None
|
|
85
|
+
|
|
86
|
+
# _game_source : DataTableConfig = core_config.GameSourceMap.get(game_id, DataTableConfig.Default())
|
|
87
|
+
|
|
88
|
+
# if _game_source.Source is not None:
|
|
89
|
+
# # set up interface and request
|
|
90
|
+
# match _game_source.Source.Type.upper():
|
|
91
|
+
# case "MYSQL":
|
|
92
|
+
# ret_val = MySQLInterface(game_id, config=_game_source, fail_fast=False)
|
|
93
|
+
# if logger:
|
|
94
|
+
# logger.info(f"Using MySQLInterface for {game_id}")
|
|
95
|
+
# case "BIGQUERY":
|
|
96
|
+
# if logger:
|
|
97
|
+
# logger.info(f"Generating BigQueryInterface for {game_id}, from directory {os.getcwd()}...")
|
|
98
|
+
# ret_val = BigQueryInterface(game_id=game_id, config=_game_source, fail_fast=False)
|
|
99
|
+
# if logger:
|
|
100
|
+
# logger.info("Done")
|
|
101
|
+
# case _:
|
|
102
|
+
# ret_val = MySQLInterface(game_id, config=_game_source, fail_fast=False)
|
|
103
|
+
# if logger:
|
|
104
|
+
# logger.warning(f"Could not find a valid interface for {game_id}, defaulting to MySQL!")
|
|
105
|
+
# return ret_val
|
|
106
|
+
|
|
107
|
+
# def gen_coding_interface(game_id) -> Optional[CodingInterface]:
|
|
108
|
+
# """Utility to set up an Interface object for use by the API, given a game_id.
|
|
109
|
+
|
|
110
|
+
# :param game_id: _description_
|
|
111
|
+
# :type game_id: _type_
|
|
112
|
+
# :return: _description_
|
|
113
|
+
# :rtype: _type_
|
|
114
|
+
# """
|
|
115
|
+
# ret_val = None
|
|
116
|
+
|
|
117
|
+
# _core_config = ConfigSchema(name="Core Config", all_elements=core_settings)
|
|
118
|
+
# _game_source : GameSourceSchema = _core_config.GameSourceMap.get(game_id, GameSourceSchema.EmptySchema())
|
|
119
|
+
|
|
120
|
+
# if _game_source.Source is not None:
|
|
121
|
+
# # set up interface and request
|
|
122
|
+
# if _game_source.Source.Type == "BigQuery":
|
|
123
|
+
# ret_val = BigQueryCodingInterface(game_id=game_id, config=_core_config)
|
|
124
|
+
# current_app.logger.info(f"Using BigQueryCodingInterface for {game_id}")
|
|
125
|
+
# else:
|
|
126
|
+
# ret_val = BigQueryCodingInterface(game_id=game_id, config=_core_config)
|
|
127
|
+
# current_app.logger.warning(f"Could not find a valid interface for {game_id}, defaulting to BigQuery!")
|
|
128
|
+
# return ret_val
|
|
File without changes
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: opengamedata-api-utils
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Package of utilities for server-side scripts in OpenGameData.
|
|
5
|
+
Author: Ryan Wilkinson, Glenn Palmer, Daus Husaini
|
|
6
|
+
Author-email: Luke Swanson <superscription58@gmail.com>
|
|
7
|
+
Project-URL: Homepage, https://github.com/opengamedata/opengamedata-api-utils
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/opengamedata/opengamedata-api-utils/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: flask==2.3.3
|
|
16
|
+
Requires-Dist: flask-restful==0.3.10
|
|
17
|
+
Requires-Dist: requests==2.32.*
|
|
18
|
+
Requires-Dist: opengamedata-common>=2.0.0b6
|
|
19
|
+
|
|
20
|
+
# opengamedata-api-utils
|
|
21
|
+
|
|
22
|
+
Repository for utility server-side script and classes used by OpenGameData APIs.
|
|
23
|
+
|
|
24
|
+
## Contents
|
|
25
|
+
|
|
26
|
+
### Python Package
|
|
27
|
+
|
|
28
|
+
The `utils` repository contains a package that can be installed from `pip`, and places its classes under the `ogd.apis` namespace.
|
|
29
|
+
The available modules include:
|
|
30
|
+
|
|
31
|
+
* `HelloAPI.py` : Class for creating a "Hello, World" API to aid in testing deploys of other APIs.
|
|
32
|
+
* `schemas` : Contains a base class for server config schemas to handle version and debug-level config items
|
|
33
|
+
* `utils` : Contains helper classes for API requests and responses, as well as general-purpose parsing and setup functions.
|
|
34
|
+
|
|
35
|
+
### Data Store Utilities
|
|
36
|
+
|
|
37
|
+
The `store` directory contains a script for reindexing a folder of OGD datasets after new datasets have been added.
|
|
38
|
+
This index is used by the FileAPI to check what datasets are available on the server.
|
|
39
|
+
|
|
40
|
+
## Getting Started
|
|
41
|
+
|
|
42
|
+
### Hello World of Flask
|
|
43
|
+
|
|
44
|
+
Steps to run:
|
|
45
|
+
|
|
46
|
+
1. Check out latest `opengamedata-server`.
|
|
47
|
+
2. Run `pip install -r requirements.txt` to ensure you've got flask.
|
|
48
|
+
3. Run `flask run`.
|
|
49
|
+
4. Open localhost:5000 or localhost:5000/hello to see some really basic text output from the Flask server.
|
|
50
|
+
|
|
51
|
+
If Flask doesn't run, it's possible you'd need to first export FLASK_APP as an environment variable, set to "wsgi" (so in Bash, export FLASK_APP=wsgi).
|
|
52
|
+
However, the script is named wsgi.py specifically because Flask is supposed to auto-detect it. So if this issue ever did come up, please ping Luke so he can look into it.
|
|
53
|
+
|
|
54
|
+
## Testing in VS Code
|
|
55
|
+
|
|
56
|
+
Be sure to add the following to `.vscode/settings.json` in your local checkout:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
"python.testing.unittestEnabled": true,
|
|
60
|
+
"python.testing.unittestArgs": [
|
|
61
|
+
"-v",
|
|
62
|
+
"-s",
|
|
63
|
+
"./tests/cases",
|
|
64
|
+
"-t",
|
|
65
|
+
"./",
|
|
66
|
+
"-p",
|
|
67
|
+
"t_*.py"
|
|
68
|
+
],
|
|
69
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
requirements.txt
|
|
5
|
+
src/ogd/apis/HelloAPI.py
|
|
6
|
+
src/ogd/apis/__init__.py
|
|
7
|
+
src/ogd/apis/configs/ServerConfig.py
|
|
8
|
+
src/ogd/apis/configs/__init__.py
|
|
9
|
+
src/ogd/apis/models/APIRequest.py
|
|
10
|
+
src/ogd/apis/models/APIResponse.py
|
|
11
|
+
src/ogd/apis/models/__init__.py
|
|
12
|
+
src/ogd/apis/models/enums/RESTType.py
|
|
13
|
+
src/ogd/apis/models/enums/ResponseStatus.py
|
|
14
|
+
src/ogd/apis/models/enums/__init__.py
|
|
15
|
+
src/ogd/apis/resources/Hello.py
|
|
16
|
+
src/ogd/apis/resources/ParamHello.py
|
|
17
|
+
src/ogd/apis/utils/APIUtils.py
|
|
18
|
+
src/ogd/apis/utils/__init__.py
|
|
19
|
+
src/opengamedata_api_utils.egg-info/PKG-INFO
|
|
20
|
+
src/opengamedata_api_utils.egg-info/SOURCES.txt
|
|
21
|
+
src/opengamedata_api_utils.egg-info/dependency_links.txt
|
|
22
|
+
src/opengamedata_api_utils.egg-info/requires.txt
|
|
23
|
+
src/opengamedata_api_utils.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ogd
|