keepup-http 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.
- keepup_http/__init__.py +38 -0
- keepup_http/api_docs.py +76 -0
- keepup_http/body_limit.py +156 -0
- keepup_http/events_api.py +237 -0
- keepup_http/factory.py +562 -0
- keepup_http/lock_routes.py +155 -0
- keepup_http/plugin.py +53 -0
- keepup_http/routes.py +549 -0
- keepup_http/static/css/aos.css +1 -0
- keepup_http/static/css/main_nebula.css +232 -0
- keepup_http/static/css/main_new.css +869 -0
- keepup_http/static/css/tailwind.css +1 -0
- keepup_http/static/index_nebula.html +282 -0
- keepup_http/static/index_new.html +275 -0
- keepup_http/static/js/aos.js +1 -0
- keepup_http/static/js/feather-icons.js +13 -0
- keepup_http/static/js/layout_bootstrap.js +29 -0
- keepup_http/static/js/main_new.js +2076 -0
- keepup_http/static/js/tailwind.js +83 -0
- keepup_http/static/modules/css/background_tasks.css +233 -0
- keepup_http/static/modules/css/cluster.css +16 -0
- keepup_http/static/modules/css/event_manager.css +386 -0
- keepup_http/static/modules/css/integration_logs.css +33 -0
- keepup_http/static/modules/css/metrics.css +115 -0
- keepup_http/static/modules/css/modules.css +189 -0
- keepup_http/static/modules/css/themes.css +563 -0
- keepup_http/static/modules/css/users.css +278 -0
- keepup_http/static/modules/js/background_tasks.js +695 -0
- keepup_http/static/modules/js/chart.js +14 -0
- keepup_http/static/modules/js/chartjs-adapter-date-fns.bundle.min.js +7 -0
- keepup_http/static/modules/js/cluster.js +374 -0
- keepup_http/static/modules/js/event_manager.js +1007 -0
- keepup_http/static/modules/js/integration_logs.js +795 -0
- keepup_http/static/modules/js/metrics.js +929 -0
- keepup_http/static/modules/js/modules.js +1128 -0
- keepup_http/static/modules/js/themes.js +684 -0
- keepup_http/static/modules/js/users.js +898 -0
- keepup_http/web.py +214 -0
- keepup_http-0.5.0.dist-info/METADATA +29 -0
- keepup_http-0.5.0.dist-info/RECORD +43 -0
- keepup_http-0.5.0.dist-info/WHEEL +5 -0
- keepup_http-0.5.0.dist-info/entry_points.txt +2 -0
- keepup_http-0.5.0.dist-info/top_level.txt +1 -0
keepup_http/web.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""The pages and the health of the application itself.
|
|
2
|
+
|
|
3
|
+
Everything here is served from the working directory, not from the package:
|
|
4
|
+
the front end and the version file are data of the deployment, and the
|
|
5
|
+
application says where they are through ``configure()``. The public health
|
|
6
|
+
answer deliberately carries nothing about the database beyond whether it
|
|
7
|
+
answered -- the detailed one is administrative.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from fastapi import Depends, HTTPException
|
|
15
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
16
|
+
|
|
17
|
+
from keepup import clock
|
|
18
|
+
from keepup.api_versions import describe
|
|
19
|
+
from keepup_audit.audit import incoming_requests_buffer
|
|
20
|
+
from keepup_auth.dependencies import get_current_admin
|
|
21
|
+
from keepup.instance import get_instance_id
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _replica_state() -> str:
|
|
25
|
+
"""Whether this replica serves, from the cluster capability if it is there.
|
|
26
|
+
|
|
27
|
+
A deployment that never installed `keepup-cluster` has one replica and no
|
|
28
|
+
state to report (keepup-117).
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
from keepup_cluster.cluster import current_state
|
|
32
|
+
except ImportError:
|
|
33
|
+
return "serving"
|
|
34
|
+
return current_state()
|
|
35
|
+
from keepup_ui.themes import config_service
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
#: Where the front end lives, relative to the working directory.
|
|
40
|
+
STATIC_DIR = "static"
|
|
41
|
+
CLIENT_PAGE = "main.html"
|
|
42
|
+
VERSION_FILE = "version.json"
|
|
43
|
+
FAVICON_FILE = "favicon.ico"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
#: The package's own front end -- the panel shell. Resolved from this file's
|
|
47
|
+
#: location and never from the working directory: it is data of the package, and
|
|
48
|
+
#: the process may have been started anywhere.
|
|
49
|
+
SHELL_DIR = str(Path(__file__).resolve().parent / "static")
|
|
50
|
+
|
|
51
|
+
#: The page served when the active theme names a file that may not be served.
|
|
52
|
+
#: Not an error page: an administrator who broke the theme still has to be able
|
|
53
|
+
#: to reach the panel and fix it.
|
|
54
|
+
DEFAULT_PANEL_PAGE = "index_new.html"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def static_page(name):
|
|
58
|
+
"""Resolve a page name against the front ends, refusing to leave either.
|
|
59
|
+
|
|
60
|
+
Two directories, in this order: the application's, then the package's. The
|
|
61
|
+
application first because a theme of its own must be able to replace one of
|
|
62
|
+
the package's by name; the package second because that is where the shipped
|
|
63
|
+
panel lives once keepup is installed rather than sitting in the repository.
|
|
64
|
+
|
|
65
|
+
The name reaches here from the `visual_themes` table, so it is data, not a
|
|
66
|
+
constant of the code: `os.path.join(STATIC_DIR, "../config/auth.yaml")` is
|
|
67
|
+
a perfectly ordinary path, and FileResponse would serve it. Containment is
|
|
68
|
+
checked after resolution, so that `..`, a symbolic link and an absolute
|
|
69
|
+
name are all refused by the same rule.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name: the file name as it was stored.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The path to serve, or None when the name leads outside both directories
|
|
76
|
+
or names nothing in either.
|
|
77
|
+
"""
|
|
78
|
+
if not name:
|
|
79
|
+
return None
|
|
80
|
+
for root_dir in (STATIC_DIR, SHELL_DIR):
|
|
81
|
+
root = os.path.realpath(root_dir)
|
|
82
|
+
candidate = os.path.realpath(os.path.join(root_dir, name))
|
|
83
|
+
if candidate != root and not candidate.startswith(root + os.sep):
|
|
84
|
+
continue
|
|
85
|
+
if os.path.isfile(candidate):
|
|
86
|
+
return candidate
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def theme_page():
|
|
91
|
+
"""The active theme's page, or the default one when it may not be served."""
|
|
92
|
+
name = config_service.get_active_theme_page_file()
|
|
93
|
+
path = static_page(name)
|
|
94
|
+
if path is None:
|
|
95
|
+
logger.error(
|
|
96
|
+
"The active theme names a page that is outside the front ends or "
|
|
97
|
+
"absent from both (%r); serving %s instead", name, DEFAULT_PANEL_PAGE)
|
|
98
|
+
return os.path.join(SHELL_DIR, DEFAULT_PANEL_PAGE)
|
|
99
|
+
return path
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
#: The panel pages are revalidated like the shell's assets (GatedStaticFiles
|
|
103
|
+
#: says the same for them): without it a browser invents a freshness lifetime
|
|
104
|
+
#: and keeps serving the page it cached before the upgrade -- and a page cached
|
|
105
|
+
#: before keepup-93 carries inline handlers, which the Content-Security-Policy
|
|
106
|
+
#: it is now served with refuses.
|
|
107
|
+
PAGE_HEADERS = {"Cache-Control": "no-cache"}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def page_response(path):
|
|
111
|
+
"""A panel page, with the revalidation its assets already carry."""
|
|
112
|
+
return FileResponse(path, headers=PAGE_HEADERS)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def configure(static_dir=None, client_page=None, version_file=None, favicon_file=None):
|
|
116
|
+
"""Supply the application's front-end paths."""
|
|
117
|
+
global STATIC_DIR, CLIENT_PAGE, VERSION_FILE, FAVICON_FILE
|
|
118
|
+
if static_dir is not None:
|
|
119
|
+
STATIC_DIR = static_dir
|
|
120
|
+
if client_page is not None:
|
|
121
|
+
CLIENT_PAGE = client_page
|
|
122
|
+
if version_file is not None:
|
|
123
|
+
VERSION_FILE = version_file
|
|
124
|
+
if favicon_file is not None:
|
|
125
|
+
FAVICON_FILE = favicon_file
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def register_web_routes(app):
|
|
130
|
+
"""Register the pages, the version and the health endpoints."""
|
|
131
|
+
|
|
132
|
+
@app.get("/", response_class=FileResponse)
|
|
133
|
+
async def read_root():
|
|
134
|
+
return page_response(os.path.join(STATIC_DIR, CLIENT_PAGE))
|
|
135
|
+
@app.get("/selfcare", response_class=FileResponse)
|
|
136
|
+
async def read_root_selfcare():
|
|
137
|
+
"""Return the main page of the active visual theme."""
|
|
138
|
+
file_path = theme_page()
|
|
139
|
+
logger.debug(f"Serving {file_path} for selfcare page")
|
|
140
|
+
return page_response(file_path)
|
|
141
|
+
@app.get("/api/version", response_class=FileResponse)
|
|
142
|
+
async def get_version():
|
|
143
|
+
return FileResponse(os.path.join(STATIC_DIR, VERSION_FILE))
|
|
144
|
+
@app.get("/selfcare/modules/{path:path}")
|
|
145
|
+
async def serve_modules_app(path: str):
|
|
146
|
+
"""Serve every path under /modules to the frontend."""
|
|
147
|
+
file_path = theme_page()
|
|
148
|
+
logger.debug(f"Serving {file_path} for selfcare page")
|
|
149
|
+
return page_response(file_path)
|
|
150
|
+
@app.get("/favicon.ico", include_in_schema=False)
|
|
151
|
+
async def favicon():
|
|
152
|
+
return FileResponse(os.path.join(STATIC_DIR, FAVICON_FILE))
|
|
153
|
+
@app.get("/api/health")
|
|
154
|
+
def health_check():
|
|
155
|
+
"""Whether this instance is up and reaches its database -- and nothing more.
|
|
156
|
+
|
|
157
|
+
The answer is public: the deployment proxies it to the world. It used to carry
|
|
158
|
+
the database connection string, which named the host, port, database and user
|
|
159
|
+
inside the stand's network -- a network deliberately kept behind the public
|
|
160
|
+
proxy. Details are for an administrator, at /api/admin/health.
|
|
161
|
+
|
|
162
|
+
A replica an administrator stopped answers 503 "stopped": that is what
|
|
163
|
+
makes a load balancer take it out (keepup/cluster.py).
|
|
164
|
+
"""
|
|
165
|
+
state = _replica_state()
|
|
166
|
+
if state == "stopped":
|
|
167
|
+
return JSONResponse(status_code=503, content={
|
|
168
|
+
"status": state,
|
|
169
|
+
"instance_id": get_instance_id(),
|
|
170
|
+
"timestamp": clock.utcnow().isoformat(),
|
|
171
|
+
})
|
|
172
|
+
try:
|
|
173
|
+
# Taken here, not at the top of the module: importing the framework
|
|
174
|
+
# must not load a database (keepup-127).
|
|
175
|
+
from keepup_db import DatabaseManagerV2
|
|
176
|
+
|
|
177
|
+
db_status = DatabaseManagerV2.test_connection()
|
|
178
|
+
except Exception as e:
|
|
179
|
+
db_status = {"success": False, "error": str(e)}
|
|
180
|
+
|
|
181
|
+
if not db_status.get("success"):
|
|
182
|
+
# The reason goes to the log; the caller learns that it failed, not where.
|
|
183
|
+
logger.error(f"Health check failed: {db_status.get('error')}")
|
|
184
|
+
raise HTTPException(status_code=503, detail="Service unavailable")
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
"status": "healthy",
|
|
188
|
+
# Which instance answered: how one tells the public address and the stand
|
|
189
|
+
# are the same deployment (doc/stand_update.md). A container name, not an address.
|
|
190
|
+
"instance_id": get_instance_id(),
|
|
191
|
+
"timestamp": clock.utcnow().isoformat(),
|
|
192
|
+
}
|
|
193
|
+
@app.get("/api/versions")
|
|
194
|
+
async def api_versions():
|
|
195
|
+
"""Which versions of the API this server speaks; public, read by agents at start."""
|
|
196
|
+
return describe()
|
|
197
|
+
@app.get("/api/admin/health")
|
|
198
|
+
def admin_health_check(admin: dict = Depends(get_current_admin)):
|
|
199
|
+
"""The full picture for an administrator: database, its version and settings."""
|
|
200
|
+
try:
|
|
201
|
+
# Taken here, not at the top of the module: importing the framework
|
|
202
|
+
# must not load a database (keepup-127).
|
|
203
|
+
from keepup_db import DatabaseManagerV2
|
|
204
|
+
|
|
205
|
+
db_status = DatabaseManagerV2.test_connection()
|
|
206
|
+
except Exception as e:
|
|
207
|
+
db_status = {"success": False, "error": str(e)}
|
|
208
|
+
return {
|
|
209
|
+
"status": "healthy" if db_status.get("success") else "unhealthy",
|
|
210
|
+
"database": db_status,
|
|
211
|
+
"instance_id": get_instance_id(),
|
|
212
|
+
"timestamp": clock.utcnow().isoformat(),
|
|
213
|
+
"incoming_requests_buffer_size": len(incoming_requests_buffer),
|
|
214
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keepup-http
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: The way into the application over HTTP: the application, its routes and its middleware.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: keepup-admin>=0.4.0
|
|
8
|
+
Requires-Dist: keepup-db>=0.4.0
|
|
9
|
+
Requires-Dist: keepup-auth>=0.4.0
|
|
10
|
+
Requires-Dist: keepup-audit>=0.4.0
|
|
11
|
+
Requires-Dist: keepup-cluster>=0.4.0
|
|
12
|
+
Requires-Dist: keepup-modules>=0.4.0
|
|
13
|
+
Requires-Dist: keepup-metrics>=0.4.0
|
|
14
|
+
Requires-Dist: keepup-tasks>=0.4.0
|
|
15
|
+
Requires-Dist: keepup-ui>=0.4.0
|
|
16
|
+
Requires-Dist: fastapi<1,>=0.128
|
|
17
|
+
Requires-Dist: starlette<2,>=1.3.1
|
|
18
|
+
Requires-Dist: pydantic<3,>=2.11
|
|
19
|
+
|
|
20
|
+
# keepup-http
|
|
21
|
+
|
|
22
|
+
The way into the application over HTTP: the application, its routes and its middleware.
|
|
23
|
+
|
|
24
|
+
One capability of [keepup](https://github.com/FedorSergeev/keepup-admin), the framework over FastAPI for admin
|
|
25
|
+
panels: the base is installed as `keepup-admin` and imported as `keepup`, and
|
|
26
|
+
this distribution carries one part of it. It is installed on its own
|
|
27
|
+
(`pip install keepup-http`) or together with the base (`pip install "keepup-admin[panel]"`),
|
|
28
|
+
and it declares itself to the kernel through its
|
|
29
|
+
`[project.entry-points."keepup.plugins"]` entry point.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
keepup_http/__init__.py,sha256=oaBdDuvOXIr97sciS1AKcFYeBDGMb9mQcmpTiSu6spE,1379
|
|
2
|
+
keepup_http/api_docs.py,sha256=rw-LGuouQUIonRP7TEeLwvJaycwHg9a8unYACTinm8Q,3211
|
|
3
|
+
keepup_http/body_limit.py,sha256=ExS17EauRgsRjDtfMpPunHr997OAKmwaBfBZZda3-Qc,6789
|
|
4
|
+
keepup_http/events_api.py,sha256=GVhL5ZMnNT4-T8Vo1FEz903hORleW0LQJ8DqDc2peOY,10318
|
|
5
|
+
keepup_http/factory.py,sha256=Ny7tR9Oh6MbqUNtgWdi9wWWkZM0VhuZMRWROSl4kWSI,25355
|
|
6
|
+
keepup_http/lock_routes.py,sha256=A5gD8w3g-xj6EXUgAMbnDtHWuKWPEYxNSY9EcY-DO9g,6052
|
|
7
|
+
keepup_http/plugin.py,sha256=BQtG-c2yfkbPN4YAwV_tLV0n-rNErJheIZW7U0fBKCs,2042
|
|
8
|
+
keepup_http/routes.py,sha256=DZbRqWiwmembhENh0PHowfk-OxfhYBM7u4K37ygRzOo,23828
|
|
9
|
+
keepup_http/web.py,sha256=NxWGbB2WNqpklbDmyVeeYU4wTEF6PmvY1bwX7-JBC0Y,8765
|
|
10
|
+
keepup_http/static/index_nebula.html,sha256=_I-3Df-I2tM0-4R2Cic8XI1MGsRrIwm9ylFm_jlaLAY,13393
|
|
11
|
+
keepup_http/static/index_new.html,sha256=gJrD9F3MvQDGuCN1FCJi5VQLF1QIp6kbiM7c4tnH-3k,13151
|
|
12
|
+
keepup_http/static/css/aos.css,sha256=GqiEX9BuR1rv5zPU5Vs2qS_NSHl1BJyBcjQYJ6ycwD4,26053
|
|
13
|
+
keepup_http/static/css/main_nebula.css,sha256=dg9DoXia9kM5A1dYwm1Cj5Y8bCSN-gdUGE7kkp3vLW8,7670
|
|
14
|
+
keepup_http/static/css/main_new.css,sha256=Q5uz63TOcH6H46I8KqwzhdrA0PEVlEeS6Is9Ub80MZA,23947
|
|
15
|
+
keepup_http/static/css/tailwind.css,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
16
|
+
keepup_http/static/js/aos.js,sha256=8mhhK6WerRskNTu3fWZ4O8xDWv8cIr5fk8QLrDhplo4,14239
|
|
17
|
+
keepup_http/static/js/feather-icons.js,sha256=6kZaxraaA33En9pqscWF-VclCR7_RRXfYduR2dPF5U4,76047
|
|
18
|
+
keepup_http/static/js/layout_bootstrap.js,sha256=qsrnIaMllpu76ZOsLtfCcMw0AHfis1CFGSbVBAYyyzU,1357
|
|
19
|
+
keepup_http/static/js/main_new.js,sha256=_9ZXLp2GapCAN5Ge1mDbKG2hAT2Ey408-lGrP1ht-ng,72867
|
|
20
|
+
keepup_http/static/js/tailwind.js,sha256=YUsXy7eac-_sNNwQBa7AtFLnApjlzx7yf9PLE2FlLDk,407278
|
|
21
|
+
keepup_http/static/modules/css/background_tasks.css,sha256=xEr50ttH7M1AKaIOtXffaarAPwhOXOtZ5ALDPDgR1bU,3756
|
|
22
|
+
keepup_http/static/modules/css/cluster.css,sha256=xUT2Ovp1928j7CFtsril-FsIQxeO6GhfyU5iRYZfdt4,444
|
|
23
|
+
keepup_http/static/modules/css/event_manager.css,sha256=zjBZaTpVFDcooWvr6aJ_n9g4Nmxs7k3yJk16FI3_0u0,7096
|
|
24
|
+
keepup_http/static/modules/css/integration_logs.css,sha256=GrcAZf56q_BmHapoplB1Bf-nA1JT5C91sNiPGKkyO8o,629
|
|
25
|
+
keepup_http/static/modules/css/metrics.css,sha256=BgWa_XITZvigBUf1w3kOtNROAqJwFWPgjuZmFbXor04,2018
|
|
26
|
+
keepup_http/static/modules/css/modules.css,sha256=tVZ5ZM9U8aw6_Us_rgrdrMk_jGx3HqOqCSbOa48Mo84,3308
|
|
27
|
+
keepup_http/static/modules/css/themes.css,sha256=Xe9x8e3XzrNqgg_7tN3kARxBbwRdZ6dBDhfMUn-nXtc,11208
|
|
28
|
+
keepup_http/static/modules/css/users.css,sha256=avR3K4-mD7nZusy4m9Plm929gS3_aABr9UaikQAznOY,4518
|
|
29
|
+
keepup_http/static/modules/js/background_tasks.js,sha256=zCvtKWheAno9m1Gcs4GQsWRbq75jsT779e0HLVgtnR4,28206
|
|
30
|
+
keepup_http/static/modules/js/chart.js,sha256=M5q_6Ip8lDTBJQZG4N44JM0epSnA0UjNFmUwj0LpA4E,208521
|
|
31
|
+
keepup_http/static/modules/js/chartjs-adapter-date-fns.bundle.min.js,sha256=VvDT2DpQOuVGg6w8jmJ-cKZiLFAJyPKVZt9A4LzbZ4M,50649
|
|
32
|
+
keepup_http/static/modules/js/cluster.js,sha256=RNML0Z5IOdvFWmHS4lyo-8pcKFZ8VzU1uLxYw66P-p0,16391
|
|
33
|
+
keepup_http/static/modules/js/event_manager.js,sha256=awyfis19PdAIeUL32h7dQU8h6ZUCXh33Hu1ZyURJZwk,34969
|
|
34
|
+
keepup_http/static/modules/js/integration_logs.js,sha256=AaPwkw2WKCO8n0ejONxGcuYfxaiDdcN_SX2vK4YSiE4,34056
|
|
35
|
+
keepup_http/static/modules/js/metrics.js,sha256=Z3NrwXIoH9Gsvx0NuYIibjLIZCaSBHdDq9Q0wgNnt6w,35637
|
|
36
|
+
keepup_http/static/modules/js/modules.js,sha256=qEbz-swdOOgzrHWKoOFBNt_Hy5E8-bBF7HKu3v5j_5c,50249
|
|
37
|
+
keepup_http/static/modules/js/themes.js,sha256=PAbGHjIU4FqC4COptapP6NstrA6_jAHPEV6J6bHuJIU,26530
|
|
38
|
+
keepup_http/static/modules/js/users.js,sha256=GlifR_XDwKisqn-sjMrRW9f7LlGFqYXUF4JZVUmM3sQ,38410
|
|
39
|
+
keepup_http-0.5.0.dist-info/METADATA,sha256=LnNMQM8xzGRci4VxGw42EbH2SQDT_czx_io1PftXN-M,1191
|
|
40
|
+
keepup_http-0.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
41
|
+
keepup_http-0.5.0.dist-info/entry_points.txt,sha256=gS1ctzDW3Dw-nKFqmyN8Z5rrR8qm_vu-vXpfM-rvOfo,47
|
|
42
|
+
keepup_http-0.5.0.dist-info/top_level.txt,sha256=BNfVgntXFDeEe-9ZSZXszO02YwuihPOb6n73yf5lXr8,12
|
|
43
|
+
keepup_http-0.5.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
keepup_http
|