devpi-admin 1.1.2__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.2 → devpi_admin-1.2.0}/.gitignore +1 -1
- devpi_admin-1.2.0/INSTALL.textile +190 -0
- {devpi_admin-1.1.2 → devpi_admin-1.2.0}/PKG-INFO +8 -5
- {devpi_admin-1.1.2 → devpi_admin-1.2.0}/README.md +3 -1
- {devpi_admin-1.1.2/src → devpi_admin-1.2.0}/devpi_admin/_version.py +3 -3
- {devpi_admin-1.1.2/src → devpi_admin-1.2.0}/devpi_admin/static/css/style.css +25 -0
- {devpi_admin-1.1.2/src → devpi_admin-1.2.0}/devpi_admin/static/js/app.js +61 -7
- 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.2 → devpi_admin-1.2.0}/pyproject.toml +7 -19
- devpi_admin-1.2.0/setup.cfg +4 -0
- {devpi_admin-1.1.2 → devpi_admin-1.2.0}/LICENSE +0 -0
- {devpi_admin-1.1.2/src → devpi_admin-1.2.0}/devpi_admin/__init__.py +0 -0
- {devpi_admin-1.1.2/src → devpi_admin-1.2.0}/devpi_admin/main.py +0 -0
- {devpi_admin-1.1.2/src → devpi_admin-1.2.0}/devpi_admin/static/favicon.svg +0 -0
- {devpi_admin-1.1.2/src → devpi_admin-1.2.0}/devpi_admin/static/index.html +0 -0
- {devpi_admin-1.1.2/src → devpi_admin-1.2.0}/devpi_admin/static/js/api.js +0 -0
- {devpi_admin-1.1.2/src → devpi_admin-1.2.0}/devpi_admin/static/js/marked.min.js +0 -0
- {devpi_admin-1.1.2/src → devpi_admin-1.2.0}/devpi_admin/static/js/theme.js +0 -0
- {devpi_admin-1.1.2 → devpi_admin-1.2.0}/tests/__init__.py +0 -0
- {devpi_admin-1.1.2 → devpi_admin-1.2.0}/tests/test_cached_versions.py +0 -0
- {devpi_admin-1.1.2 → devpi_admin-1.2.0}/tests/test_helpers.py +0 -0
- {devpi_admin-1.1.2 → devpi_admin-1.2.0}/tests/test_hooks.py +0 -0
- {devpi_admin-1.1.2 → devpi_admin-1.2.0}/tests/test_json_safe.py +0 -0
- {devpi_admin-1.1.2 → devpi_admin-1.2.0}/tests/test_package.py +0 -0
- {devpi_admin-1.1.2 → devpi_admin-1.2.0}/tests/test_tween.py +0 -0
- {devpi_admin-1.1.2 → 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'
|
|
@@ -1536,6 +1536,31 @@ body {
|
|
|
1536
1536
|
margin-bottom: 4px;
|
|
1537
1537
|
}
|
|
1538
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
|
+
|
|
1539
1564
|
.status-row {
|
|
1540
1565
|
display: flex;
|
|
1541
1566
|
justify-content: space-between;
|
|
@@ -1185,6 +1185,12 @@
|
|
|
1185
1185
|
|
|
1186
1186
|
var PKG_LIMIT = 100;
|
|
1187
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
|
+
|
|
1188
1194
|
function buildPackageCard(indexPath, pkg, fetchVersion) {
|
|
1189
1195
|
var card = el('div', {className: 'pkg-card'});
|
|
1190
1196
|
var cardHead = el('div', {className: 'pkg-card-head'});
|
|
@@ -1194,9 +1200,11 @@
|
|
|
1194
1200
|
textContent: pkg,
|
|
1195
1201
|
}));
|
|
1196
1202
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1203
|
+
if (canDeleteFromIndex(indexPath)) {
|
|
1204
|
+
cardHead.appendChild(buildKebabMenu([
|
|
1205
|
+
{label: 'Delete all versions', danger: true, onclick: function () { closeAllKebabs(); deletePackage(indexPath, pkg); }},
|
|
1206
|
+
]));
|
|
1207
|
+
}
|
|
1200
1208
|
card.appendChild(cardHead);
|
|
1201
1209
|
|
|
1202
1210
|
card.appendChild(buildPipBlock(indexPath, pkg));
|
|
@@ -1240,7 +1248,8 @@
|
|
|
1240
1248
|
function fetchStage() {
|
|
1241
1249
|
showHeadingAndLoading(false);
|
|
1242
1250
|
Api.get('/' + indexPath).then(function (data) {
|
|
1243
|
-
|
|
1251
|
+
var resultIsMirror = !!(data.result && data.result.type === 'mirror');
|
|
1252
|
+
renderPackages(indexPath, data.result, resultIsMirror);
|
|
1244
1253
|
}).catch(handleApiError);
|
|
1245
1254
|
}
|
|
1246
1255
|
|
|
@@ -1443,13 +1452,13 @@
|
|
|
1443
1452
|
' ',
|
|
1444
1453
|
el('span', {className: 'page-heading-version', textContent: 'v' + currentVer}),
|
|
1445
1454
|
]),
|
|
1446
|
-
el('div', {className: 'view-header-actions'}, [
|
|
1455
|
+
el('div', {className: 'view-header-actions'}, canDeleteFromIndex(indexPath) ? [
|
|
1447
1456
|
el('button', {
|
|
1448
|
-
className: 'btn btn-danger
|
|
1457
|
+
className: 'btn btn-danger',
|
|
1449
1458
|
textContent: 'Delete package',
|
|
1450
1459
|
onclick: function () { deletePackage(indexPath, pkg); },
|
|
1451
1460
|
}),
|
|
1452
|
-
]),
|
|
1461
|
+
] : []),
|
|
1453
1462
|
]));
|
|
1454
1463
|
|
|
1455
1464
|
if (cachedVersions.length === 0 && !allVersions) {
|
|
@@ -1972,10 +1981,55 @@
|
|
|
1972
1981
|
grid.appendChild(whooshCard);
|
|
1973
1982
|
}
|
|
1974
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
|
+
|
|
1975
2022
|
content.appendChild(grid);
|
|
1976
2023
|
}).catch(handleApiError);
|
|
1977
2024
|
}
|
|
1978
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
|
+
|
|
1979
2033
|
function formatNum(n) {
|
|
1980
2034
|
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
1981
2035
|
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|