devpi-admin 1.1.1__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devpi_admin-1.2.0/.github/workflows/publish.yml +39 -0
- devpi_admin-1.2.0/.github/workflows/tests.yml +32 -0
- {devpi_admin-1.1.1 → devpi_admin-1.2.0}/.gitignore +1 -1
- devpi_admin-1.2.0/INSTALL.textile +190 -0
- {devpi_admin-1.1.1 → devpi_admin-1.2.0}/PKG-INFO +8 -5
- {devpi_admin-1.1.1 → devpi_admin-1.2.0}/README.md +3 -1
- {devpi_admin-1.1.1/src → devpi_admin-1.2.0}/devpi_admin/_version.py +3 -3
- {devpi_admin-1.1.1/src → devpi_admin-1.2.0}/devpi_admin/main.py +16 -0
- {devpi_admin-1.1.1/src → devpi_admin-1.2.0}/devpi_admin/static/css/style.css +44 -3
- devpi_admin-1.2.0/devpi_admin/static/favicon.svg +10 -0
- {devpi_admin-1.1.1/src → devpi_admin-1.2.0}/devpi_admin/static/index.html +1 -0
- {devpi_admin-1.1.1/src → devpi_admin-1.2.0}/devpi_admin/static/js/app.js +93 -8
- devpi_admin-1.2.0/devpi_admin.egg-info/PKG-INFO +231 -0
- devpi_admin-1.2.0/devpi_admin.egg-info/SOURCES.txt +31 -0
- devpi_admin-1.2.0/devpi_admin.egg-info/dependency_links.txt +1 -0
- devpi_admin-1.2.0/devpi_admin.egg-info/entry_points.txt +2 -0
- devpi_admin-1.2.0/devpi_admin.egg-info/requires.txt +1 -0
- devpi_admin-1.2.0/devpi_admin.egg-info/top_level.txt +1 -0
- {devpi_admin-1.1.1 → devpi_admin-1.2.0}/pyproject.toml +7 -19
- devpi_admin-1.2.0/setup.cfg +4 -0
- {devpi_admin-1.1.1 → devpi_admin-1.2.0}/LICENSE +0 -0
- {devpi_admin-1.1.1/src → devpi_admin-1.2.0}/devpi_admin/__init__.py +0 -0
- {devpi_admin-1.1.1/src → devpi_admin-1.2.0}/devpi_admin/static/js/api.js +0 -0
- {devpi_admin-1.1.1/src → devpi_admin-1.2.0}/devpi_admin/static/js/marked.min.js +0 -0
- {devpi_admin-1.1.1/src → devpi_admin-1.2.0}/devpi_admin/static/js/theme.js +0 -0
- {devpi_admin-1.1.1 → devpi_admin-1.2.0}/tests/__init__.py +0 -0
- {devpi_admin-1.1.1 → devpi_admin-1.2.0}/tests/test_cached_versions.py +0 -0
- {devpi_admin-1.1.1 → devpi_admin-1.2.0}/tests/test_helpers.py +0 -0
- {devpi_admin-1.1.1 → devpi_admin-1.2.0}/tests/test_hooks.py +0 -0
- {devpi_admin-1.1.1 → devpi_admin-1.2.0}/tests/test_json_safe.py +0 -0
- {devpi_admin-1.1.1 → devpi_admin-1.2.0}/tests/test_package.py +0 -0
- {devpi_admin-1.1.1 → devpi_admin-1.2.0}/tests/test_tween.py +0 -0
- {devpi_admin-1.1.1 → devpi_admin-1.2.0}/tests/test_wants_html.py +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
with:
|
|
17
|
+
fetch-depth: 0 # needed for hatch-vcs to derive version from tag
|
|
18
|
+
|
|
19
|
+
- name: Set up Python
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.14"
|
|
23
|
+
|
|
24
|
+
- name: Install build dependencies
|
|
25
|
+
run: pip install build
|
|
26
|
+
|
|
27
|
+
- name: Install package
|
|
28
|
+
run: pip install .
|
|
29
|
+
|
|
30
|
+
- name: Run unit tests
|
|
31
|
+
run: python -m unittest discover -v tests/
|
|
32
|
+
env:
|
|
33
|
+
PYTHONWARNINGS: "ignore::UserWarning"
|
|
34
|
+
|
|
35
|
+
- name: Build package
|
|
36
|
+
run: python -m build
|
|
37
|
+
|
|
38
|
+
- name: Publish to PyPI
|
|
39
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test-ubuntu:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.14"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0 # needed for hatch-vcs to read git tags
|
|
20
|
+
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
|
|
26
|
+
- name: Install package
|
|
27
|
+
run: pip install .
|
|
28
|
+
|
|
29
|
+
- name: Run unit tests
|
|
30
|
+
run: python -m unittest discover -v tests/
|
|
31
|
+
env:
|
|
32
|
+
PYTHONWARNINGS: "ignore::UserWarning"
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
h1. devpi-admin — inštalačný návod
|
|
2
|
+
|
|
3
|
+
h2. Predpoklady
|
|
4
|
+
|
|
5
|
+
* Python 3.9+
|
|
6
|
+
* Systémový používateľ vyhradený pre službu (ďalej @pypi@)
|
|
7
|
+
* Adresárová štruktúra podľa konvencie — príklady nižšie používajú @/opt/pypi@ a @/var/lib/pypi@, cesty je možné upraviť podľa potreby
|
|
8
|
+
|
|
9
|
+
h2. 1. Adresárová štruktúra
|
|
10
|
+
|
|
11
|
+
<pre>
|
|
12
|
+
/opt/pypi/venv/ — Python virtualenv
|
|
13
|
+
/var/lib/pypi/data/ — dáta devpi servera (databáza, stiahnuté balíky)
|
|
14
|
+
/opt/pypi/etc/ — konfiguračné súbory
|
|
15
|
+
</pre>
|
|
16
|
+
|
|
17
|
+
h2. 2. Vytvorenie virtualenv a inštalácia
|
|
18
|
+
|
|
19
|
+
<pre>
|
|
20
|
+
python3 -m venv /opt/pypi/venv
|
|
21
|
+
/opt/pypi/venv/bin/pip install --upgrade pip
|
|
22
|
+
/opt/pypi/venv/bin/pip install devpi-server devpi-admin
|
|
23
|
+
</pre>
|
|
24
|
+
|
|
25
|
+
h2. 3. Inicializácia serverových dát
|
|
26
|
+
|
|
27
|
+
Inicializácia sa vykoná *raz* pred prvým spustením:
|
|
28
|
+
|
|
29
|
+
<pre>
|
|
30
|
+
sudo -u pypi /opt/pypi/venv/bin/devpi-server \
|
|
31
|
+
--serverdir /var/lib/pypi/data \
|
|
32
|
+
--init
|
|
33
|
+
</pre>
|
|
34
|
+
|
|
35
|
+
h2. 4. Generovanie secret súboru
|
|
36
|
+
|
|
37
|
+
Devpi podpisuje autentifikačné tokeny tajným kľúčom. Bez tohto súboru sa pri každom reštarte vygeneruje nový náhodný kľúč a *všetky existujúce tokeny sa zneplatnia* — používatelia sa musia znova prihlásiť.
|
|
38
|
+
|
|
39
|
+
Secret súbor sa vytvorí *raz*:
|
|
40
|
+
|
|
41
|
+
<pre>
|
|
42
|
+
sudo -u pypi /opt/pypi/venv/bin/devpi-gen-secret --secretfile /opt/pypi/etc/devpi-secret
|
|
43
|
+
chmod 600 /opt/pypi/etc/devpi-secret
|
|
44
|
+
</pre>
|
|
45
|
+
|
|
46
|
+
*Pozor:* adresár @/opt/pypi/etc/@ nesmie byť group-writable, inak devpi odmietne štart.
|
|
47
|
+
Skontrolovať: @ls -ld /opt/pypi/etc/@ — permissions musia byť @drwxr-x---@ alebo prísnejšie.
|
|
48
|
+
|
|
49
|
+
h2. 5. Konfigurácia logovania
|
|
50
|
+
|
|
51
|
+
Devpi štandardne pridáva do logov vlastné timestampy, čo je pri journald zbytočné.
|
|
52
|
+
Vytvorí sa konfiguračný súbor @/opt/pypi/etc/devpi-logging.json@:
|
|
53
|
+
|
|
54
|
+
<pre>
|
|
55
|
+
{
|
|
56
|
+
"version": 1,
|
|
57
|
+
"disable_existing_loggers": false,
|
|
58
|
+
"formatters": {
|
|
59
|
+
"simple": {
|
|
60
|
+
"format": "%(levelname)-5s %(name)s %(message)s"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"handlers": {
|
|
64
|
+
"console": {
|
|
65
|
+
"class": "logging.StreamHandler",
|
|
66
|
+
"stream": "ext://sys.stdout",
|
|
67
|
+
"formatter": "simple"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"root": {
|
|
71
|
+
"level": "INFO",
|
|
72
|
+
"handlers": ["console"]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
</pre>
|
|
76
|
+
|
|
77
|
+
h2. 6. systemd service unit
|
|
78
|
+
|
|
79
|
+
Uložiť ako @/etc/systemd/system/devpi.service@:
|
|
80
|
+
|
|
81
|
+
<pre>
|
|
82
|
+
[Unit]
|
|
83
|
+
Description=devpi package server
|
|
84
|
+
After=network.target
|
|
85
|
+
|
|
86
|
+
[Service]
|
|
87
|
+
User=pypi
|
|
88
|
+
Group=pypi
|
|
89
|
+
ExecStart=/opt/pypi/venv/bin/devpi-server \
|
|
90
|
+
--serverdir /var/lib/pypi/data \
|
|
91
|
+
--secretfile /opt/pypi/etc/devpi-secret \
|
|
92
|
+
--logger-cfg /opt/pypi/etc/devpi-logging.json \
|
|
93
|
+
--host 0.0.0.0 \
|
|
94
|
+
--port 3141 \
|
|
95
|
+
--outside-url https://pypi.villapro.eu \
|
|
96
|
+
--restrict-modify root
|
|
97
|
+
Restart=on-failure
|
|
98
|
+
RestartSec=5
|
|
99
|
+
StandardOutput=journal
|
|
100
|
+
StandardError=journal
|
|
101
|
+
|
|
102
|
+
[Install]
|
|
103
|
+
WantedBy=multi-user.target
|
|
104
|
+
</pre>
|
|
105
|
+
|
|
106
|
+
*@--outside-url@* — verejná URL servera (ak beží za reverse proxy). Ak nie je reverse proxy, je možné túto voľbu vynechať alebo nastaviť na @http://server-ip:3141@.
|
|
107
|
+
|
|
108
|
+
*@--restrict-modify root@* — zakazuje bežným používateľom vytvárať nových používateľov a indexy. Odporúčané pre produkciu.
|
|
109
|
+
|
|
110
|
+
Aktivácia a spustenie:
|
|
111
|
+
|
|
112
|
+
<pre>
|
|
113
|
+
systemctl daemon-reload
|
|
114
|
+
systemctl enable devpi
|
|
115
|
+
systemctl start devpi
|
|
116
|
+
systemctl status devpi
|
|
117
|
+
</pre>
|
|
118
|
+
|
|
119
|
+
h2. 7. Overenie inštalácie
|
|
120
|
+
|
|
121
|
+
Po spustení je server dostupný na @http://localhost:3141@.
|
|
122
|
+
Webové rozhranie: @http://localhost:3141/+admin/@
|
|
123
|
+
|
|
124
|
+
Na overenie, že plugin devpi-admin je správne načítaný:
|
|
125
|
+
|
|
126
|
+
<pre>
|
|
127
|
+
curl -s http://localhost:3141/+api | python3 -m json.tool | grep devpi-admin
|
|
128
|
+
</pre>
|
|
129
|
+
|
|
130
|
+
Výstup by mal obsahovať @"devpi-admin"@ v zozname features.
|
|
131
|
+
|
|
132
|
+
h2. 8. Prvé prihlásenie a nastavenie
|
|
133
|
+
|
|
134
|
+
h3. 8.1 Prihlásenie
|
|
135
|
+
|
|
136
|
+
Webové rozhranie sa otvorí v prehliadači. Prihlásenie cez tlačidlo *Login* (vpravo hore).
|
|
137
|
+
|
|
138
|
+
Predvolené prihlasovacie údaje:
|
|
139
|
+
* *Používateľ:* @root@
|
|
140
|
+
* *Heslo:* _(prázdne — pri prvom prihlásení stačí stlačiť Login bez hesla)_
|
|
141
|
+
|
|
142
|
+
*Ihneď po prihlásení je potrebné nastaviť heslo pre root:* kliknutím na meno @root@ v pravom hornom rohu sa otvorí dialóg na zmenu hesla.
|
|
143
|
+
|
|
144
|
+
h3. 8.2 Mirror index (PyPI)
|
|
145
|
+
|
|
146
|
+
devpi pri inicializácii automaticky vytvorí používateľa @root@ a index @root/pypi@ (mirror PyPI). Tento index je pripravený na použitie bez ďalšej konfigurácie.
|
|
147
|
+
|
|
148
|
+
V záložke *Indexes* by mal byť viditeľný @root/pypi@ typu @mirror@.
|
|
149
|
+
|
|
150
|
+
h3. 8.3 Vytvorenie privátneho indexu
|
|
151
|
+
|
|
152
|
+
Pre nahrávanie vlastných balíkov:
|
|
153
|
+
|
|
154
|
+
# Vytvorenie používateľa pre tím: *Users → + New User*
|
|
155
|
+
# Vytvorenie indexu: *Indexes → + New Index*
|
|
156
|
+
** *Owner:* daný používateľ
|
|
157
|
+
** *Type:* @stage@
|
|
158
|
+
** *Bases:* @root/pypi@ (balíky ktoré nie sú v indexe sa stiahnu z PyPI)
|
|
159
|
+
** *Volatile:* podľa potreby (volatile = možnosť prepísať rovnakú verziu)
|
|
160
|
+
|
|
161
|
+
h3. 8.4 Konfigurácia pip na klientoch
|
|
162
|
+
|
|
163
|
+
<pre>
|
|
164
|
+
# /etc/pip.conf alebo ~/.pip/pip.conf
|
|
165
|
+
[global]
|
|
166
|
+
index-url = https://pypi.villapro.eu/user/index/+simple/
|
|
167
|
+
</pre>
|
|
168
|
+
|
|
169
|
+
Pre autentifikovaný prístup:
|
|
170
|
+
|
|
171
|
+
<pre>
|
|
172
|
+
[global]
|
|
173
|
+
index-url = https://user:heslo@pypi.villapro.eu/user/index/+simple/
|
|
174
|
+
</pre>
|
|
175
|
+
|
|
176
|
+
h2. 9. Aktualizácia devpi-admin
|
|
177
|
+
|
|
178
|
+
<pre>
|
|
179
|
+
systemctl stop devpi
|
|
180
|
+
/opt/pypi/venv/bin/pip install --upgrade devpi-admin
|
|
181
|
+
systemctl start devpi
|
|
182
|
+
</pre>
|
|
183
|
+
|
|
184
|
+
Dáta servera (@/var/lib/pypi/data@) sa aktualizáciou pluginu nedotknú.
|
|
185
|
+
|
|
186
|
+
h2. 10. Logy
|
|
187
|
+
|
|
188
|
+
<pre>
|
|
189
|
+
journalctl -u devpi -f
|
|
190
|
+
</pre>
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devpi-admin
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Modern web UI plugin for devpi-server — drop-in replacement for devpi-web
|
|
5
5
|
Author-email: Pavel Revak <pavelrevak@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
|
-
|
|
8
|
-
Keywords: admin,devpi,pypi,ui,web
|
|
7
|
+
Keywords: devpi,pypi,admin,web,ui
|
|
9
8
|
Classifier: Development Status :: 4 - Beta
|
|
10
9
|
Classifier: Framework :: Pyramid
|
|
11
10
|
Classifier: Intended Audience :: Developers
|
|
@@ -16,8 +15,10 @@ Classifier: Programming Language :: Python :: 3
|
|
|
16
15
|
Classifier: Topic :: Software Development :: Libraries
|
|
17
16
|
Classifier: Topic :: System :: Software Distribution
|
|
18
17
|
Requires-Python: >=3.9
|
|
19
|
-
Requires-Dist: devpi-server>=6.0
|
|
20
18
|
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: devpi-server>=6.0
|
|
21
|
+
Dynamic: license-file
|
|
21
22
|
|
|
22
23
|
# devpi-admin
|
|
23
24
|
|
|
@@ -35,6 +36,8 @@ talks to the standard devpi JSON API directly.
|
|
|
35
36
|
- Server info with version of devpi-server and all installed plugins (auto-detected)
|
|
36
37
|
- Cache metrics with hit-rate bars (storage, changelog, relpath caches)
|
|
37
38
|
- Whoosh search index queue status
|
|
39
|
+
- **Replica status** (master only) — per-replica cards with online/offline badge, serial lag,
|
|
40
|
+
and last-seen timestamp; visible only when replicas are connected
|
|
38
41
|
|
|
39
42
|
### Indexes
|
|
40
43
|
- Visual cards color-coded by type: green (stage), amber (volatile stage), blue (mirror)
|
|
@@ -51,7 +54,7 @@ talks to the standard devpi JSON API directly.
|
|
|
51
54
|
|
|
52
55
|
### Packages
|
|
53
56
|
- Client-side search with PEP 503 name normalization
|
|
54
|
-
- Mirror indexes: shows only cached packages (
|
|
57
|
+
- Mirror indexes: shows only cached packages (filesystem scan, no 17 MB index download);
|
|
55
58
|
"Download full index" button available for complete browse
|
|
56
59
|
- Package cards with latest version and `pip install` command
|
|
57
60
|
|
|
@@ -14,6 +14,8 @@ talks to the standard devpi JSON API directly.
|
|
|
14
14
|
- Server info with version of devpi-server and all installed plugins (auto-detected)
|
|
15
15
|
- Cache metrics with hit-rate bars (storage, changelog, relpath caches)
|
|
16
16
|
- Whoosh search index queue status
|
|
17
|
+
- **Replica status** (master only) — per-replica cards with online/offline badge, serial lag,
|
|
18
|
+
and last-seen timestamp; visible only when replicas are connected
|
|
17
19
|
|
|
18
20
|
### Indexes
|
|
19
21
|
- Visual cards color-coded by type: green (stage), amber (volatile stage), blue (mirror)
|
|
@@ -30,7 +32,7 @@ talks to the standard devpi JSON API directly.
|
|
|
30
32
|
|
|
31
33
|
### Packages
|
|
32
34
|
- Client-side search with PEP 503 name normalization
|
|
33
|
-
- Mirror indexes: shows only cached packages (
|
|
35
|
+
- Mirror indexes: shows only cached packages (filesystem scan, no 17 MB index download);
|
|
34
36
|
"Download full index" button available for complete browse
|
|
35
37
|
- Package cards with latest version and `pip install` command
|
|
36
38
|
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
21
|
+
__version__ = version = '1.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 2, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id =
|
|
24
|
+
__commit_id__ = commit_id = 'g34be8ff92'
|
|
@@ -44,6 +44,14 @@ def devpiserver_pyramid_configure(config, pyramid_config):
|
|
|
44
44
|
lambda request: HTTPFound("/+admin/"),
|
|
45
45
|
route_name="devpi_admin_spa_noslash")
|
|
46
46
|
|
|
47
|
+
# Session validity check.
|
|
48
|
+
pyramid_config.add_route(
|
|
49
|
+
"devpi_admin_session",
|
|
50
|
+
"/+admin-api/session")
|
|
51
|
+
pyramid_config.add_view(
|
|
52
|
+
_session_view, route_name="devpi_admin_session",
|
|
53
|
+
request_method="GET")
|
|
54
|
+
|
|
47
55
|
# Cached packages API for mirror indexes.
|
|
48
56
|
pyramid_config.add_route(
|
|
49
57
|
"devpi_admin_cached",
|
|
@@ -82,6 +90,14 @@ def _serve_index(request):
|
|
|
82
90
|
content_type="text/html")
|
|
83
91
|
|
|
84
92
|
|
|
93
|
+
def _session_view(request):
|
|
94
|
+
"""Return whether the current request carries a valid authenticated session."""
|
|
95
|
+
user = request.authenticated_userid
|
|
96
|
+
if user:
|
|
97
|
+
return _json_response({"valid": True, "user": user})
|
|
98
|
+
raise HTTPForbidden(json_body={"valid": False, "error": "not authenticated"})
|
|
99
|
+
|
|
100
|
+
|
|
85
101
|
def _get_stage_or_404(xom, user, index):
|
|
86
102
|
"""Return stage object or raise HTTPNotFound."""
|
|
87
103
|
stage = xom.model.getstage(user, index)
|
|
@@ -196,14 +196,18 @@ body {
|
|
|
196
196
|
.user-btn-name {
|
|
197
197
|
padding: 4px 10px;
|
|
198
198
|
font-weight: 600;
|
|
199
|
-
color:
|
|
199
|
+
color: #22c55e;
|
|
200
200
|
background: var(--bg);
|
|
201
201
|
cursor: pointer;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
.user-btn-name:hover {
|
|
205
205
|
background: var(--bg-alt);
|
|
206
|
-
|
|
206
|
+
opacity: 0.8;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.user-btn.is-root .user-btn-name {
|
|
210
|
+
color: #f59e0b;
|
|
207
211
|
}
|
|
208
212
|
|
|
209
213
|
.user-btn-sep {
|
|
@@ -366,7 +370,19 @@ body {
|
|
|
366
370
|
/* --- User cards --- */
|
|
367
371
|
|
|
368
372
|
.user-card {
|
|
369
|
-
border-left-color:
|
|
373
|
+
border-left-color: #22c55e;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.user-card.user-root {
|
|
377
|
+
border-left-color: #f59e0b;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.user-card .index-card-name {
|
|
381
|
+
color: #22c55e;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.user-card.user-root .index-card-name {
|
|
385
|
+
color: #f59e0b;
|
|
370
386
|
}
|
|
371
387
|
|
|
372
388
|
.user-card .kebab-menu {
|
|
@@ -1520,6 +1536,31 @@ body {
|
|
|
1520
1536
|
margin-bottom: 4px;
|
|
1521
1537
|
}
|
|
1522
1538
|
|
|
1539
|
+
.replica-title {
|
|
1540
|
+
display: flex;
|
|
1541
|
+
justify-content: space-between;
|
|
1542
|
+
align-items: center;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
.replica-badge {
|
|
1546
|
+
font-size: 11px;
|
|
1547
|
+
font-weight: 600;
|
|
1548
|
+
padding: 2px 7px;
|
|
1549
|
+
border-radius: 10px;
|
|
1550
|
+
text-transform: uppercase;
|
|
1551
|
+
letter-spacing: 0.04em;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
.replica-online {
|
|
1555
|
+
background: color-mix(in srgb, var(--success) 15%, transparent);
|
|
1556
|
+
color: var(--success);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
.replica-offline {
|
|
1560
|
+
background: color-mix(in srgb, var(--danger) 15%, transparent);
|
|
1561
|
+
color: var(--danger);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1523
1564
|
.status-row {
|
|
1524
1565
|
display: flex;
|
|
1525
1566
|
justify-content: space-between;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
|
+
<!-- Box bottom face -->
|
|
3
|
+
<polygon points="16,18 2,11 16,4 30,11" fill="#4a9eff" opacity="0.85"/>
|
|
4
|
+
<!-- Box left face -->
|
|
5
|
+
<polygon points="2,11 2,23 16,30 16,18" fill="#2563eb"/>
|
|
6
|
+
<!-- Box right face -->
|
|
7
|
+
<polygon points="30,11 30,23 16,30 16,18" fill="#1d4ed8"/>
|
|
8
|
+
<!-- Highlight stripe on top face -->
|
|
9
|
+
<polygon points="16,6 26,10.5 16,15 6,10.5" fill="#93c5fd" opacity="0.4"/>
|
|
10
|
+
</svg>
|
|
@@ -463,6 +463,7 @@
|
|
|
463
463
|
logoutBtn.appendChild(el('span', {className: 'user-btn-action', textContent: 'Logout'}));
|
|
464
464
|
loginBtn.hidden = true;
|
|
465
465
|
logoutBtn.hidden = false;
|
|
466
|
+
logoutBtn.classList.toggle('is-root', user === 'root');
|
|
466
467
|
navUsers.hidden = user !== 'root';
|
|
467
468
|
document.body.classList.add('authenticated');
|
|
468
469
|
} else {
|
|
@@ -543,6 +544,18 @@
|
|
|
543
544
|
});
|
|
544
545
|
}
|
|
545
546
|
|
|
547
|
+
// Reload current view when clicking an already-active nav link
|
|
548
|
+
document.getElementById('main-nav').addEventListener('click', function (e) {
|
|
549
|
+
if (e.target.tagName === 'A') {
|
|
550
|
+
var href = e.target.getAttribute('href') || '#';
|
|
551
|
+
var current = window.location.hash || '#';
|
|
552
|
+
if (href === current) {
|
|
553
|
+
e.preventDefault();
|
|
554
|
+
navigate();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
546
559
|
logoutBtn.addEventListener('click', function (e) {
|
|
547
560
|
// Clicking the username part opens change-password modal
|
|
548
561
|
if (e.target.classList.contains('user-btn-name')) {
|
|
@@ -634,7 +647,7 @@
|
|
|
634
647
|
var currentUser = Api.getUser();
|
|
635
648
|
var canEdit = currentUser === name || currentUser === 'root';
|
|
636
649
|
|
|
637
|
-
var card = el('div', {className: 'index-card user-card'});
|
|
650
|
+
var card = el('div', {className: 'index-card user-card' + (name === 'root' ? ' user-root' : '')});
|
|
638
651
|
|
|
639
652
|
// Card head: username + kebab menu
|
|
640
653
|
var cardHead = el('div', {className: 'index-card-head'});
|
|
@@ -1172,6 +1185,12 @@
|
|
|
1172
1185
|
|
|
1173
1186
|
var PKG_LIMIT = 100;
|
|
1174
1187
|
|
|
1188
|
+
function canDeleteFromIndex(indexPath) {
|
|
1189
|
+
var user = Api.getUser();
|
|
1190
|
+
if (!user) return false;
|
|
1191
|
+
return user === 'root' || user === indexPath.split('/')[0];
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1175
1194
|
function buildPackageCard(indexPath, pkg, fetchVersion) {
|
|
1176
1195
|
var card = el('div', {className: 'pkg-card'});
|
|
1177
1196
|
var cardHead = el('div', {className: 'pkg-card-head'});
|
|
@@ -1181,9 +1200,11 @@
|
|
|
1181
1200
|
textContent: pkg,
|
|
1182
1201
|
}));
|
|
1183
1202
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1203
|
+
if (canDeleteFromIndex(indexPath)) {
|
|
1204
|
+
cardHead.appendChild(buildKebabMenu([
|
|
1205
|
+
{label: 'Delete all versions', danger: true, onclick: function () { closeAllKebabs(); deletePackage(indexPath, pkg); }},
|
|
1206
|
+
]));
|
|
1207
|
+
}
|
|
1187
1208
|
card.appendChild(cardHead);
|
|
1188
1209
|
|
|
1189
1210
|
card.appendChild(buildPipBlock(indexPath, pkg));
|
|
@@ -1227,7 +1248,8 @@
|
|
|
1227
1248
|
function fetchStage() {
|
|
1228
1249
|
showHeadingAndLoading(false);
|
|
1229
1250
|
Api.get('/' + indexPath).then(function (data) {
|
|
1230
|
-
|
|
1251
|
+
var resultIsMirror = !!(data.result && data.result.type === 'mirror');
|
|
1252
|
+
renderPackages(indexPath, data.result, resultIsMirror);
|
|
1231
1253
|
}).catch(handleApiError);
|
|
1232
1254
|
}
|
|
1233
1255
|
|
|
@@ -1430,13 +1452,13 @@
|
|
|
1430
1452
|
' ',
|
|
1431
1453
|
el('span', {className: 'page-heading-version', textContent: 'v' + currentVer}),
|
|
1432
1454
|
]),
|
|
1433
|
-
el('div', {className: 'view-header-actions'}, [
|
|
1455
|
+
el('div', {className: 'view-header-actions'}, canDeleteFromIndex(indexPath) ? [
|
|
1434
1456
|
el('button', {
|
|
1435
|
-
className: 'btn btn-danger
|
|
1457
|
+
className: 'btn btn-danger',
|
|
1436
1458
|
textContent: 'Delete package',
|
|
1437
1459
|
onclick: function () { deletePackage(indexPath, pkg); },
|
|
1438
1460
|
}),
|
|
1439
|
-
]),
|
|
1461
|
+
] : []),
|
|
1440
1462
|
]));
|
|
1441
1463
|
|
|
1442
1464
|
if (cachedVersions.length === 0 && !allVersions) {
|
|
@@ -1959,10 +1981,55 @@
|
|
|
1959
1981
|
grid.appendChild(whooshCard);
|
|
1960
1982
|
}
|
|
1961
1983
|
|
|
1984
|
+
// Replicas — only shown on master with connected replicas
|
|
1985
|
+
var pollingReplicas = status.polling_replicas || {};
|
|
1986
|
+
var replicaUuids = Object.keys(pollingReplicas);
|
|
1987
|
+
if (status.role === 'MASTER' && replicaUuids.length > 0) {
|
|
1988
|
+
var masterSerial = status.serial || 0;
|
|
1989
|
+
var now = Date.now() / 1000;
|
|
1990
|
+
// Replica is considered offline if it hasn't polled in >90s
|
|
1991
|
+
// (normal polling interval is ~37.5s)
|
|
1992
|
+
var OFFLINE_THRESHOLD = 90;
|
|
1993
|
+
|
|
1994
|
+
for (var ri = 0; ri < replicaUuids.length; ri++) {
|
|
1995
|
+
var uuid = replicaUuids[ri];
|
|
1996
|
+
var rep = pollingReplicas[uuid];
|
|
1997
|
+
var lastRequest = rep['last-request'] || 0;
|
|
1998
|
+
var age = now - lastRequest;
|
|
1999
|
+
var isOnline = rep['in-request'] || age < OFFLINE_THRESHOLD;
|
|
2000
|
+
var lag = masterSerial - (rep['serial'] || 0);
|
|
2001
|
+
var label = rep['remote-ip'] || uuid.substring(0, 8);
|
|
2002
|
+
|
|
2003
|
+
var repCard = el('div', {className: 'status-card'});
|
|
2004
|
+
var titleRow = el('div', {className: 'status-card-title replica-title'}, [
|
|
2005
|
+
el('span', {textContent: 'Replica: ' + label}),
|
|
2006
|
+
el('span', {
|
|
2007
|
+
className: 'replica-badge ' + (isOnline ? 'replica-online' : 'replica-offline'),
|
|
2008
|
+
textContent: isOnline ? 'online' : 'offline',
|
|
2009
|
+
}),
|
|
2010
|
+
]);
|
|
2011
|
+
repCard.appendChild(titleRow);
|
|
2012
|
+
repCard.appendChild(statusRow('Serial lag', lag === 0 ? 'in sync' : '+' + lag));
|
|
2013
|
+
repCard.appendChild(statusRow('Last seen', _formatAge(age)));
|
|
2014
|
+
repCard.appendChild(statusRow('Polling', rep['in-request'] ? 'active' : 'idle'));
|
|
2015
|
+
if (rep['outside-url']) {
|
|
2016
|
+
repCard.appendChild(statusRow('URL', rep['outside-url']));
|
|
2017
|
+
}
|
|
2018
|
+
grid.appendChild(repCard);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
|
|
1962
2022
|
content.appendChild(grid);
|
|
1963
2023
|
}).catch(handleApiError);
|
|
1964
2024
|
}
|
|
1965
2025
|
|
|
2026
|
+
function _formatAge(seconds) {
|
|
2027
|
+
if (seconds < 5) return 'just now';
|
|
2028
|
+
if (seconds < 60) return Math.floor(seconds) + 's ago';
|
|
2029
|
+
if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago';
|
|
2030
|
+
return Math.floor(seconds / 3600) + 'h ago';
|
|
2031
|
+
}
|
|
2032
|
+
|
|
1966
2033
|
function formatNum(n) {
|
|
1967
2034
|
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
1968
2035
|
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
|
|
@@ -1981,4 +2048,22 @@
|
|
|
1981
2048
|
updateAuthUI();
|
|
1982
2049
|
updateNav();
|
|
1983
2050
|
navigate();
|
|
2051
|
+
|
|
2052
|
+
var _sessionCheckReady = false;
|
|
2053
|
+
setTimeout(function () { _sessionCheckReady = true; }, 2000);
|
|
2054
|
+
|
|
2055
|
+
function checkSession() {
|
|
2056
|
+
if (!_sessionCheckReady || !Api.getUser()) return;
|
|
2057
|
+
Api.get('/+admin-api/session').catch(function (err) {
|
|
2058
|
+
if (err.status === 403 || err.status === 401) {
|
|
2059
|
+
Api.logout();
|
|
2060
|
+
updateAuthUI();
|
|
2061
|
+
showError(new Error('Session expired. Please log in again.'));
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
document.addEventListener('visibilitychange', function () {
|
|
2066
|
+
if (document.visibilityState === 'visible') checkSession();
|
|
2067
|
+
});
|
|
2068
|
+
window.addEventListener('focus', checkSession);
|
|
1984
2069
|
})();
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: devpi-admin
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Modern web UI plugin for devpi-server — drop-in replacement for devpi-web
|
|
5
|
+
Author-email: Pavel Revak <pavelrevak@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: devpi,pypi,admin,web,ui
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Framework :: Pyramid
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Classifier: Topic :: System :: Software Distribution
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: devpi-server>=6.0
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# devpi-admin
|
|
24
|
+
|
|
25
|
+
A modern web UI plugin for [devpi-server](https://devpi.net/) — a drop-in replacement for
|
|
26
|
+
`devpi-web`. Ships as a Python package that registers itself as a devpi-server plugin via the
|
|
27
|
+
standard entry point mechanism, so a single `pip install devpi-admin` is enough.
|
|
28
|
+
|
|
29
|
+
The UI itself is a bundled single-page application (pure HTML + CSS + vanilla JavaScript, no
|
|
30
|
+
build step) served under `/+admin/`. All devpi REST API endpoints remain untouched — the SPA
|
|
31
|
+
talks to the standard devpi JSON API directly.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
### Dashboard
|
|
36
|
+
- Server info with version of devpi-server and all installed plugins (auto-detected)
|
|
37
|
+
- Cache metrics with hit-rate bars (storage, changelog, relpath caches)
|
|
38
|
+
- Whoosh search index queue status
|
|
39
|
+
- **Replica status** (master only) — per-replica cards with online/offline badge, serial lag,
|
|
40
|
+
and last-seen timestamp; visible only when replicas are connected
|
|
41
|
+
|
|
42
|
+
### Indexes
|
|
43
|
+
- Visual cards color-coded by type: green (stage), amber (volatile stage), blue (mirror)
|
|
44
|
+
- `pip install` command with copy-to-clipboard (click to copy, green flash feedback)
|
|
45
|
+
- `pip.conf` toggle — switch between short form and full `--index-url` / `--trusted-host`
|
|
46
|
+
- `pip.conf` generator — download a ready-to-use config per index
|
|
47
|
+
- Create / edit / delete indexes via modal dialogs
|
|
48
|
+
- `bases` editor with drag & drop priority ordering and transitive inheritance display
|
|
49
|
+
- `acl_upload` tag picker with user selection dropdown
|
|
50
|
+
- `volatile`, `mirror_url`, `title` configuration
|
|
51
|
+
|
|
52
|
+
### Users
|
|
53
|
+
- Create, edit (email, password), delete users (admin only)
|
|
54
|
+
|
|
55
|
+
### Packages
|
|
56
|
+
- Client-side search with PEP 503 name normalization
|
|
57
|
+
- Mirror indexes: shows only cached packages (filesystem scan, no 17 MB index download);
|
|
58
|
+
"Download full index" button available for complete browse
|
|
59
|
+
- Package cards with latest version and `pip install` command
|
|
60
|
+
|
|
61
|
+
### Package detail (PyPI-like layout)
|
|
62
|
+
- **Sidebar**: metadata (author, license, Python version, keywords, platform, maintainer,
|
|
63
|
+
extras, project URLs, dependencies), `pip install` command, file downloads with upload dates
|
|
64
|
+
- **Version list**: cached versions shown normally, uncached versions link to pypi.org (↗);
|
|
65
|
+
"Load all versions" button for mirrors
|
|
66
|
+
- **README**: rendered markdown (via `marked.js`); fetched from PyPI.org for mirror packages
|
|
67
|
+
where devpi doesn't cache the description
|
|
68
|
+
|
|
69
|
+
### General
|
|
70
|
+
- **Anonymous browsing** — visitors can explore public indexes without logging in; admin
|
|
71
|
+
actions (create/edit/delete) appear only after authentication
|
|
72
|
+
- **Dark / light / auto theme** with half-circle icon for auto mode
|
|
73
|
+
- **Responsive mobile menu** with hamburger toggle
|
|
74
|
+
- **ESC + outside-click** dismissal for modals, dropdown menus, mobile menu
|
|
75
|
+
- **Login via modal** — no separate login page
|
|
76
|
+
|
|
77
|
+
## Plugin API endpoints
|
|
78
|
+
|
|
79
|
+
In addition to serving the SPA, `devpi-admin` registers custom API endpoints under
|
|
80
|
+
`/+admin-api/` for features that the standard devpi REST API doesn't provide efficiently:
|
|
81
|
+
|
|
82
|
+
| Endpoint | Method | Description |
|
|
83
|
+
|----------|--------|-------------|
|
|
84
|
+
| `/+admin-api/cached/{user}/{index}` | GET | List cached package names for a mirror index (filesystem scan) |
|
|
85
|
+
| `/+admin-api/versions/{user}/{index}/{project}` | GET | Version list with cached/uncached distinction |
|
|
86
|
+
| `/+admin-api/versions/{user}/{index}/{project}?all=1` | GET | Include all upstream versions (mirrors) |
|
|
87
|
+
| `/+admin-api/versiondata/{user}/{index}/{project}/{version}` | GET | Metadata + file links for a single version |
|
|
88
|
+
|
|
89
|
+
## Installation
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install devpi-admin
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
This pulls in `devpi-server` as a dependency. If you are using devpi in a dedicated venv
|
|
96
|
+
(recommended), install the plugin into the same venv:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
/var/lib/pypi/venv/bin/pip install devpi-admin
|
|
100
|
+
systemctl --user restart devpi # or however you run devpi-server
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
You should uninstall `devpi-web` — `devpi-admin` replaces it entirely:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pip uninstall devpi-web
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Both plugins can technically coexist but it is not recommended. `devpi-admin` intercepts `/`
|
|
110
|
+
for HTML requests while `devpi-web` would still serve its own HTML on other routes like
|
|
111
|
+
`/<user>/<index>/<package>`, leading to a confusing mixed experience.
|
|
112
|
+
|
|
113
|
+
## Usage
|
|
114
|
+
|
|
115
|
+
After restart, open:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
http://<your-devpi-host>:3141/
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Browser visits to `/` are redirected to `/+admin/`, which serves the SPA. Direct links like
|
|
122
|
+
`http://<host>:3141/+admin/#packages/ci/testing` work and can be bookmarked.
|
|
123
|
+
|
|
124
|
+
devpi CLI tools and other JSON clients are unaffected — they send `Accept: application/json`
|
|
125
|
+
and bypass the redirect.
|
|
126
|
+
|
|
127
|
+
## How it works
|
|
128
|
+
|
|
129
|
+
`devpi-admin` registers a `devpi_server` entry point that hooks into
|
|
130
|
+
`devpiserver_pyramid_configure` (with `@hookimpl` from pluggy) to:
|
|
131
|
+
|
|
132
|
+
1. Serve the bundled static assets under `/+admin/` via a Pyramid static view.
|
|
133
|
+
2. Add an explicit view at `/+admin/` that returns `index.html`.
|
|
134
|
+
3. Register custom API views under `/+admin-api/` for cached-package and per-version queries.
|
|
135
|
+
4. Install a tween that redirects HTML browser requests on `/` to `/+admin/` while leaving
|
|
136
|
+
JSON requests intact.
|
|
137
|
+
|
|
138
|
+
The plugin uses devpi-server internals (`xom.model.getstage`, `stage.list_versions`,
|
|
139
|
+
`stage.get_versiondata`, `stage.get_releaselinks`) and direct filesystem access
|
|
140
|
+
(`serverdir/+files/`) for the cached-packages API.
|
|
141
|
+
|
|
142
|
+
## Requirements
|
|
143
|
+
|
|
144
|
+
- Python 3.9+
|
|
145
|
+
- devpi-server 6.0+
|
|
146
|
+
- A browser with ES6 support (`Promise`, `fetch`, `sessionStorage`)
|
|
147
|
+
|
|
148
|
+
## Routes (UI)
|
|
149
|
+
|
|
150
|
+
Routing is hash-based, so any of these URLs can be bookmarked or shared:
|
|
151
|
+
|
|
152
|
+
| Hash | View |
|
|
153
|
+
|------|------|
|
|
154
|
+
| `#` | Status dashboard (default) |
|
|
155
|
+
| `#indexes` | All indexes |
|
|
156
|
+
| `#indexes/<user>` | Indexes filtered by user |
|
|
157
|
+
| `#packages/<user>/<index>` | Packages in an index |
|
|
158
|
+
| `#package/<user>/<index>/<name>` | Package detail (latest cached version) |
|
|
159
|
+
| `#package/<user>/<index>/<name>?version=<ver>` | Specific version |
|
|
160
|
+
| `#users` | User management (requires login) |
|
|
161
|
+
|
|
162
|
+
## Project layout
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
devpi-admin/
|
|
166
|
+
├── pyproject.toml
|
|
167
|
+
├── README.md
|
|
168
|
+
├── LICENSE
|
|
169
|
+
├── .github/workflows/
|
|
170
|
+
│ ├── tests.yml — CI on push/PR (Python 3.10 + 3.14)
|
|
171
|
+
│ └── publish.yml — publish to PyPI on release
|
|
172
|
+
├── src/
|
|
173
|
+
│ └── devpi_admin/
|
|
174
|
+
│ ├── __init__.py — version (from git tag via hatch-vcs)
|
|
175
|
+
│ ├── main.py — Pyramid hooks, tween, API views
|
|
176
|
+
│ └── static/
|
|
177
|
+
│ ├── index.html — SPA entry point
|
|
178
|
+
│ ├── css/style.css
|
|
179
|
+
│ └── js/
|
|
180
|
+
│ ├── api.js — devpi REST wrapper + auth
|
|
181
|
+
│ ├── theme.js — theme toggle (light/dark/auto)
|
|
182
|
+
│ ├── marked.min.js — vendored markdown renderer
|
|
183
|
+
│ └── app.js — routing, views, rendering
|
|
184
|
+
└── tests/
|
|
185
|
+
├── test_cached_versions.py — filesystem scan (tmpdir)
|
|
186
|
+
├── test_helpers.py — filename parsing, normalization
|
|
187
|
+
├── test_hooks.py — pluggy hook registration
|
|
188
|
+
├── test_json_safe.py — readonly view conversion
|
|
189
|
+
├── test_package.py — entry point, static files
|
|
190
|
+
├── test_tween.py — redirect behavior
|
|
191
|
+
└── test_wants_html.py — Accept header heuristic
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Development
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
git clone <repo>
|
|
198
|
+
cd devpi-admin
|
|
199
|
+
python -m venv .venv
|
|
200
|
+
.venv/bin/pip install -e .
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The static files live at `src/devpi_admin/static/` and can be edited in place — changes
|
|
204
|
+
show up on the next browser reload, no restart of devpi-server required (static views
|
|
205
|
+
read from disk on each request). Python changes (`main.py`) require a devpi-server restart.
|
|
206
|
+
|
|
207
|
+
Run the unit tests:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
PYTHONWARNINGS="ignore::UserWarning" python -m unittest discover -v tests/
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
(The `PYTHONWARNINGS` shim hides an unrelated deprecation warning emitted by Pyramid 2.1
|
|
214
|
+
when it imports `pkg_resources`.)
|
|
215
|
+
|
|
216
|
+
## Releasing
|
|
217
|
+
|
|
218
|
+
Version is derived from the git tag via `hatch-vcs`. To release:
|
|
219
|
+
|
|
220
|
+
1. `git tag v0.1.0 && git push --tags`
|
|
221
|
+
2. On GitHub: Releases → Draft new release → select tag → Publish
|
|
222
|
+
3. The `publish.yml` workflow runs tests, builds wheel+sdist, and uploads to PyPI via trusted
|
|
223
|
+
publishing (no API tokens needed — configure the GitHub environment `pypi` in PyPI settings).
|
|
224
|
+
|
|
225
|
+
## Author
|
|
226
|
+
|
|
227
|
+
Pavel Revak <pavelrevak@gmail.com>
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
.gitignore
|
|
2
|
+
INSTALL.textile
|
|
3
|
+
LICENSE
|
|
4
|
+
README.md
|
|
5
|
+
pyproject.toml
|
|
6
|
+
.github/workflows/publish.yml
|
|
7
|
+
.github/workflows/tests.yml
|
|
8
|
+
devpi_admin/__init__.py
|
|
9
|
+
devpi_admin/_version.py
|
|
10
|
+
devpi_admin/main.py
|
|
11
|
+
devpi_admin.egg-info/PKG-INFO
|
|
12
|
+
devpi_admin.egg-info/SOURCES.txt
|
|
13
|
+
devpi_admin.egg-info/dependency_links.txt
|
|
14
|
+
devpi_admin.egg-info/entry_points.txt
|
|
15
|
+
devpi_admin.egg-info/requires.txt
|
|
16
|
+
devpi_admin.egg-info/top_level.txt
|
|
17
|
+
devpi_admin/static/favicon.svg
|
|
18
|
+
devpi_admin/static/index.html
|
|
19
|
+
devpi_admin/static/css/style.css
|
|
20
|
+
devpi_admin/static/js/api.js
|
|
21
|
+
devpi_admin/static/js/app.js
|
|
22
|
+
devpi_admin/static/js/marked.min.js
|
|
23
|
+
devpi_admin/static/js/theme.js
|
|
24
|
+
tests/__init__.py
|
|
25
|
+
tests/test_cached_versions.py
|
|
26
|
+
tests/test_helpers.py
|
|
27
|
+
tests/test_hooks.py
|
|
28
|
+
tests/test_json_safe.py
|
|
29
|
+
tests/test_package.py
|
|
30
|
+
tests/test_tween.py
|
|
31
|
+
tests/test_wants_html.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
devpi-server>=6.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
devpi_admin
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["
|
|
3
|
-
build-backend = "
|
|
2
|
+
requires = ["setuptools", "setuptools-scm"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devpi-admin"
|
|
@@ -31,21 +31,9 @@ dependencies = [
|
|
|
31
31
|
[project.entry-points.devpi_server]
|
|
32
32
|
devpi-admin = "devpi_admin.main"
|
|
33
33
|
|
|
34
|
-
[tool.
|
|
35
|
-
|
|
36
|
-
fallback-version = "0.0.0"
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
include = ["devpi_admin*"]
|
|
37
36
|
|
|
38
|
-
[tool.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
[tool.hatch.build.targets.wheel]
|
|
42
|
-
packages = ["src/devpi_admin"]
|
|
43
|
-
|
|
44
|
-
[tool.hatch.build.targets.sdist]
|
|
45
|
-
include = [
|
|
46
|
-
"src/devpi_admin",
|
|
47
|
-
"tests",
|
|
48
|
-
"README.md",
|
|
49
|
-
"pyproject.toml",
|
|
50
|
-
"LICENSE",
|
|
51
|
-
]
|
|
37
|
+
[tool.setuptools_scm]
|
|
38
|
+
write_to = "devpi_admin/_version.py"
|
|
39
|
+
fallback_version = "0.0.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|