harp-proxy 0.5.0__py3-none-any.whl
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.
- harp/__init__.py +99 -0
- harp/__main__.py +4 -0
- harp/_logging.py +109 -0
- harp/asgi/__init__.py +7 -0
- harp/asgi/bridge/__init__.py +0 -0
- harp/asgi/bridge/requests.py +71 -0
- harp/asgi/bridge/responses.py +42 -0
- harp/asgi/events.py +91 -0
- harp/asgi/kernel.py +169 -0
- harp/asgi/tests/__init__.py +0 -0
- harp/asgi/tests/test_http_bridge_asgi_request.py +200 -0
- harp/commandline/__init__.py +89 -0
- harp/commandline/install.py +11 -0
- harp/commandline/migrations.py +84 -0
- harp/commandline/options/__init__.py +0 -0
- harp/commandline/options/server.py +77 -0
- harp/commandline/server.py +20 -0
- harp/commandline/start.py +110 -0
- harp/commandline/tests/__init__.py +0 -0
- harp/commandline/tests/test_server_options.py +18 -0
- harp/commandline/utils/__init__.py +0 -0
- harp/commandline/utils/_hacks.py +41 -0
- harp/commandline/utils/manager.py +131 -0
- harp/config/__init__.py +40 -0
- harp/config/adapters/__init__.py +0 -0
- harp/config/adapters/hypercorn.py +56 -0
- harp/config/application.py +78 -0
- harp/config/config.py +327 -0
- harp/config/events.py +60 -0
- harp/config/factories/__init__.py +0 -0
- harp/config/factories/kernel_factory.py +116 -0
- harp/config/factories/tests/__init__.py +0 -0
- harp/config/factories/tests/test_configuration.py +62 -0
- harp/config/factories/tests/test_settings.py +29 -0
- harp/config/settings/__init__.py +15 -0
- harp/config/settings/base.py +60 -0
- harp/config/settings/disabled.py +38 -0
- harp/config/settings/from_file.py +21 -0
- harp/config/settings/lazy.py +103 -0
- harp/config/settings/tests/__init__.py +0 -0
- harp/config/settings/tests/test_disabled.py +10 -0
- harp/config/settings/tests/test_lazy.py +143 -0
- harp/controllers/__init__.py +62 -0
- harp/controllers/default.py +24 -0
- harp/controllers/resolvers.py +37 -0
- harp/controllers/routing.py +128 -0
- harp/controllers/typing.py +7 -0
- harp/errors.py +29 -0
- harp/event_dispatcher.py +36 -0
- harp/http/__init__.py +21 -0
- harp/http/errors.py +28 -0
- harp/http/requests.py +115 -0
- harp/http/responses.py +46 -0
- harp/http/serializers.py +103 -0
- harp/http/tests/__init__.py +0 -0
- harp/http/tests/stubs.py +65 -0
- harp/http/tests/test_requests.py +189 -0
- harp/http/tests/test_requests_wrapped.py +83 -0
- harp/http/tests/test_utils_cookies.py +68 -0
- harp/http/typing/__init__.py +11 -0
- harp/http/typing/bridges.py +34 -0
- harp/http/typing/messages.py +33 -0
- harp/http/typing/serializers.py +15 -0
- harp/http/utils/__init__.py +7 -0
- harp/http/utils/cookies.py +20 -0
- harp/http/utils/methods.py +118 -0
- harp/meta/__init__.py +33 -0
- harp/models/__init__.py +23 -0
- harp/models/base.py +25 -0
- harp/models/blobs.py +55 -0
- harp/models/messages.py +15 -0
- harp/models/transactions.py +41 -0
- harp/settings.py +30 -0
- harp/tests/__init__.py +0 -0
- harp/tests/test_asgi_kernel.py +51 -0
- harp/tests/test_settings.py +32 -0
- harp/typing/__init__.py +21 -0
- harp/typing/global_settings.py +6 -0
- harp/typing/signs.py +11 -0
- harp/typing/storage.py +64 -0
- harp/utils/__init__.py +7 -0
- harp/utils/arguments.py +14 -0
- harp/utils/background.py +36 -0
- harp/utils/bytes.py +10 -0
- harp/utils/collections.py +62 -0
- harp/utils/commandline.py +49 -0
- harp/utils/dates.py +25 -0
- harp/utils/env.py +25 -0
- harp/utils/guids.py +4 -0
- harp/utils/identifiers.py +2 -0
- harp/utils/json.py +9 -0
- harp/utils/network.py +44 -0
- harp/utils/processes.py +14 -0
- harp/utils/testing/__init__.py +0 -0
- harp/utils/testing/applications.py +32 -0
- harp/utils/testing/benchmarking.py +110 -0
- harp/utils/testing/communicators/__init__.py +10 -0
- harp/utils/testing/communicators/asgi.py +64 -0
- harp/utils/testing/communicators/http.py +57 -0
- harp/utils/testing/databases.py +110 -0
- harp/utils/testing/http.py +101 -0
- harp/utils/testing/mixins/__init__.py +6 -0
- harp/utils/testing/mixins/controllers.py +68 -0
- harp/utils/testing/stub_api/__init__.py +33 -0
- harp/utils/tests/__init__.py +0 -0
- harp/utils/tests/test_collections.py +84 -0
- harp/utils/tests/test_dates.py +17 -0
- harp/utils/tests/test_env.py +28 -0
- harp/utils/tests/test_urls.py +6 -0
- harp/utils/tpdex.py +13 -0
- harp/utils/urls.py +5 -0
- harp/views/__init__.py +17 -0
- harp/views/json.py +62 -0
- harp/views/strings.py +14 -0
- harp_apps/__init__.py +1 -0
- harp_apps/contrib/__init__.py +1 -0
- harp_apps/contrib/sentry/__app__.py +27 -0
- harp_apps/contrib/sentry/__init__.py +1 -0
- harp_apps/contrib/sentry/settings.py +6 -0
- harp_apps/dashboard/__app__.py +39 -0
- harp_apps/dashboard/__init__.py +1 -0
- harp_apps/dashboard/controllers/__init__.py +145 -0
- harp_apps/dashboard/controllers/blobs.py +25 -0
- harp_apps/dashboard/controllers/overview.py +136 -0
- harp_apps/dashboard/controllers/system.py +78 -0
- harp_apps/dashboard/controllers/transactions.py +95 -0
- harp_apps/dashboard/filters/__init__.py +15 -0
- harp_apps/dashboard/filters/base.py +120 -0
- harp_apps/dashboard/filters/transaction_endpoint.py +21 -0
- harp_apps/dashboard/filters/transaction_flag.py +7 -0
- harp_apps/dashboard/filters/transaction_method.py +6 -0
- harp_apps/dashboard/filters/transaction_status.py +6 -0
- harp_apps/dashboard/filters/transaction_tpdex.py +5 -0
- harp_apps/dashboard/filters/utils.py +17 -0
- harp_apps/dashboard/schemas/__init__.py +3 -0
- harp_apps/dashboard/schemas/transactions_grouped_by.py +11 -0
- harp_apps/dashboard/settings.py +88 -0
- harp_apps/dashboard/tests/__init__.py +0 -0
- harp_apps/dashboard/tests/filters/__init__.py +0 -0
- harp_apps/dashboard/tests/filters/test_base.py +42 -0
- harp_apps/dashboard/tests/filters/test_utils.py +9 -0
- harp_apps/dashboard/tests/test_controllers_blobs.py +52 -0
- harp_apps/dashboard/tests/test_controllers_overview.py +100 -0
- harp_apps/dashboard/tests/test_controllers_transactions.py +83 -0
- harp_apps/dashboard/tests/test_settings.py +31 -0
- harp_apps/dashboard/tests/test_utils_dependencies.py +127 -0
- harp_apps/dashboard/tests/utils/__init__.py +0 -0
- harp_apps/dashboard/tests/utils/test_dates.py +77 -0
- harp_apps/dashboard/utils/__init__.py +0 -0
- harp_apps/dashboard/utils/dates.py +73 -0
- harp_apps/dashboard/utils/dependencies.py +30 -0
- harp_apps/dashboard/web/android-icon-144x144.png +0 -0
- harp_apps/dashboard/web/android-icon-192x192.png +0 -0
- harp_apps/dashboard/web/android-icon-36x36.png +0 -0
- harp_apps/dashboard/web/android-icon-48x48.png +0 -0
- harp_apps/dashboard/web/android-icon-72x72.png +0 -0
- harp_apps/dashboard/web/android-icon-96x96.png +0 -0
- harp_apps/dashboard/web/apple-icon-114x114.png +0 -0
- harp_apps/dashboard/web/apple-icon-120x120.png +0 -0
- harp_apps/dashboard/web/apple-icon-144x144.png +0 -0
- harp_apps/dashboard/web/apple-icon-152x152.png +0 -0
- harp_apps/dashboard/web/apple-icon-180x180.png +0 -0
- harp_apps/dashboard/web/apple-icon-57x57.png +0 -0
- harp_apps/dashboard/web/apple-icon-60x60.png +0 -0
- harp_apps/dashboard/web/apple-icon-72x72.png +0 -0
- harp_apps/dashboard/web/apple-icon-76x76.png +0 -0
- harp_apps/dashboard/web/apple-icon-precomposed.png +0 -0
- harp_apps/dashboard/web/apple-icon.png +0 -0
- harp_apps/dashboard/web/assets/dateFns-HtJmkpQQ.js +1 -0
- harp_apps/dashboard/web/assets/index-B3DOsDR-.js +535 -0
- harp_apps/dashboard/web/assets/index-CCrzvmeS.css +1 -0
- harp_apps/dashboard/web/assets/react-D_Tw0keN.js +32 -0
- harp_apps/dashboard/web/assets/reactQuery-CZkty2vN.js +1 -0
- harp_apps/dashboard/web/assets/reactRouter-DpNtGriS.js +29 -0
- harp_apps/dashboard/web/assets/recharts-rDo41veu.js +68 -0
- harp_apps/dashboard/web/assets/sentry-B_XI-0Ih.js +14 -0
- harp_apps/dashboard/web/assets/syntaxHighlighter-w3E7YOxW.js +13 -0
- harp_apps/dashboard/web/assets/ui-dyya_Ddv.js +4 -0
- harp_apps/dashboard/web/browserconfig.xml +2 -0
- harp_apps/dashboard/web/favicon-16x16.png +0 -0
- harp_apps/dashboard/web/favicon-32x32.png +0 -0
- harp_apps/dashboard/web/favicon-96x96.png +0 -0
- harp_apps/dashboard/web/favicon.ico +0 -0
- harp_apps/dashboard/web/index.html +38 -0
- harp_apps/dashboard/web/manifest.json +41 -0
- harp_apps/dashboard/web/ms-icon-144x144.png +0 -0
- harp_apps/dashboard/web/ms-icon-150x150.png +0 -0
- harp_apps/dashboard/web/ms-icon-310x310.png +0 -0
- harp_apps/dashboard/web/ms-icon-70x70.png +0 -0
- harp_apps/http_client/__app__.py +18 -0
- harp_apps/http_client/__init__.py +1 -0
- harp_apps/http_client/client.py +19 -0
- harp_apps/http_client/settings.py +40 -0
- harp_apps/http_client/tests/__init__.py +0 -0
- harp_apps/http_client/tests/test_settings.py +68 -0
- harp_apps/janitor/__app__.py +18 -0
- harp_apps/janitor/__init__.py +1 -0
- harp_apps/janitor/settings.py +7 -0
- harp_apps/janitor/tests/__init__.py +0 -0
- harp_apps/janitor/tests/test_worker.py +68 -0
- harp_apps/janitor/worker.py +112 -0
- harp_apps/proxy/__app__.py +38 -0
- harp_apps/proxy/__init__.py +1 -0
- harp_apps/proxy/controllers.py +196 -0
- harp_apps/proxy/events.py +3 -0
- harp_apps/proxy/settings.py +26 -0
- harp_apps/proxy/tests/__init__.py +0 -0
- harp_apps/proxy/tests/test_controllers_http_proxy.py +205 -0
- harp_apps/sqlalchemy_storage/__app__.py +37 -0
- harp_apps/sqlalchemy_storage/__init__.py +1 -0
- harp_apps/sqlalchemy_storage/constants.py +10 -0
- harp_apps/sqlalchemy_storage/migrations/env.py +76 -0
- harp_apps/sqlalchemy_storage/migrations/script.py.mako +26 -0
- harp_apps/sqlalchemy_storage/migrations/versions/20240609082234_248285939c4c_initial.py +147 -0
- harp_apps/sqlalchemy_storage/migrations/versions/20240614152802_0b4d9cb71c38_rename_apdex_to_tpdex.py +26 -0
- harp_apps/sqlalchemy_storage/models/__init__.py +32 -0
- harp_apps/sqlalchemy_storage/models/base.py +82 -0
- harp_apps/sqlalchemy_storage/models/blobs.py +58 -0
- harp_apps/sqlalchemy_storage/models/flags.py +32 -0
- harp_apps/sqlalchemy_storage/models/messages.py +69 -0
- harp_apps/sqlalchemy_storage/models/metrics.py +42 -0
- harp_apps/sqlalchemy_storage/models/tags.py +32 -0
- harp_apps/sqlalchemy_storage/models/transactions.py +172 -0
- harp_apps/sqlalchemy_storage/models/users.py +37 -0
- harp_apps/sqlalchemy_storage/optionals/__init__.py +0 -0
- harp_apps/sqlalchemy_storage/optionals/_base.py +18 -0
- harp_apps/sqlalchemy_storage/optionals/pg_trgm.py +101 -0
- harp_apps/sqlalchemy_storage/settings.py +13 -0
- harp_apps/sqlalchemy_storage/storage.py +538 -0
- harp_apps/sqlalchemy_storage/tests/__init__.py +0 -0
- harp_apps/sqlalchemy_storage/tests/test_application.py +35 -0
- harp_apps/sqlalchemy_storage/tests/test_migrations.py +8 -0
- harp_apps/sqlalchemy_storage/tests/test_models_base.py +74 -0
- harp_apps/sqlalchemy_storage/tests/test_optionals_pg_trgm.py +118 -0
- harp_apps/sqlalchemy_storage/tests/test_storage_tags.py +21 -0
- harp_apps/sqlalchemy_storage/tests/test_storage_transactions.py +30 -0
- harp_apps/sqlalchemy_storage/tests/test_storage_usage.py +14 -0
- harp_apps/sqlalchemy_storage/utils/__init__.py +0 -0
- harp_apps/sqlalchemy_storage/utils/dates.py +90 -0
- harp_apps/sqlalchemy_storage/utils/migrations.py +91 -0
- harp_apps/sqlalchemy_storage/utils/sql.py +20 -0
- harp_apps/sqlalchemy_storage/utils/testing/__init__.py +0 -0
- harp_apps/sqlalchemy_storage/utils/testing/database.py +34 -0
- harp_apps/sqlalchemy_storage/utils/testing/mixins.py +102 -0
- harp_apps/telemetry/__app__.py +37 -0
- harp_apps/telemetry/__init__.py +1 -0
- harp_apps/telemetry/manager.py +85 -0
- harp_apps/telemetry/tests/__init__.py +0 -0
- harp_apps/telemetry/tests/test_application.py +5 -0
- harp_apps/telemetry/tests/test_manager.py +54 -0
- harp_proxy-0.5.0.dist-info/LICENSE.rst +43 -0
- harp_proxy-0.5.0.dist-info/METADATA +187 -0
- harp_proxy-0.5.0.dist-info/RECORD +255 -0
- harp_proxy-0.5.0.dist-info/WHEEL +4 -0
- harp_proxy-0.5.0.dist-info/entry_points.txt +3 -0
harp/__init__.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The Core (:mod:`harp`) package is the root namespace of the Harp framework.
|
|
3
|
+
|
|
4
|
+
It mostly contains a reference to the :class:`Config` class, because it's the only object you need to start using Harp
|
|
5
|
+
using the python API (you don't *need* to use this API, configuration files should be enough for most use cases, but
|
|
6
|
+
if you want to, this is the starting point).
|
|
7
|
+
|
|
8
|
+
For convenience, the :func:`run` function is also available, which is a simple way to start the default server
|
|
9
|
+
implementation for your configuration object.
|
|
10
|
+
|
|
11
|
+
Example usage:
|
|
12
|
+
|
|
13
|
+
.. code-block:: python
|
|
14
|
+
|
|
15
|
+
from harp import Config, run
|
|
16
|
+
|
|
17
|
+
config = Config()
|
|
18
|
+
config.add_defaults()
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
run(config)
|
|
22
|
+
|
|
23
|
+
You can find more information about how configuration works in the :mod:`harp.config` module.
|
|
24
|
+
|
|
25
|
+
Contents
|
|
26
|
+
--------
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import os
|
|
31
|
+
from subprocess import check_output
|
|
32
|
+
|
|
33
|
+
from packaging.version import InvalidVersion, Version
|
|
34
|
+
|
|
35
|
+
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_version(version: str, /, *, default=None) -> Version:
|
|
39
|
+
try:
|
|
40
|
+
return Version(version)
|
|
41
|
+
except InvalidVersion:
|
|
42
|
+
if "-" in version:
|
|
43
|
+
return _parse_version(version.rsplit("-", 1)[0], default=default)
|
|
44
|
+
return default
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# last release
|
|
48
|
+
__title__ = "Core"
|
|
49
|
+
__version__ = "0.5.0"
|
|
50
|
+
__revision__ = __version__ # we can't commit the not yet known revision
|
|
51
|
+
|
|
52
|
+
# override with version.txt if available (after docker build for example)
|
|
53
|
+
if os.path.exists(os.path.join(ROOT_DIR, "version.txt")):
|
|
54
|
+
with open(os.path.join(ROOT_DIR, "version.txt")) as f:
|
|
55
|
+
__version__ = f.read().strip()
|
|
56
|
+
|
|
57
|
+
__parsed_version__ = _parse_version(__version__)
|
|
58
|
+
|
|
59
|
+
# override with current development version/revision if available (disabled in CI, for docs)
|
|
60
|
+
if not os.environ.get("CI", False) and os.path.exists(os.path.join(ROOT_DIR, ".git")):
|
|
61
|
+
__revision__ = check_output(["git", "rev-parse", "HEAD"], cwd=ROOT_DIR).decode("utf-8").strip()
|
|
62
|
+
try:
|
|
63
|
+
__version__ = (
|
|
64
|
+
check_output(["git", "describe", "--tags", "--always", "--dirty"], cwd=ROOT_DIR).decode("utf-8").strip()
|
|
65
|
+
)
|
|
66
|
+
__parsed_version__ = _parse_version(__version__, default=__parsed_version__)
|
|
67
|
+
except Exception:
|
|
68
|
+
__version__ = __revision__[:7]
|
|
69
|
+
|
|
70
|
+
from ._logging import get_logger # noqa: E402, isort: skip
|
|
71
|
+
from harp.config import Config # noqa: E402, isort: skip
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def run(config: Config):
|
|
75
|
+
"""
|
|
76
|
+
Run the default server using provided configuration.
|
|
77
|
+
|
|
78
|
+
:param config: Config
|
|
79
|
+
:return:
|
|
80
|
+
"""
|
|
81
|
+
import asyncio
|
|
82
|
+
|
|
83
|
+
from harp.config.adapters.hypercorn import HypercornAdapter
|
|
84
|
+
from harp.config.factories.kernel_factory import KernelFactory
|
|
85
|
+
|
|
86
|
+
factory = KernelFactory(config)
|
|
87
|
+
server = HypercornAdapter(factory)
|
|
88
|
+
return asyncio.run(server.serve())
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
"Config",
|
|
93
|
+
"ROOT_DIR",
|
|
94
|
+
"__revision__",
|
|
95
|
+
"__version__",
|
|
96
|
+
"__parsed_version__",
|
|
97
|
+
"get_logger",
|
|
98
|
+
"run",
|
|
99
|
+
]
|
harp/__main__.py
ADDED
harp/_logging.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import logging.config
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import structlog
|
|
6
|
+
|
|
7
|
+
shared_processors = [
|
|
8
|
+
structlog.stdlib.add_logger_name,
|
|
9
|
+
structlog.stdlib.add_log_level,
|
|
10
|
+
structlog.stdlib.ExtraAdder(),
|
|
11
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
processors = [
|
|
15
|
+
structlog.stdlib.filter_by_level,
|
|
16
|
+
structlog.contextvars.merge_contextvars,
|
|
17
|
+
*shared_processors,
|
|
18
|
+
structlog.dev.set_exc_info,
|
|
19
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
20
|
+
structlog.processors.StackInfoRenderer(),
|
|
21
|
+
structlog.processors.UnicodeDecoder(),
|
|
22
|
+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
_exc_formatter_options = {"width": os.get_terminal_size()[0]}
|
|
27
|
+
except OSError:
|
|
28
|
+
_exc_formatter_options = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
LOGGING_FORMATTERS = {
|
|
32
|
+
"json": {
|
|
33
|
+
"()": structlog.stdlib.ProcessorFormatter,
|
|
34
|
+
"processor": structlog.processors.JSONRenderer(),
|
|
35
|
+
"foreign_pre_chain": shared_processors,
|
|
36
|
+
},
|
|
37
|
+
"plain": {
|
|
38
|
+
"()": structlog.stdlib.ProcessorFormatter,
|
|
39
|
+
"processor": structlog.dev.ConsoleRenderer(exception_formatter=structlog.dev.plain_traceback, colors=False),
|
|
40
|
+
"foreign_pre_chain": shared_processors,
|
|
41
|
+
},
|
|
42
|
+
"pretty": {
|
|
43
|
+
"()": structlog.stdlib.ProcessorFormatter,
|
|
44
|
+
"processor": structlog.dev.ConsoleRenderer(
|
|
45
|
+
exception_formatter=structlog.dev.RichTracebackFormatter(**_exc_formatter_options)
|
|
46
|
+
),
|
|
47
|
+
"foreign_pre_chain": shared_processors,
|
|
48
|
+
},
|
|
49
|
+
"keyvalue": {
|
|
50
|
+
"()": structlog.stdlib.ProcessorFormatter,
|
|
51
|
+
"processor": structlog.processors.KeyValueRenderer(key_order=["timestamp", "level", "event", "logger"]),
|
|
52
|
+
"foreign_pre_chain": shared_processors,
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
DEFAULT_LOGGING_FORMAT = "pretty"
|
|
57
|
+
LOGGING_FORMAT = os.environ.get("LOGGING_FORMAT", DEFAULT_LOGGING_FORMAT)
|
|
58
|
+
if LOGGING_FORMAT not in LOGGING_FORMATTERS:
|
|
59
|
+
LOGGING_FORMAT = DEFAULT_LOGGING_FORMAT
|
|
60
|
+
|
|
61
|
+
logging_config = {
|
|
62
|
+
"version": 1,
|
|
63
|
+
"disable_existing_loggers": True,
|
|
64
|
+
"formatters": LOGGING_FORMATTERS,
|
|
65
|
+
"handlers": {
|
|
66
|
+
"console": {
|
|
67
|
+
"class": "logging.StreamHandler",
|
|
68
|
+
"formatter": LOGGING_FORMAT,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
"root": {
|
|
72
|
+
"handlers": ["console"],
|
|
73
|
+
"level": logging.INFO,
|
|
74
|
+
},
|
|
75
|
+
"loggers": {
|
|
76
|
+
"harp": {"level": os.environ.get("LOGGING_HARP", "INFO")},
|
|
77
|
+
"harp_apps": {"level": os.environ.get("LOGGING_HARP", "INFO")},
|
|
78
|
+
"harp.event_dispatcher": {"level": os.environ.get("LOGGING_HARP_EVENTS", "WARNING")},
|
|
79
|
+
"httpcore": {"level": os.environ.get("LOGGING_HTTP", "WARNING")}, # todo wrap in structlog
|
|
80
|
+
"httpx": {"level": os.environ.get("LOGGING_HTTP", "WARNING")}, # todo wrap in structlog
|
|
81
|
+
"hypercorn.access": {"level": os.environ.get("LOGGING_HYPERCORN_ACCESS", "WARNING")},
|
|
82
|
+
"hypercorn.error": {"level": os.environ.get("LOGGING_HYPERCORN_ERROR", "INFO")},
|
|
83
|
+
"sqlalchemy.engine": {"level": os.environ.get("LOGGING_SQLALCHEMY", "WARNING")},
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
logging.config.dictConfig(logging_config)
|
|
89
|
+
|
|
90
|
+
structlog.configure(
|
|
91
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
92
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
93
|
+
processors=processors,
|
|
94
|
+
cache_logger_on_first_use=True,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_logger(name, *args: Any, **initial_values: Any) -> Any:
|
|
99
|
+
if name == "env_py":
|
|
100
|
+
name = "__migrations__"
|
|
101
|
+
else:
|
|
102
|
+
pkg, mod = name.rsplit(".", 1)
|
|
103
|
+
if mod in ("__init__", "__main__", "__app__"):
|
|
104
|
+
name = pkg
|
|
105
|
+
|
|
106
|
+
return structlog.get_logger(name, *args, **initial_values)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
__all__ = ["get_logger"]
|
harp/asgi/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from typing import AsyncIterator, Optional, cast
|
|
2
|
+
from urllib.parse import parse_qsl
|
|
3
|
+
|
|
4
|
+
from asgiref.typing import ASGIReceiveCallable, HTTPScope
|
|
5
|
+
from multidict import CIMultiDict, MultiDict
|
|
6
|
+
|
|
7
|
+
from harp.http import HttpRequestBridge
|
|
8
|
+
|
|
9
|
+
_default_host = (None, None)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HttpRequestAsgiBridge(HttpRequestBridge):
|
|
13
|
+
"""Actually implements the getters required by HttpRequest using the asgi scope and receive callable. It is still
|
|
14
|
+
an early implementation and will need to support streaming requests in the future."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, scope: HTTPScope, receive: ASGIReceiveCallable):
|
|
17
|
+
"""
|
|
18
|
+
:param scope: see https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope
|
|
19
|
+
:param receive:
|
|
20
|
+
"""
|
|
21
|
+
self.asgi_scope = scope
|
|
22
|
+
self.asgi_receive = receive
|
|
23
|
+
|
|
24
|
+
self._closed = False
|
|
25
|
+
|
|
26
|
+
def get_server_ipaddr(self) -> Optional[str]:
|
|
27
|
+
"""Get the server IP address from asgi scope."""
|
|
28
|
+
try:
|
|
29
|
+
return (self.asgi_scope.get("server", _default_host) or _default_host)[0]
|
|
30
|
+
except IndexError:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
def get_server_port(self) -> Optional[int]:
|
|
34
|
+
"""Get the server port from asgi scope."""
|
|
35
|
+
try:
|
|
36
|
+
return (self.asgi_scope.get("server", _default_host) or _default_host)[1]
|
|
37
|
+
except IndexError:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
def get_method(self) -> str:
|
|
41
|
+
"""Get the HTTP method from asgi scope."""
|
|
42
|
+
return self.asgi_scope.get("method", None)
|
|
43
|
+
|
|
44
|
+
def get_path(self) -> str:
|
|
45
|
+
"""Get the HTTP path from asgi scope (/foo/bar), without the query string part."""
|
|
46
|
+
if "raw_path" in self.asgi_scope:
|
|
47
|
+
return self.asgi_scope["raw_path"].decode("utf-8")
|
|
48
|
+
return self.asgi_scope.get("path", "/")
|
|
49
|
+
|
|
50
|
+
def get_query(self) -> MultiDict:
|
|
51
|
+
"""Get the query string from asgi scope, as a multidict."""
|
|
52
|
+
return MultiDict(parse_qsl(self.asgi_scope.get("query_string", b"").decode("utf-8"), keep_blank_values=True))
|
|
53
|
+
|
|
54
|
+
def get_headers(self) -> CIMultiDict:
|
|
55
|
+
"""Get the headers from asgi scope, as a case-insensitive multidict."""
|
|
56
|
+
headers = CIMultiDict()
|
|
57
|
+
for name, value in self.asgi_scope.get("headers", []):
|
|
58
|
+
headers.add(name.decode("utf-8"), value.decode("utf-8"))
|
|
59
|
+
return headers
|
|
60
|
+
|
|
61
|
+
async def stream(self) -> AsyncIterator[bytes]:
|
|
62
|
+
"""Returns an async iterator that reads the request body chunks. This can only be used once, as it delegates to
|
|
63
|
+
the ASGI receive callable."""
|
|
64
|
+
if self._closed:
|
|
65
|
+
raise RuntimeError("Request body has already been read.")
|
|
66
|
+
|
|
67
|
+
# TODO max iter to avoid infinite loop on erroneous proto content?
|
|
68
|
+
while not self._closed:
|
|
69
|
+
message = await self.asgi_receive()
|
|
70
|
+
self._closed = not message.get("more_body", False)
|
|
71
|
+
yield cast(bytes, message.get("body", b""))
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from asgiref.typing import ASGISendCallable
|
|
4
|
+
from multidict import CIMultiDictProxy
|
|
5
|
+
|
|
6
|
+
from harp.utils.bytes import ensure_bytes
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from harp.http import HttpResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HttpResponseAsgiBridge: # todo protocol HttpResponseBridge
|
|
13
|
+
"""Implements the ability of sending our HttpResponse object over the asgi protocol. This is still early impl. and
|
|
14
|
+
it will need to support streaming responses in the future."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, response: "HttpResponse", send: ASGISendCallable):
|
|
17
|
+
self.response = response
|
|
18
|
+
self.asgi_send = send
|
|
19
|
+
|
|
20
|
+
async def send(self):
|
|
21
|
+
# set the headers as read only
|
|
22
|
+
self.response._headers = CIMultiDictProxy(self.response._headers)
|
|
23
|
+
|
|
24
|
+
# prepare headers for asgi
|
|
25
|
+
headers = tuple((ensure_bytes(k), ensure_bytes(v)) for k, v in self.response.headers.items())
|
|
26
|
+
|
|
27
|
+
# start the response
|
|
28
|
+
await self.asgi_send(
|
|
29
|
+
{
|
|
30
|
+
"type": "http.response.start",
|
|
31
|
+
"status": self.response.status,
|
|
32
|
+
"headers": headers,
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# send the body
|
|
37
|
+
await self.asgi_send(
|
|
38
|
+
{
|
|
39
|
+
"type": "http.response.body",
|
|
40
|
+
"body": ensure_bytes(self.response.body),
|
|
41
|
+
}
|
|
42
|
+
)
|
harp/asgi/events.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
3
|
+
|
|
4
|
+
from whistle import Event
|
|
5
|
+
|
|
6
|
+
from harp.http import BaseMessage, HttpRequest
|
|
7
|
+
from harp.models.transactions import Transaction
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from harp.http import HttpResponse
|
|
11
|
+
|
|
12
|
+
EVENT_CORE_STARTED = "core.started"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RequestEvent(Event):
|
|
16
|
+
name = "core.request"
|
|
17
|
+
|
|
18
|
+
def __init__(self, request: HttpRequest):
|
|
19
|
+
self._request = request
|
|
20
|
+
self._controller = None
|
|
21
|
+
|
|
22
|
+
@cached_property
|
|
23
|
+
def request(self):
|
|
24
|
+
return self._request
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def controller(self):
|
|
28
|
+
return self._controller
|
|
29
|
+
|
|
30
|
+
def set_controller(self, controller):
|
|
31
|
+
self._controller = controller
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
EVENT_CORE_REQUEST = RequestEvent.name
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ControllerEvent(RequestEvent):
|
|
38
|
+
name = "core.controller"
|
|
39
|
+
|
|
40
|
+
def __init__(self, request: HttpRequest, controller):
|
|
41
|
+
super().__init__(request)
|
|
42
|
+
self._controller = controller
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
EVENT_CORE_CONTROLLER = ControllerEvent.name
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ResponseEvent(RequestEvent):
|
|
49
|
+
name = "core.response"
|
|
50
|
+
|
|
51
|
+
def __init__(self, request: HttpRequest, response):
|
|
52
|
+
super().__init__(request)
|
|
53
|
+
self.response = response
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
EVENT_CORE_RESPONSE = ResponseEvent.name
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ControllerViewEvent(RequestEvent):
|
|
60
|
+
"""
|
|
61
|
+
The view event allows to transform controller return values into response objects.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
name = "controller.view"
|
|
65
|
+
|
|
66
|
+
value: Any
|
|
67
|
+
response: Optional["HttpResponse"]
|
|
68
|
+
|
|
69
|
+
def __init__(self, request: HttpRequest, value: Any):
|
|
70
|
+
super().__init__(request)
|
|
71
|
+
|
|
72
|
+
self.value = value
|
|
73
|
+
self.response = None
|
|
74
|
+
|
|
75
|
+
def set_response(self, response: "HttpResponse"):
|
|
76
|
+
self.response = response
|
|
77
|
+
self.stop_propagation()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
EVENT_CONTROLLER_VIEW = ControllerViewEvent.name
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class TransactionEvent(Event):
|
|
84
|
+
def __init__(self, transaction: Transaction):
|
|
85
|
+
self.transaction = transaction
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class MessageEvent(TransactionEvent):
|
|
89
|
+
def __init__(self, transaction: Transaction, message: BaseMessage):
|
|
90
|
+
super().__init__(transaction)
|
|
91
|
+
self.message = message
|
harp/asgi/kernel.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
from inspect import signature
|
|
3
|
+
|
|
4
|
+
from asgiref.typing import ASGIReceiveCallable, ASGISendCallable, Scope
|
|
5
|
+
from hypercorn.utils import LifespanFailureError
|
|
6
|
+
from whistle import AsyncEventDispatcher, Event, IAsyncEventDispatcher
|
|
7
|
+
|
|
8
|
+
from harp import get_logger
|
|
9
|
+
from harp.controllers import DefaultControllerResolver
|
|
10
|
+
from harp.http import AlreadyHandledHttpResponse, HttpRequest, HttpResponse
|
|
11
|
+
|
|
12
|
+
from .bridge.requests import HttpRequestAsgiBridge
|
|
13
|
+
from .bridge.responses import HttpResponseAsgiBridge
|
|
14
|
+
from .events import (
|
|
15
|
+
EVENT_CONTROLLER_VIEW,
|
|
16
|
+
EVENT_CORE_CONTROLLER,
|
|
17
|
+
EVENT_CORE_REQUEST,
|
|
18
|
+
EVENT_CORE_RESPONSE,
|
|
19
|
+
EVENT_CORE_STARTED,
|
|
20
|
+
ControllerEvent,
|
|
21
|
+
ControllerViewEvent,
|
|
22
|
+
RequestEvent,
|
|
23
|
+
ResponseEvent,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ASGIKernel:
|
|
30
|
+
dispatcher: IAsyncEventDispatcher
|
|
31
|
+
|
|
32
|
+
def __init__(self, *, dispatcher=None, resolver=None, debug=False, handle_errors=True):
|
|
33
|
+
self.dispatcher = dispatcher or AsyncEventDispatcher()
|
|
34
|
+
# TODO IControllerResolver ? What contract do we expect ?
|
|
35
|
+
self.resolver = resolver or DefaultControllerResolver()
|
|
36
|
+
self.started = False
|
|
37
|
+
self.debug = debug
|
|
38
|
+
self.handle_errors = handle_errors
|
|
39
|
+
|
|
40
|
+
async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
|
|
41
|
+
asgi_type = scope.get("type", None)
|
|
42
|
+
|
|
43
|
+
if asgi_type == "http":
|
|
44
|
+
response = await self.handle_http(scope, receive, send)
|
|
45
|
+
if isinstance(response, AlreadyHandledHttpResponse):
|
|
46
|
+
return
|
|
47
|
+
return await HttpResponseAsgiBridge(response, send).send()
|
|
48
|
+
|
|
49
|
+
if asgi_type == "lifespan":
|
|
50
|
+
await receive()
|
|
51
|
+
try:
|
|
52
|
+
await self.dispatcher.adispatch(EVENT_CORE_STARTED, Event())
|
|
53
|
+
except Exception as exc:
|
|
54
|
+
raise LifespanFailureError(EVENT_CORE_STARTED, repr(exc)) from exc
|
|
55
|
+
|
|
56
|
+
self.started = True
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
if asgi_type == "websocket":
|
|
60
|
+
# NOT IMPLEMENTED YET!
|
|
61
|
+
# This is ignored here to avoid huge errors in the console.
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
raise RuntimeError(f'Unable to handle request, invalid type "{asgi_type}".')
|
|
65
|
+
|
|
66
|
+
def _resolve_arguments(self, subject, **candidates):
|
|
67
|
+
"""
|
|
68
|
+
Dynamicaly resolve arguments by names in prototype, looking at a "candidates" dictionary that
|
|
69
|
+
contains the possible arguments used.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
while hasattr(subject, "_mock_wraps"):
|
|
73
|
+
subject = subject._mock_wraps
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
return [
|
|
77
|
+
candidates[name] if name in candidates or param.default is param.empty else param.default
|
|
78
|
+
for name, param in signature(subject).parameters.items()
|
|
79
|
+
if param.kind is not param.KEYWORD_ONLY
|
|
80
|
+
], {}
|
|
81
|
+
except KeyError as exc:
|
|
82
|
+
raise TypeError(f"Unable to resolve arguments: {exc}")
|
|
83
|
+
|
|
84
|
+
async def _resolve_http_controller(self, request: HttpRequest):
|
|
85
|
+
"""
|
|
86
|
+
Resolves the controller for the given request, aka the callable that will handle the request. If we fail to do
|
|
87
|
+
so, we will have hard time building a response for the user, and thus we raise an error.
|
|
88
|
+
|
|
89
|
+
Resolving the controller involves two steps:
|
|
90
|
+
- first, we use `self.resolver` to find a controller for the request.
|
|
91
|
+
See `harp.controllers.ControllerResolver`
|
|
92
|
+
|
|
93
|
+
:param request:
|
|
94
|
+
:return:
|
|
95
|
+
"""
|
|
96
|
+
controller = await self.resolver.resolve(request)
|
|
97
|
+
if not controller:
|
|
98
|
+
raise RuntimeError("Unable to find request controller using resolver.")
|
|
99
|
+
|
|
100
|
+
event = ControllerEvent(request, controller)
|
|
101
|
+
await self.dispatcher.adispatch(EVENT_CORE_CONTROLLER, event)
|
|
102
|
+
|
|
103
|
+
if not event.controller:
|
|
104
|
+
raise RuntimeError("Unable to find request controller after controller event dispatch.")
|
|
105
|
+
|
|
106
|
+
return event.controller
|
|
107
|
+
|
|
108
|
+
async def _execute_http_controller(self, controller, request: HttpRequest, send: ASGISendCallable):
|
|
109
|
+
"""Executes the given controller and make all efforts to build a http response."""
|
|
110
|
+
args, kwargs = self._resolve_arguments(controller, request=request, asgi_send=send)
|
|
111
|
+
response = await controller(*args, **kwargs)
|
|
112
|
+
|
|
113
|
+
# if the controller returned anything that is not a response, maybe some view listener can create one from this
|
|
114
|
+
# value (for example for json data, etc.).
|
|
115
|
+
if not isinstance(response, HttpResponse):
|
|
116
|
+
event = ControllerViewEvent(request, response)
|
|
117
|
+
await self.dispatcher.adispatch(EVENT_CONTROLLER_VIEW, event)
|
|
118
|
+
response = event.response
|
|
119
|
+
|
|
120
|
+
# if after the view event has been dispatched we still don't have a response, we have to raise an error.
|
|
121
|
+
if not isinstance(response, HttpResponse):
|
|
122
|
+
raise RuntimeError(
|
|
123
|
+
f"Response did not start despite the efforts made (controller return value is "
|
|
124
|
+
f"{type(response).__name__} after controller view event was dispatched)."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# the core response event may want to filter the response, for example to add some headers, etc.
|
|
128
|
+
await self.dispatcher.adispatch(EVENT_CORE_RESPONSE, ResponseEvent(request, response))
|
|
129
|
+
|
|
130
|
+
return response
|
|
131
|
+
|
|
132
|
+
async def handle_http(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
|
|
133
|
+
request = HttpRequest(HttpRequestAsgiBridge(scope, receive))
|
|
134
|
+
|
|
135
|
+
if not self.started:
|
|
136
|
+
return HttpResponse(
|
|
137
|
+
"Unhandled server error: Cannot access service provider, the lifespan.startup asgi event "
|
|
138
|
+
"probably never went through.",
|
|
139
|
+
status=500,
|
|
140
|
+
content_type="text/plain",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
return await self.do_handle_http(request, send)
|
|
145
|
+
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
if not self.handle_errors:
|
|
148
|
+
raise
|
|
149
|
+
|
|
150
|
+
# todo refactor this
|
|
151
|
+
logger.exception()
|
|
152
|
+
if self.debug:
|
|
153
|
+
return HttpResponse(
|
|
154
|
+
f"<h1>Internal Server Error</h1><h2>{type(exc).__name__}: {exc}</h2> "
|
|
155
|
+
f"<pre>{traceback.format_exc()}</pre>",
|
|
156
|
+
status=500,
|
|
157
|
+
content_type="text/html",
|
|
158
|
+
)
|
|
159
|
+
return HttpResponse("Internal Server Error", status=500, content_type="text/plain")
|
|
160
|
+
|
|
161
|
+
async def do_handle_http(self, request: HttpRequest, send: ASGISendCallable):
|
|
162
|
+
event = RequestEvent(request)
|
|
163
|
+
await self.dispatcher.adispatch(EVENT_CORE_REQUEST, event)
|
|
164
|
+
|
|
165
|
+
if event.controller:
|
|
166
|
+
return await self._execute_http_controller(event.controller, request, send)
|
|
167
|
+
|
|
168
|
+
controller = await self._resolve_http_controller(request)
|
|
169
|
+
return await self._execute_http_controller(controller, request, send)
|
|
File without changes
|