mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
Compare commits
256 commits
debian/4.3
...
dev
Author | SHA1 | Date | |
---|---|---|---|
|
8de98670e9 | ||
|
068d6d369a | ||
|
3c7f55c610 | ||
|
531c972bed | ||
|
7c378210fa | ||
|
497ea8a4af | ||
|
65d694280c | ||
|
dc1ce5a9ab | ||
|
663d80666a | ||
|
36c20d5582 | ||
|
95503ca2e6 | ||
|
96539ce5c7 | ||
|
578fb497e9 | ||
|
954d608a61 | ||
|
ac447a1f9d | ||
|
c63634f86f | ||
|
14bbff41ad | ||
|
291ed50843 | ||
|
5cb073bb75 | ||
|
8eb3ce3377 | ||
|
d1a7445773 | ||
|
41e170a214 | ||
|
dbcadf0c87 | ||
|
7f2563e65a | ||
|
e8b46f8c9b | ||
|
e196ed34c4 | ||
|
750aab64da | ||
|
1cac05b9fc | ||
|
f4712d9300 | ||
|
e96c380d29 | ||
|
d81b4578e9 | ||
|
0ffda1a0db | ||
|
b2b912e7b9 | ||
|
4a52f20417 | ||
|
cdd5ee6134 | ||
|
efc7d23738 | ||
|
2a2bb468fc | ||
|
f7a9678b59 | ||
|
50ce4c9b03 | ||
|
3bed227f6d | ||
|
45b1a92743 | ||
|
257e7095e5 | ||
|
4dfa2ee710 | ||
|
080d244f44 | ||
|
39113a8ed5 | ||
|
fe607a2ef1 | ||
|
d59f142019 | ||
|
c59b869286 | ||
|
8c38478d02 | ||
|
e8e206b482 | ||
|
21ecfbcd69 | ||
|
8bde8eee14 | ||
|
0ee77f922e | ||
|
97a24dbbce | ||
|
6e5429ce00 | ||
|
0ad9eac48c | ||
|
e00be08bee | ||
|
9316db6054 | ||
|
6e434cddc6 | ||
|
c698807f12 | ||
|
95f7e6804a | ||
|
c8193819c7 | ||
|
cadb449528 | ||
|
518e5ebb85 | ||
|
d09e1a2fab | ||
|
f1010fb8a3 | ||
|
7a46e1499d | ||
|
1b28bca0c0 | ||
|
d0089dbdf9 | ||
|
ecdc43a09a | ||
|
38d83520b5 | ||
|
1d3908c9a5 | ||
|
a69b276188 | ||
|
04a688a141 | ||
|
ed0cd88da9 | ||
|
b4e79bd278 | ||
|
5ec28b19c0 | ||
|
64a39cc595 | ||
|
e5fa7ab734 | ||
|
d66fd94f68 | ||
|
145fd9a91a | ||
|
0c71095db3 | ||
|
c06e1a91c9 | ||
|
d4769ec0a5 | ||
|
9078c881ea | ||
|
f6234577c4 | ||
|
6ec06b4ce3 | ||
|
d1e10e23d4 | ||
|
c5aeb1acad | ||
|
2373a7fa5e | ||
|
d1827d1a41 | ||
|
f1e7984fc9 | ||
|
1fb77feec3 | ||
|
52bbab7df1 | ||
|
aaf7f764e3 | ||
|
4d2694ef8d | ||
|
2539ea9c70 | ||
|
d7162b209a | ||
|
cabe9a728b | ||
|
4c03e16de9 | ||
|
9fc33eec1a | ||
|
95695e94d7 | ||
|
bc9b4a2ce2 | ||
|
55a09c57ac | ||
|
bbd2468e35 | ||
|
12c5482d6c | ||
|
ea90c056b7 | ||
|
c99439740f | ||
|
2195ed6f90 | ||
|
2d2eab4c30 | ||
|
9bc187d404 | ||
|
d0e65fdb46 | ||
|
01a4dc0942 | ||
|
baa00c0812 | ||
|
1e79e99cc8 | ||
|
ac0ac24996 | ||
|
7f4e8b394c | ||
|
aa06187284 | ||
|
676a19dc28 | ||
|
161c5d5c2b | ||
|
80873777c6 | ||
|
50b19a95c6 | ||
|
5258f22985 | ||
|
d54dd286be | ||
|
ceba5273f2 | ||
|
6bdd6dcecb | ||
|
fcd84e3e34 | ||
|
9dd1a8a63e | ||
|
4239f466f8 | ||
|
b20b1af116 | ||
|
eadf40c552 | ||
|
b27f2351ca | ||
|
0d2fbb3ad0 | ||
|
f0632f0927 | ||
|
f9d899ca52 | ||
|
fc13528570 | ||
|
5f6d5ac4de | ||
|
4a13653f3b | ||
|
e6ca65923c | ||
|
70e41495a4 | ||
|
6f9b0f4ea4 | ||
|
e654b67180 | ||
|
641335ea7c | ||
|
e4aba32128 | ||
|
d96d2b9c15 | ||
|
b19f1f6f80 | ||
|
5c0f025e46 | ||
|
e1e98263cd | ||
|
2e117e8d63 | ||
|
4fa7568c84 | ||
|
9700d8e293 | ||
|
bcf41f6ccc | ||
|
74f2c26da6 | ||
|
b0b440e4cc | ||
|
cb9ecb468d | ||
|
7e2da7a897 | ||
|
45dbcd98f4 | ||
|
23d6107861 | ||
|
d8cb39d809 | ||
|
0ff3f45d92 | ||
|
5434a8994c | ||
|
5192c66f95 | ||
|
88bf138501 | ||
|
496e387900 | ||
|
e8c0ffac53 | ||
|
8473743fea | ||
|
5440053d5f | ||
|
9ccc914ca1 | ||
|
35351696ac | ||
|
c04aac6d98 | ||
|
be0006fdb9 | ||
|
29d0d0cfc1 | ||
|
8b05fcf2fd | ||
|
0afface3f2 | ||
|
ea6eaa6b1e | ||
|
ccc005fb84 | ||
|
37d43d1bdf | ||
|
d10879faa2 | ||
|
83a95fd8c9 | ||
|
ce8322bbf4 | ||
|
ef08a8be53 | ||
|
9855b6d7f5 | ||
|
8127e7cd1a | ||
|
964483b23b | ||
|
ee1e63c7a1 | ||
|
2fc9611b53 | ||
|
265753aef7 | ||
|
a53dd3d175 | ||
|
5b506a5f70 | ||
|
27bbd81f49 | ||
|
9fcc9630bd | ||
|
b2c67369a8 | ||
|
845399dba0 | ||
|
96b7cb237e | ||
|
be3b9f7a53 | ||
|
b7c05f1daa | ||
|
cb6e0464ad | ||
|
166fd7e824 | ||
|
c5700f1ba3 | ||
|
c5f577c04f | ||
|
fc4b31bdea | ||
|
40ca5a8ad6 | ||
|
56ce07d114 | ||
|
1c412bf8ea | ||
|
a745c93a58 | ||
|
891216dec4 | ||
|
7694024c58 | ||
|
07b6779eac | ||
|
d997e5f912 | ||
|
551ad28ed2 | ||
|
c44e025210 | ||
|
5d61b74fde | ||
|
12218bcbae | ||
|
d901d28add | ||
|
9130574191 | ||
|
aaeeb0a02e | ||
|
f2ffcd09b2 | ||
|
824a9ed42e | ||
|
b3d950ca4b | ||
|
84ccb993ab | ||
|
63defa3926 | ||
|
8ef882b4f5 | ||
|
14e37366df | ||
|
f25d202fee | ||
|
e01fd04c7a | ||
|
7ab1999ffd | ||
|
8b4a29b8ce | ||
|
6fa32f548d | ||
|
4f3ba338ed | ||
|
02d925e712 | ||
|
bbf498321f | ||
|
2e3fe5b4af | ||
|
d2ed30c172 | ||
|
b224dd356a | ||
|
c1029d9787 | ||
|
974f15d7d0 | ||
|
ce3ec1025c | ||
|
433f84323f | ||
|
3c21a5cfbf | ||
|
2ce920e2ff | ||
|
2f59023eee | ||
|
2b62f3d2e6 | ||
|
088ef05e07 | ||
|
c4b559cf6b | ||
|
1f7bb1d54c | ||
|
681517bc86 | ||
|
8e3a52446a | ||
|
b0a2b0efeb | ||
|
2339377763 | ||
|
8218c2061a | ||
|
3cb81a54fc | ||
|
4cacaefbfe | ||
|
bdec23b533 | ||
|
4774e2a0a9 | ||
|
7c89e44404 | ||
|
c1c517d5b2 |
78 changed files with 1129 additions and 918 deletions
19
.github/workflows/autoblack.yml
vendored
19
.github/workflows/autoblack.yml
vendored
|
@ -8,16 +8,23 @@ jobs:
|
||||||
name: Check / auto apply black
|
name: Check / auto apply black
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Check files using the black formatter
|
- name: Check files using the black formatter
|
||||||
uses: rickstaa/action-black@v1
|
uses: psf/black@stable
|
||||||
id: action_black
|
id: black
|
||||||
with:
|
with:
|
||||||
black_args: "."
|
options: "."
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
- shell: pwsh
|
||||||
|
id: check_files_changed
|
||||||
|
run: |
|
||||||
|
# Diff HEAD with the previous commit
|
||||||
|
$diff = git diff
|
||||||
|
$HasDiff = $diff.Length -gt 0
|
||||||
|
Write-Host "::set-output name=files_changed::$HasDiff"
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
if: steps.action_black.outputs.is_formatted == 'true'
|
if: steps.check_files_changed.outputs.files_changed == 'true'
|
||||||
uses: peter-evans/create-pull-request@v3
|
uses: peter-evans/create-pull-request@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
title: "Format Python code with Black"
|
title: "Format Python code with Black"
|
||||||
|
|
4
.github/workflows/i18n.yml
vendored
4
.github/workflows/i18n.yml
vendored
|
@ -8,7 +8,7 @@ jobs:
|
||||||
name: Autoreformat locale files
|
name: Autoreformat locale files
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Apply reformating scripts
|
- name: Apply reformating scripts
|
||||||
id: action_reformat
|
id: action_reformat
|
||||||
run: |
|
run: |
|
||||||
|
@ -18,7 +18,7 @@ jobs:
|
||||||
git diff -w --exit-code
|
git diff -w --exit-code
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
if: ${{ failure() }}
|
if: ${{ failure() }}
|
||||||
uses: peter-evans/create-pull-request@v3
|
uses: peter-evans/create-pull-request@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
title: "Reformat locale files"
|
title: "Reformat locale files"
|
||||||
|
|
19
.github/workflows/tox.yml
vendored
19
.github/workflows/tox.yml
vendored
|
@ -4,6 +4,7 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
|
- bullseye
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -11,11 +12,11 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [3.7]
|
python-version: [3.9]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install apt dependencies
|
- name: Install apt dependencies
|
||||||
|
@ -25,17 +26,17 @@ jobs:
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install tox tox-gh-actions
|
pip install tox tox-gh-actions
|
||||||
- name: Test with tox
|
- name: Test with tox
|
||||||
run: tox -e py37-pytest
|
run: tox -e py39-pytest
|
||||||
|
|
||||||
invalidcode:
|
invalidcode:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [3.7]
|
python-version: [3.9]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install tox
|
- name: Install tox
|
||||||
|
@ -43,6 +44,6 @@ jobs:
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install tox tox-gh-actions
|
pip install tox tox-gh-actions
|
||||||
- name: Linter
|
- name: Linter
|
||||||
run: tox -e py37-invalidcode
|
run: tox -e py39-invalidcode
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: tox -e py37-mypy
|
run: tox -e py39-mypy
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|

|
||||||
[](https://github.com/YunoHost/moulinette/actions/workflows/tox.yml)
|
[](https://github.com/YunoHost/moulinette/actions/workflows/tox.yml)
|
||||||
|
[](https://lgtm.com/projects/g/YunoHost/moulinette/context:python)
|
||||||
[](https://github.com/YunoHost/moulinette/blob/dev/LICENSE)
|
[](https://github.com/YunoHost/moulinette/blob/dev/LICENSE)
|
||||||
|
|
||||||
|
|
||||||
|
|
169
debian/changelog
vendored
169
debian/changelog
vendored
|
@ -1,3 +1,172 @@
|
||||||
|
moulinette (11.2.1) stable; urgency=low
|
||||||
|
|
||||||
|
- repo chores: various black enhancements
|
||||||
|
- [i18n] Translations updated for Arabic, Basque, Catalan, Chinese (Simplified), Czech, Dutch, English, Esperanto, French, Galician, German, Hindi, Indonesian, Italian, Japanese, Nepali, Norwegian Bokmål, Persian, Polish, Portuguese, Russian, Spanish, Swedish, Turkish, Ukrainian
|
||||||
|
|
||||||
|
Thanks to all contributors <3 ! (Alexandre Aubin, Francescc, José M, Metin Bektas, ppr, Psycojoker, rosbeef andino, Tagada, xabirequejo, xaloc33)
|
||||||
|
|
||||||
|
-- OniriCorpe <oniricorpe@yunohost.org> Sun, 19 May 2024 23:49:00 +0200
|
||||||
|
|
||||||
|
moulinette (11.2) stable; urgency=low
|
||||||
|
|
||||||
|
- [i18n] Translations updated for Japanese
|
||||||
|
|
||||||
|
Thanks to all contributors <3 ! (motcha)
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 17 Jul 2023 16:32:34 +0200
|
||||||
|
|
||||||
|
moulinette (11.1.5) stable; urgency=low
|
||||||
|
|
||||||
|
- setup.py: fix version specifier in python_requires, python tooling not happy with * i guess (2373a7fa)
|
||||||
|
- auth: prevent stupid issue where outdated cookie usage would trigger error 500 intead of 401, resulting in a ~bug after Yunohost self-upgrade and the webadmin is confused about the API not being up again (c06e1a91)
|
||||||
|
- i18n: Translations updated for Chinese (Simplified), Indonesian, Japanese
|
||||||
|
|
||||||
|
Thanks to all contributors <3 ! (liimee, motcha, Neko Nekowazarashi, Poesty Li)
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 10 Jul 2023 21:32:20 +0200
|
||||||
|
|
||||||
|
moulinette (11.1.4) stable; urgency=low
|
||||||
|
|
||||||
|
- Releasing as stable
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 01 Feb 2023 20:28:56 +0100
|
||||||
|
|
||||||
|
moulinette (11.1.2.1) testing; urgency=low
|
||||||
|
|
||||||
|
- Handle calling 'yunohost' with no args more gracefully (4c03e16d)
|
||||||
|
- [i18n] Translations updated for Arabic
|
||||||
|
|
||||||
|
Thanks to all contributors <3 ! (ButterflyOfFire)
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 30 Jan 2023 16:34:23 +0100
|
||||||
|
|
||||||
|
moulinette (11.1.2) testing; urgency=low
|
||||||
|
|
||||||
|
- legacy: Remove the 'global argument' mechanism ... we only use it for --version and it's just batshit overly complicated for what this achieves... (50b19a95, 80873777, 7f4e8b39)
|
||||||
|
- Dont take lock for read/GET operations ([#327](https://github.com/yunohost/moulinette/pull/327))
|
||||||
|
- [i18n] Translations updated for Arabic, Basque, Dutch
|
||||||
|
|
||||||
|
Thanks to all contributors <3 ! (André Koot, ButterflyOfFire, xabirequejo)
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Fri, 06 Jan 2023 00:37:23 +0100
|
||||||
|
|
||||||
|
moulinette (11.1.0) testing; urgency=low
|
||||||
|
|
||||||
|
- Bump version for testing release
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 26 Oct 2022 16:15:37 +0200
|
||||||
|
|
||||||
|
moulinette (11.0.9) stable; urgency=low
|
||||||
|
|
||||||
|
- Bump version for stable release
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Sun, 07 Aug 2022 23:30:35 +0200
|
||||||
|
|
||||||
|
moulinette (11.0.8) testing; urgency=low
|
||||||
|
|
||||||
|
- [fix] random_ascii helper was generating inconsistent string length (4239f466)
|
||||||
|
- [i18n] Translations updated for German, Polish, Slovak
|
||||||
|
|
||||||
|
Thanks to all contributors <3 ! (Gregor, Jose Riha, Radek Raczkowski)
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Sun, 07 Aug 2022 11:55:06 +0200
|
||||||
|
|
||||||
|
moulinette (11.0.7) testing; urgency=low
|
||||||
|
|
||||||
|
- [i18n] Translations updated for Spanish
|
||||||
|
|
||||||
|
Thanks to all contributors <3 ! (JimScope, Alexandre Aubin)
|
||||||
|
|
||||||
|
-- tituspijean <titus+yunohost@pijean.ovh> Wed, 18 May 2022 00:02:25 +0200
|
||||||
|
|
||||||
|
moulinette (11.0.6) testing; urgency=low
|
||||||
|
|
||||||
|
- [enh] cli: Add possibility to hide commands in --help (cb9ecb46)
|
||||||
|
- [i18n] Translations updated for Finnish, Galician
|
||||||
|
|
||||||
|
Thanks to all contributors <3 ! (Alexandre Aubin, alexAubin, José M, Mico Hauataluoma, Weblate)
|
||||||
|
|
||||||
|
-- Kay0u <pierre@kayou.io> Tue, 29 Mar 2022 14:23:40 +0200
|
||||||
|
|
||||||
|
moulinette (11.0.2) testing; urgency=low
|
||||||
|
|
||||||
|
- [fix] Various tweaks for Python 3.9
|
||||||
|
- [fix] cli: Adapt prompt_toolkit call because now using the v3.x of the lib (d901d28a, 12218bcb, c44e0252)
|
||||||
|
- [mod] refactor: Rework actionsmap and m18n init, drop multiple actionsmap support (9fcc9630)
|
||||||
|
- [mod] api: Move cookie session management logic to the authenticator for more flexibility ([#311](https://github.com/YunoHost/moulinette/pull/311))
|
||||||
|
|
||||||
|
Thanks to all contributors <3 ! (Kay0u)
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 19 Jan 2022 21:15:58 +0100
|
||||||
|
|
||||||
|
moulinette (4.4.0) testing; urgency=low
|
||||||
|
|
||||||
|
- Bump version for 4.4 release
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 19 Jan 2022 21:10:44 +0100
|
||||||
|
|
||||||
|
moulinette (4.3.3.1) stable; urgency=low
|
||||||
|
|
||||||
|
- [i18n] Translations updated for Dutch, Finnish, German
|
||||||
|
|
||||||
|
Thanks to all contributors <3 ! (Boudewijn, Gregor, Kay0u, Mico Hauataluoma)
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 19 Jan 2022 21:10:44 +0100
|
||||||
|
|
||||||
|
moulinette (4.3.3) stable; urgency=low
|
||||||
|
|
||||||
|
- [enh] quality: Apply pyupgrade ([#312](https://github.com/YunoHost/moulinette/pull/312))
|
||||||
|
- [i18n] Translations updated for Czech, German, Portuguese
|
||||||
|
|
||||||
|
Thanks to all contributors <3 ! (Bram, Christian Wehrli, maique madeira, Radek S, Valentin von Guttenberg)
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 29 Dec 2021 01:08:10 +0100
|
||||||
|
|
||||||
|
moulinette (4.3.2.2) stable; urgency=low
|
||||||
|
|
||||||
|
Aaaaaand typoed 'testing' instead of 'stable' in previous changelog
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 15 Nov 2021 18:44:09 +0100
|
||||||
|
|
||||||
|
moulinette (4.3.2.1) testing; urgency=low
|
||||||
|
|
||||||
|
- [fix] api: 'Missing credentials parameter' bug : request.POST is buggy when value contains special chars ... request.params appears to be more reliable (c5f577c0)
|
||||||
|
- [fix] api: issue with accented chars in form .. decode() is not needed anymore? (c5700f1b)
|
||||||
|
- [i18n] Translations updated for Chinese (Simplified), Italian
|
||||||
|
|
||||||
|
Thanks to all contributors <3 ! (dagangtie, Flavio Cristoforetti)
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 15 Nov 2021 18:44:09 +0100
|
||||||
|
|
||||||
|
moulinette (4.3.2) stable; urgency=low
|
||||||
|
|
||||||
|
- Bump version for stable release
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Fri, 05 Nov 2021 02:35:56 +0100
|
||||||
|
|
||||||
|
moulinette (4.3.1.2) testing; urgency=low
|
||||||
|
|
||||||
|
- [i18n] Translations updated for Basque, Occitan, Russian, Spanish
|
||||||
|
|
||||||
|
Thanks to all contributors <3 ! (Page Asgardius, punkrockgirl, Quentí, Semen Turchikhin)
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 03 Nov 2021 18:42:44 +0100
|
||||||
|
|
||||||
|
moulinette (4.3.1.1) testing; urgency=low
|
||||||
|
|
||||||
|
- [enh] Add cp helper (14e37366)
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 04 Oct 2021 01:24:34 +0200
|
||||||
|
|
||||||
|
moulinette (4.3.1) testing; urgency=low
|
||||||
|
|
||||||
|
- [mod] Rework cli prompt mecanisc ([#303](https://github.com/YunoHost/moulinette/pull/303))
|
||||||
|
- [i18n] Translations updated for Indonesian, Russian, Turkish
|
||||||
|
|
||||||
|
Thanks to all contributors <3 ! (Éric Gaspar, liimee)
|
||||||
|
|
||||||
|
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 29 Sep 2021 22:37:28 +0200
|
||||||
|
|
||||||
moulinette (4.3.0) testing; urgency=low
|
moulinette (4.3.0) testing; urgency=low
|
||||||
|
|
||||||
- [enh] Allow file type in actionmaps ([#258](https://github.com/YunoHost/moulinette/pull/258))
|
- [enh] Allow file type in actionmaps ([#258](https://github.com/YunoHost/moulinette/pull/258))
|
||||||
|
|
1
debian/compat
vendored
1
debian/compat
vendored
|
@ -1 +0,0 @@
|
||||||
9
|
|
6
debian/control
vendored
6
debian/control
vendored
|
@ -2,7 +2,7 @@ Source: moulinette
|
||||||
Section: python
|
Section: python
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Maintainer: YunoHost Contributors <contrib@yunohost.org>
|
Maintainer: YunoHost Contributors <contrib@yunohost.org>
|
||||||
Build-Depends: debhelper (>= 9), python3 (>= 3.7), dh-python, python3-setuptools, python3-psutil, python3-all (>= 3.7)
|
Build-Depends: debhelper (>= 9), debhelper-compat (= 13), python3 (>= 3.7), dh-python, python3-setuptools, python3-psutil, python3-all (>= 3.7)
|
||||||
Standards-Version: 3.9.6
|
Standards-Version: 3.9.6
|
||||||
Homepage: https://github.com/YunoHost/moulinette
|
Homepage: https://github.com/YunoHost/moulinette
|
||||||
|
|
||||||
|
@ -14,7 +14,9 @@ Depends: ${misc:Depends}, ${python3:Depends},
|
||||||
python3-gevent-websocket,
|
python3-gevent-websocket,
|
||||||
python3-toml,
|
python3-toml,
|
||||||
python3-psutil,
|
python3-psutil,
|
||||||
python3-tz
|
python3-tz,
|
||||||
|
python3-prompt-toolkit,
|
||||||
|
python3-pygments
|
||||||
Breaks: yunohost (<< 4.1)
|
Breaks: yunohost (<< 4.1)
|
||||||
Description: prototype interfaces with ease in Python
|
Description: prototype interfaces with ease in Python
|
||||||
Quickly and easily prototype interfaces for your application.
|
Quickly and easily prototype interfaces for your application.
|
||||||
|
|
18
doc/conf.py
18
doc/conf.py
|
@ -65,18 +65,18 @@ source_suffix = ".rst"
|
||||||
master_doc = "index"
|
master_doc = "index"
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u"Moulinette"
|
project = "Moulinette"
|
||||||
copyright = u"2017, YunoHost Collective"
|
copyright = "2017, YunoHost Collective"
|
||||||
author = u"YunoHost Collective"
|
author = "YunoHost Collective"
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = u"2.6.1"
|
version = "2.6.1"
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = u"2.6.1"
|
release = "2.6.1"
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
@ -161,8 +161,8 @@ latex_documents = [
|
||||||
(
|
(
|
||||||
master_doc,
|
master_doc,
|
||||||
"Moulinette.tex",
|
"Moulinette.tex",
|
||||||
u"Moulinette Documentation",
|
"Moulinette Documentation",
|
||||||
u"YunoHost Collective",
|
"YunoHost Collective",
|
||||||
"manual",
|
"manual",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -172,7 +172,7 @@ latex_documents = [
|
||||||
|
|
||||||
# One entry per manual page. List of tuples
|
# One entry per manual page. List of tuples
|
||||||
# (source start file, name, description, authors, manual section).
|
# (source start file, name, description, authors, manual section).
|
||||||
man_pages = [(master_doc, "moulinette", u"Moulinette Documentation", [author], 1)]
|
man_pages = [(master_doc, "moulinette", "Moulinette Documentation", [author], 1)]
|
||||||
|
|
||||||
|
|
||||||
# -- Options for Texinfo output -------------------------------------------
|
# -- Options for Texinfo output -------------------------------------------
|
||||||
|
@ -184,7 +184,7 @@ texinfo_documents = [
|
||||||
(
|
(
|
||||||
master_doc,
|
master_doc,
|
||||||
"Moulinette",
|
"Moulinette",
|
||||||
u"Moulinette Documentation",
|
"Moulinette Documentation",
|
||||||
author,
|
author,
|
||||||
"Moulinette",
|
"Moulinette",
|
||||||
"One line description of project.",
|
"One line description of project.",
|
||||||
|
|
|
@ -34,7 +34,7 @@ ldapsearch -x -b 'dc=nodomain' | \\
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
class Element(object):
|
class Element:
|
||||||
"""Represents an LDIF entry."""
|
"""Represents an LDIF entry."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -43,7 +43,7 @@ class Element(object):
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""Returns a basic state dump."""
|
"""Returns a basic state dump."""
|
||||||
return 'Element' + str(self.index) + str(self.attributes)
|
return "Element" + str(self.index) + str(self.attributes)
|
||||||
|
|
||||||
def add(self, line):
|
def add(self, line):
|
||||||
"""Adds a line of input to the object.
|
"""Adds a line of input to the object.
|
||||||
|
@ -57,10 +57,10 @@ class Element(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _valid(line):
|
def _valid(line):
|
||||||
return line and not line.startswith('#')
|
return line and not line.startswith("#")
|
||||||
|
|
||||||
def _interesting(line):
|
def _interesting(line):
|
||||||
return line != 'objectClass: top'
|
return line != "objectClass: top"
|
||||||
|
|
||||||
if self.is_valid() and not _valid(line):
|
if self.is_valid() and not _valid(line):
|
||||||
return True
|
return True
|
||||||
|
@ -70,11 +70,11 @@ class Element(object):
|
||||||
|
|
||||||
def is_valid(self):
|
def is_valid(self):
|
||||||
"""Indicates whether a valid entry has been read."""
|
"""Indicates whether a valid entry has been read."""
|
||||||
return len(self.attributes) != 0 and self.attributes[0].startswith('dn: ')
|
return len(self.attributes) != 0 and self.attributes[0].startswith("dn: ")
|
||||||
|
|
||||||
def dn(self):
|
def dn(self):
|
||||||
"""Returns the DN for this entry."""
|
"""Returns the DN for this entry."""
|
||||||
if self.attributes[0].startswith('dn: '):
|
if self.attributes[0].startswith("dn: "):
|
||||||
return self.attributes[0][4:]
|
return self.attributes[0][4:]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@ -86,12 +86,12 @@ class Element(object):
|
||||||
Element objects) and returns a string which declares a DOT edge, or an
|
Element objects) and returns a string which declares a DOT edge, or an
|
||||||
empty string, if no parent was found.
|
empty string, if no parent was found.
|
||||||
"""
|
"""
|
||||||
dn_components = self.dn().split(',')
|
dn_components = self.dn().split(",")
|
||||||
for i in range(1, len(dn_components) + 1):
|
for i in range(1, len(dn_components) + 1):
|
||||||
parent = ','.join(dn_components[i:])
|
parent = ",".join(dn_components[i:])
|
||||||
if parent in dnmap:
|
if parent in dnmap:
|
||||||
return ' n%d->n%d\n' % (dnmap[parent].index, self.index)
|
return " n%d->n%d\n" % (dnmap[parent].index, self.index)
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
def dot(self, dnmap):
|
def dot(self, dnmap):
|
||||||
"""Returns a text representation of the node and perhaps its parent edge.
|
"""Returns a text representation of the node and perhaps its parent edge.
|
||||||
|
@ -99,6 +99,7 @@ class Element(object):
|
||||||
Args:
|
Args:
|
||||||
- dnmap: dictionary mapping dn names to Element objects
|
- dnmap: dictionary mapping dn names to Element objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _format(attributes):
|
def _format(attributes):
|
||||||
result = [TITLE_ENTRY_TEMPALTE % attributes[0]]
|
result = [TITLE_ENTRY_TEMPALTE % attributes[0]]
|
||||||
|
|
||||||
|
@ -107,9 +108,14 @@ class Element(object):
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
return TABLE_TEMPLATE % (self.index, '\n '.join(_format(self.attributes)), self.edge(dnmap))
|
return TABLE_TEMPLATE % (
|
||||||
|
self.index,
|
||||||
|
"\n ".join(_format(self.attributes)),
|
||||||
|
self.edge(dnmap),
|
||||||
|
)
|
||||||
|
|
||||||
class Converter(object):
|
|
||||||
|
class Converter:
|
||||||
"""An LDIF to DOT converter."""
|
"""An LDIF to DOT converter."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -144,7 +150,11 @@ class Converter(object):
|
||||||
e = Element()
|
e = Element()
|
||||||
if e.is_valid():
|
if e.is_valid():
|
||||||
self._append(e)
|
self._append(e)
|
||||||
return (BASE_TEMPLATE % (name, ''.join([e.dot(self.dnmap) for e in self.elements])))
|
return BASE_TEMPLATE % (
|
||||||
|
name,
|
||||||
|
"".join([e.dot(self.dnmap) for e in self.elements]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
BASE_TEMPLATE = """\
|
BASE_TEMPLATE = """\
|
||||||
strict digraph "%s" {
|
strict digraph "%s" {
|
||||||
|
@ -191,13 +201,13 @@ ENTRY_TEMPALTE = """\
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
if len(sys.argv) > 2:
|
if len(sys.argv) > 2:
|
||||||
raise 'Expected at most one argument.'
|
raise "Expected at most one argument."
|
||||||
elif len(sys.argv) == 2:
|
elif len(sys.argv) == 2:
|
||||||
name = sys.argv[1]
|
name = sys.argv[1]
|
||||||
file = open(sys.argv[1], 'r')
|
file = open(sys.argv[1], "r")
|
||||||
else:
|
else:
|
||||||
name = '<stdin>'
|
name = "<stdin>"
|
||||||
file = sys.stdin
|
file = sys.stdin
|
||||||
print Converter().parse(file, name)
|
print(Converter().parse(file, name))
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
"argument_required": "المُعامِل '{argument}' مطلوب",
|
"argument_required": "المُعامِل '{argument}' مطلوب",
|
||||||
"authentication_required": "المصادقة مطلوبة",
|
"authentication_required": "المصادقة مطلوبة",
|
||||||
"colon": "{}: ",
|
|
||||||
"confirm": "تأكيد {prompt}",
|
"confirm": "تأكيد {prompt}",
|
||||||
"deprecated_command": "'{prog} {command}' تم التخلي عنه و سوف تتم إزالته مستقبلا",
|
"deprecated_command": "'{prog} {command}' تم التخلي عنه و سوف تتم إزالته مستقبلا",
|
||||||
"deprecated_command_alias": "'{prog} {old}' تم التخلي عنه و سوف يتم إزالته مستقبلا، إستخدم '{prog} {new}' بدلا من ذلك",
|
"deprecated_command_alias": "'{prog} {old}' تم التخلي عنه و سوف يتم إزالته مستقبلا، إستخدم '{prog} {new}' بدلا من ذلك",
|
||||||
|
@ -29,18 +28,20 @@
|
||||||
"cannot_open_file": "ليس بالإمكان فتح الملف {file} (السبب : {error})",
|
"cannot_open_file": "ليس بالإمكان فتح الملف {file} (السبب : {error})",
|
||||||
"cannot_write_file": "لا يمكن الكتابة في الملف {file} (السبب : {error})",
|
"cannot_write_file": "لا يمكن الكتابة في الملف {file} (السبب : {error})",
|
||||||
"unknown_error_reading_file": "طرأ هناك خطأ ما أثناء عملية قراءة الملف {file} (السبب: {error})",
|
"unknown_error_reading_file": "طرأ هناك خطأ ما أثناء عملية قراءة الملف {file} (السبب: {error})",
|
||||||
"corrupted_json": "قراءة json مُشوّهة مِن {ressource} (السبب : {error})",
|
"corrupted_json": "قراءة ملف JSON مُشوّهة مِن {ressource} (السبب : {error})",
|
||||||
"error_writing_file": "طرأ هناك خطأ أثناء الكتابة في الملف {file}: {error}",
|
"error_writing_file": "طرأ هناك خطأ أثناء الكتابة في الملف {file}: {error}",
|
||||||
"error_removing": "خطأ أثناء عملية حذف {path}: {error}",
|
"error_removing": "خطأ أثناء عملية حذف {path}: {error}",
|
||||||
"error_changing_file_permissions": "خطأ أثناء عملية تعديل التصريحات لـ {path}: {error}",
|
"error_changing_file_permissions": "خطأ أثناء عملية تعديل التصريحات لـ {path}: {error}",
|
||||||
"invalid_url": "خطأ في عنوان الرابط {url} (هل هذا الموقع موجود حقًا ؟)",
|
"invalid_url": "فشل الاتصال بـ {url}… ربما تكون الخدمة معطلة ، أو أنك غير متصل بشكل صحيح بالإنترنت في IPv4 / IPv6.",
|
||||||
"download_ssl_error": "خطأ في الاتصال الآمن عبر الـ SSL أثناء محاولة الربط بـ {url}",
|
"download_ssl_error": "خطأ في الاتصال الآمن عبر الـ SSL أثناء محاولة الربط بـ {url}",
|
||||||
"download_timeout": "{url} استغرق مدة طويلة جدا للإستجابة، فتوقّف.",
|
"download_timeout": "{url} استغرق مدة طويلة جدا للإستجابة، فتوقّف.",
|
||||||
"download_unknown_error": "خطأ أثناء عملية تنزيل البيانات مِن {url} : {error}",
|
"download_unknown_error": "خطأ أثناء عملية تنزيل البيانات مِن {url} : {error}",
|
||||||
"download_bad_status_code": "{url} أعاد رمز الحالة {code}",
|
"download_bad_status_code": "{url} أعاد رمز الحالة {code}",
|
||||||
"corrupted_yaml": "قراءة مُشوّهة لنسق yaml مِن {ressource} (السبب : {error})",
|
"corrupted_yaml": "قراءة مُشوّهة لملف YAML مِن {ressource} (السبب : {error})",
|
||||||
"info": "معلومة:",
|
"info": "معلومة:",
|
||||||
"warn_the_user_about_waiting_lock_again": "جارٍ الانتظار…",
|
"warn_the_user_about_waiting_lock_again": "جارٍ الانتظار…",
|
||||||
"warn_the_user_that_lock_is_acquired": "لقد انتهى تنفيذ ذاك الأمر للتوّ ، جارٍ تنفيذ هذا الأمر",
|
"warn_the_user_that_lock_is_acquired": "لقد انتهى تنفيذ ذاك الأمر للتوّ ، جارٍ تنفيذ هذا الأمر",
|
||||||
"warn_the_user_about_waiting_lock": "هناك أمر لـ YunoHost قيد التشغيل حاليا. في انتظار انتهاء تنفيذه قبل تشغيل التالي"
|
"warn_the_user_about_waiting_lock": "هناك أمر لـ YunoHost قيد التشغيل حاليا. في انتظار انتهاء تنفيذه قبل تشغيل التالي",
|
||||||
|
"edit_text_question": "{}. تعديل هذا النص؟ [yN]: ",
|
||||||
|
"corrupted_toml": "قراءة مُشوّهة لملف TOML مِن {ressource} (السبب : {error})"
|
||||||
}
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
{
|
{
|
||||||
"argument_required": "Es requereix l'argument {argument}",
|
"argument_required": "Es requereix l'argument «{argument}»",
|
||||||
"authentication_required": "Es requereix autenticació",
|
"authentication_required": "Es requereix autenticació",
|
||||||
"colon": "{}: ",
|
"confirm": "Confirmar {prompt}",
|
||||||
"confirm": "Confirmar{prompt}",
|
|
||||||
"deprecated_command": "{prog}{command}és obsolet i es desinstal·larà en el futur",
|
"deprecated_command": "{prog}{command}és obsolet i es desinstal·larà en el futur",
|
||||||
"deprecated_command_alias": "{prog}{old}és obsolet i es desinstal·larà en el futur, utilitzeu {prog}{new}en el seu lloc",
|
"deprecated_command_alias": "{prog}{old}és obsolet i es desinstal·larà en el futur, utilitzeu {prog}{new}en el seu lloc",
|
||||||
"error": "Error:",
|
"error": "Error:",
|
||||||
|
@ -34,7 +33,7 @@
|
||||||
"error_writing_file": "Error al escriure el fitxer {file}: {error}",
|
"error_writing_file": "Error al escriure el fitxer {file}: {error}",
|
||||||
"error_removing": "Error al eliminar {path}: {error}",
|
"error_removing": "Error al eliminar {path}: {error}",
|
||||||
"error_changing_file_permissions": "Error al canviar els permisos per {path}: {error}",
|
"error_changing_file_permissions": "Error al canviar els permisos per {path}: {error}",
|
||||||
"invalid_url": "URL invàlid {url} (el lloc web existeix?)",
|
"invalid_url": "No s'ha pogut connectar a {url}… pot ser que el servei estigui caigut, o que no hi hagi connexió a Internet amb IPv4/IPv6.",
|
||||||
"download_ssl_error": "Error SSL al connectar amb {url}",
|
"download_ssl_error": "Error SSL al connectar amb {url}",
|
||||||
"download_timeout": "{url} ha tardat massa en respondre, s'ha deixat d'esperar.",
|
"download_timeout": "{url} ha tardat massa en respondre, s'ha deixat d'esperar.",
|
||||||
"download_unknown_error": "Error al baixar dades des de {url}: {error}",
|
"download_unknown_error": "Error al baixar dades des de {url}: {error}",
|
||||||
|
@ -43,5 +42,6 @@
|
||||||
"corrupted_toml": "El fitxer TOML ha estat corromput en la lectura des de {ressource} (motiu: {error})",
|
"corrupted_toml": "El fitxer TOML ha estat corromput en la lectura des de {ressource} (motiu: {error})",
|
||||||
"warn_the_user_about_waiting_lock": "Hi ha una altra ordre de YunoHost en execució, s'executarà aquesta ordre un cop l'anterior hagi acabat",
|
"warn_the_user_about_waiting_lock": "Hi ha una altra ordre de YunoHost en execució, s'executarà aquesta ordre un cop l'anterior hagi acabat",
|
||||||
"warn_the_user_about_waiting_lock_again": "Encara en espera…",
|
"warn_the_user_about_waiting_lock_again": "Encara en espera…",
|
||||||
"warn_the_user_that_lock_is_acquired": "L'altra ordre tot just ha acabat, ara s'executarà aquesta ordre"
|
"warn_the_user_that_lock_is_acquired": "L'altra ordre tot just ha acabat, ara s'executarà aquesta ordre",
|
||||||
|
"edit_text_question": "{}. Edita aquest text ? [yN]: "
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
"argument_required": "参数“{argument}”是必须的",
|
"argument_required": "参数“{argument}”是必须的",
|
||||||
"authentication_required": "需要验证",
|
"authentication_required": "需要验证",
|
||||||
"colon": "{} ",
|
|
||||||
"confirm": "确认 {prompt}",
|
"confirm": "确认 {prompt}",
|
||||||
"deprecated_command": "{prog}{command}已经放弃使用,将来会删除",
|
"deprecated_command": "{prog}{command}已经放弃使用,将来会删除",
|
||||||
"deprecated_command_alias": "{prog}{old}已经放弃使用,将来会删除,请使用{prog}{new}代替",
|
"deprecated_command_alias": "{prog}{old}已经放弃使用,将来会删除,请使用{prog}{new}代替",
|
||||||
|
@ -35,13 +34,14 @@
|
||||||
"error_writing_file": "写入文件{file}失败:{error}",
|
"error_writing_file": "写入文件{file}失败:{error}",
|
||||||
"error_removing": "删除路径{path}失败:{error}",
|
"error_removing": "删除路径{path}失败:{error}",
|
||||||
"error_changing_file_permissions": "目录{path}权限修改失败:{error}",
|
"error_changing_file_permissions": "目录{path}权限修改失败:{error}",
|
||||||
"invalid_url": "URL:{url}无效(site是否存在?)",
|
"invalid_url": "{url} 连接失败… 可能是服务中断了,或者你没有正确连接到IPv4/IPv6的互联网。",
|
||||||
"download_ssl_error": "连接{url}时发生SSL错误",
|
"download_ssl_error": "连接{url}时发生SSL错误",
|
||||||
"download_timeout": "{url}响应超时,放弃。",
|
"download_timeout": "{url}响应超时,放弃。",
|
||||||
"download_unknown_error": "下载{url}失败:{error}",
|
"download_unknown_error": "下载{url}失败:{error}",
|
||||||
"download_bad_status_code": "{url}返回状态码:{code}",
|
"download_bad_status_code": "{url}返回状态码:{code}",
|
||||||
"warn_the_user_that_lock_is_acquired": "另一个命令刚刚完成,现在启动此命令",
|
"warn_the_user_that_lock_is_acquired": "另一个命令刚刚完成,现在启动此命令",
|
||||||
"warn_the_user_about_waiting_lock_again": "还在等...",
|
"warn_the_user_about_waiting_lock_again": "仍在等待…",
|
||||||
"warn_the_user_about_waiting_lock": "目前正在运行另一个YunoHost命令,我们在运行此命令之前等待它完成",
|
"warn_the_user_about_waiting_lock": "目前正在运行另一个YunoHost命令,我们在运行此命令之前等待它完成",
|
||||||
"corrupted_toml": "从{ressource:s}读取的TOML损坏(原因:{error:s})"
|
"corrupted_toml": "从{ressource}读取的TOML已损坏(原因:{error})",
|
||||||
|
"edit_text_question": "{}.编辑此文本?[yN]: "
|
||||||
}
|
}
|
|
@ -2,13 +2,13 @@
|
||||||
"password": "Heslo",
|
"password": "Heslo",
|
||||||
"logged_out": "Jste odhlášen/a",
|
"logged_out": "Jste odhlášen/a",
|
||||||
"warn_the_user_that_lock_is_acquired": "Předchozí operace dokončena, nyní spouštíme tuto",
|
"warn_the_user_that_lock_is_acquired": "Předchozí operace dokončena, nyní spouštíme tuto",
|
||||||
"warn_the_user_about_waiting_lock_again": "Stále čekáme...",
|
"warn_the_user_about_waiting_lock_again": "Stále čekáme…",
|
||||||
"warn_the_user_about_waiting_lock": "Jiná YunoHost operace právě probíhá, před spuštěním této čekáme na její dokončení",
|
"warn_the_user_about_waiting_lock": "Jiná YunoHost operace právě probíhá, před spuštěním této čekáme na její dokončení",
|
||||||
"download_bad_status_code": "{url} vrátil stavový kód {code}",
|
"download_bad_status_code": "{url} vrátil stavový kód {code}",
|
||||||
"download_unknown_error": "Chyba při stahování dat z {url}: {error}",
|
"download_unknown_error": "Chyba při stahování dat z {url}: {error}",
|
||||||
"download_timeout": "{url} příliš dlouho neodpovídá, akce přerušena.",
|
"download_timeout": "{url} příliš dlouho neodpovídá, akce přerušena.",
|
||||||
"download_ssl_error": "SSL chyba při spojení s {url}",
|
"download_ssl_error": "SSL chyba při spojení s {url}",
|
||||||
"invalid_url": "Špatný odkaz {url} (je vůbec dostupný?)",
|
"invalid_url": "Špatný odkaz {url} (je vůbec dostupný?).",
|
||||||
"error_changing_file_permissions": "Chyba při nastavování oprávnění pro {path}: {error}",
|
"error_changing_file_permissions": "Chyba při nastavování oprávnění pro {path}: {error}",
|
||||||
"error_removing": "Chyba při přesunu {path}: {error}",
|
"error_removing": "Chyba při přesunu {path}: {error}",
|
||||||
"error_writing_file": "Chyba při zápisu souboru/ů {file}: {error}",
|
"error_writing_file": "Chyba při zápisu souboru/ů {file}: {error}",
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
"deprecated_command_alias": "'{prog} {old}' je zastaralý a bude odebrán v budoucích verzích, použijte '{prog} {new}'",
|
"deprecated_command_alias": "'{prog} {old}' je zastaralý a bude odebrán v budoucích verzích, použijte '{prog} {new}'",
|
||||||
"deprecated_command": "'{prog} {command}' je zastaralý a bude odebrán v budoucích verzích",
|
"deprecated_command": "'{prog} {command}' je zastaralý a bude odebrán v budoucích verzích",
|
||||||
"confirm": "Potvrdit {prompt}",
|
"confirm": "Potvrdit {prompt}",
|
||||||
"colon": "{}: ",
|
|
||||||
"authentication_required": "Vyžadováno ověření",
|
"authentication_required": "Vyžadováno ověření",
|
||||||
"argument_required": "Je vyžadován argument '{argument}'"
|
"argument_required": "Je vyžadován argument '{argument}'",
|
||||||
|
"edit_text_question": "{}. Upravit tento text? [yN]: "
|
||||||
}
|
}
|
1
locales/da.json
Normal file
1
locales/da.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -1,10 +1,9 @@
|
||||||
{
|
{
|
||||||
"argument_required": "Der Parameter {argument} ist erforderlich",
|
"argument_required": "Der Parameter {argument} ist erforderlich",
|
||||||
"authentication_required": "Anmeldung erforderlich",
|
"authentication_required": "Anmeldung erforderlich",
|
||||||
"colon": "{}: ",
|
"confirm": "Bestätigen Sie {prompt}",
|
||||||
"confirm": "Bestätige {prompt}",
|
|
||||||
"error": "Fehler:",
|
"error": "Fehler:",
|
||||||
"file_not_exist": "Datei ist nicht vorhanden: '{path}'",
|
"file_not_exist": "Datei ist nicht vorhanden: '{path}'",
|
||||||
"folder_exists": "Ordner existiert bereits: '{path}'",
|
"folder_exists": "Ordner existiert bereits: '{path}'",
|
||||||
"instance_already_running": "Es läuft bereits eine YunoHost-Operation. Bitte warte, bis sie fertig ist, bevor du eine weitere startest.",
|
"instance_already_running": "Es läuft bereits eine YunoHost-Operation. Bitte warte, bis sie fertig ist, bevor du eine weitere startest.",
|
||||||
"invalid_argument": "Argument ungültig '{argument}': {error}",
|
"invalid_argument": "Argument ungültig '{argument}': {error}",
|
||||||
|
@ -25,7 +24,7 @@
|
||||||
"deprecated_command": "'{prog} {command}' ist veraltet und wird bald entfernt werden",
|
"deprecated_command": "'{prog} {command}' ist veraltet und wird bald entfernt werden",
|
||||||
"deprecated_command_alias": "'{prog} {old}' ist veraltet und wird bald entfernt werden, benutze '{prog} {new}' stattdessen",
|
"deprecated_command_alias": "'{prog} {old}' ist veraltet und wird bald entfernt werden, benutze '{prog} {new}' stattdessen",
|
||||||
"unknown_group": "Gruppe '{group}' ist unbekannt",
|
"unknown_group": "Gruppe '{group}' ist unbekannt",
|
||||||
"unknown_user": "Benutzer '{user}' ist unbekannt",
|
"unknown_user": "Konto '{user}' ist unbekannt",
|
||||||
"info": "Info:",
|
"info": "Info:",
|
||||||
"corrupted_json": "Beschädigtes JSON gelesen von {ressource} (reason: {error})",
|
"corrupted_json": "Beschädigtes JSON gelesen von {ressource} (reason: {error})",
|
||||||
"unknown_error_reading_file": "Unbekannter Fehler beim Lesen der Datei {file} (reason: {error})",
|
"unknown_error_reading_file": "Unbekannter Fehler beim Lesen der Datei {file} (reason: {error})",
|
||||||
|
@ -33,15 +32,16 @@
|
||||||
"cannot_open_file": "Datei {file} konnte nicht geöffnet werden (Ursache: {error})",
|
"cannot_open_file": "Datei {file} konnte nicht geöffnet werden (Ursache: {error})",
|
||||||
"corrupted_yaml": "Beschädigtes YAML gelesen von {ressource} (reason: {error})",
|
"corrupted_yaml": "Beschädigtes YAML gelesen von {ressource} (reason: {error})",
|
||||||
"warn_the_user_that_lock_is_acquired": "Der andere Befehl wurde gerade abgeschlossen, starte jetzt diesen Befehl",
|
"warn_the_user_that_lock_is_acquired": "Der andere Befehl wurde gerade abgeschlossen, starte jetzt diesen Befehl",
|
||||||
"warn_the_user_about_waiting_lock_again": "Immer noch wartend...",
|
"warn_the_user_about_waiting_lock_again": "Immer noch wartend…",
|
||||||
"warn_the_user_about_waiting_lock": "Ein anderer YunoHost Befehl läuft gerade, wir warten bis er fertig ist, bevor dieser laufen kann",
|
"warn_the_user_about_waiting_lock": "Ein anderer YunoHost Befehl läuft gerade, wir warten bis er fertig ist, bevor dieser laufen kann",
|
||||||
"download_bad_status_code": "{url} lieferte folgende(n) Status Code(s) {code}",
|
"download_bad_status_code": "{url} lieferte folgende(n) Status Code(s) {code}",
|
||||||
"download_unknown_error": "Fehler beim Herunterladen von Daten von {url}: {error}",
|
"download_unknown_error": "Fehler beim Herunterladen von Daten von {url}: {error}",
|
||||||
"download_timeout": "{url} brauchte zu lange zum Antworten, hab aufgegeben.",
|
"download_timeout": "{url} brauchte zu lange zum Antworten, hab aufgegeben.",
|
||||||
"download_ssl_error": "SSL Fehler beim Verbinden zu {url}",
|
"download_ssl_error": "SSL Fehler beim Verbinden zu {url}",
|
||||||
"invalid_url": "Ungültige URL {url} (existiert diese Seite?)",
|
"invalid_url": "Konnte keine Verbindung zu {url} herstellen… vielleicht ist der Dienst ausgefallen, oder Sie sind nicht richtig mit dem Internet in IPv4/IPv6 verbunden.",
|
||||||
"error_changing_file_permissions": "Fehler beim Ändern der Berechtigungen für {path}: {error}",
|
"error_changing_file_permissions": "Fehler beim Ändern der Berechtigungen für {path}: {error}",
|
||||||
"error_removing": "Fehler beim Entfernen {path}: {error}",
|
"error_removing": "Fehler beim Entfernen {path}: {error}",
|
||||||
"error_writing_file": "Fehler beim Schreiben von Datei {file}: {error}",
|
"error_writing_file": "Fehler beim Schreiben von Datei {file}: {error}",
|
||||||
"corrupted_toml": "Beschädigtes TOML gelesen von {ressource} (reason: {error})"
|
"corrupted_toml": "Beschädigtes TOML gelesen von {ressource} (reason: {error})",
|
||||||
|
"edit_text_question": "{}. Diesen Text bearbeiten? [yN]: "
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
"argument_required": "Argument '{argument}' is required",
|
"argument_required": "Argument '{argument}' is required",
|
||||||
"authentication_required": "Authentication required",
|
"authentication_required": "Authentication required",
|
||||||
"colon": "{}: ",
|
|
||||||
"confirm": "Confirm {prompt}",
|
"confirm": "Confirm {prompt}",
|
||||||
"deprecated_command": "'{prog} {command}' is deprecated and will be removed in the future",
|
"deprecated_command": "'{prog} {command}' is deprecated and will be removed in the future",
|
||||||
"deprecated_command_alias": "'{prog} {old}' is deprecated and will be removed in the future, use '{prog} {new}' instead",
|
"deprecated_command_alias": "'{prog} {old}' is deprecated and will be removed in the future, use '{prog} {new}' instead",
|
||||||
|
@ -37,7 +36,7 @@
|
||||||
"error_writing_file": "Error when writing file {file}: {error}",
|
"error_writing_file": "Error when writing file {file}: {error}",
|
||||||
"error_removing": "Error when removing {path}: {error}",
|
"error_removing": "Error when removing {path}: {error}",
|
||||||
"error_changing_file_permissions": "Error when changing permissions for {path}: {error}",
|
"error_changing_file_permissions": "Error when changing permissions for {path}: {error}",
|
||||||
"invalid_url": "Failed to connect to {url} ... maybe the service is down, or you are not properly connected to the Internet in IPv4/IPv6.",
|
"invalid_url": "Failed to connect to {url}... maybe the service is down, or you are not properly connected to the Internet in IPv4/IPv6.",
|
||||||
"download_ssl_error": "SSL error when connecting to {url}",
|
"download_ssl_error": "SSL error when connecting to {url}",
|
||||||
"download_timeout": "{url} took too long to answer, gave up.",
|
"download_timeout": "{url} took too long to answer, gave up.",
|
||||||
"download_unknown_error": "Error when downloading data from {url}: {error}",
|
"download_unknown_error": "Error when downloading data from {url}: {error}",
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
{
|
{
|
||||||
"password": "Pasvorto",
|
"password": "Pasvorto",
|
||||||
"colon": "{}: ",
|
|
||||||
"warn_the_user_that_lock_is_acquired": "La alia komando ĵus kompletigis, nun komencante ĉi tiun komandon",
|
"warn_the_user_that_lock_is_acquired": "La alia komando ĵus kompletigis, nun komencante ĉi tiun komandon",
|
||||||
"warn_the_user_about_waiting_lock_again": "Ankoraŭ atendanta...",
|
"warn_the_user_about_waiting_lock_again": "Ankoraŭ atendanta…",
|
||||||
"warn_the_user_about_waiting_lock": "Alia komando de YunoHost funkcias ĝuste nun, ni atendas, ke ĝi finiĝos antaŭ ol funkcii ĉi tiu",
|
"warn_the_user_about_waiting_lock": "Alia komando de YunoHost funkcias ĝuste nun, ni atendas, ke ĝi finiĝos antaŭ ol funkcii ĉi tiu",
|
||||||
"download_bad_status_code": "{url} redonita statuskodo {code}",
|
"download_bad_status_code": "{url} redonita statuskodo {code}",
|
||||||
"download_unknown_error": "Eraro dum elŝutado de datumoj de {url}: {error}",
|
"download_unknown_error": "Eraro dum elŝutado de datumoj de {url}: {error}",
|
||||||
|
@ -17,7 +16,7 @@
|
||||||
"corrupted_json": "Koruptita JSON legis de {ressource} (Kialo: {error})",
|
"corrupted_json": "Koruptita JSON legis de {ressource} (Kialo: {error})",
|
||||||
"unknown_error_reading_file": "Nekonata eraro dum provi legi dosieron {file} (kialo: {error})",
|
"unknown_error_reading_file": "Nekonata eraro dum provi legi dosieron {file} (kialo: {error})",
|
||||||
"cannot_write_file": "Ne povis skribi dosieron {file} (kialo: {error})",
|
"cannot_write_file": "Ne povis skribi dosieron {file} (kialo: {error})",
|
||||||
"cannot_open_file": "Ne povis malfermi dosieron {file: s} (kialo: {error: s})",
|
"cannot_open_file": "Ne povis malfermi dosieron {file} (kialo: {error})",
|
||||||
"websocket_request_expected": "Atendis ret-peto",
|
"websocket_request_expected": "Atendis ret-peto",
|
||||||
"warning": "Averto:",
|
"warning": "Averto:",
|
||||||
"values_mismatch": "Valoroj ne kongruas",
|
"values_mismatch": "Valoroj ne kongruas",
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
"argument_required": "Se requiere el argumento «{argument}»",
|
"argument_required": "Se requiere el argumento «{argument}»",
|
||||||
"authentication_required": "Se requiere autentificación",
|
"authentication_required": "Se requiere autentificación",
|
||||||
"colon": "{}: ",
|
|
||||||
"confirm": "Confirmar {prompt}",
|
"confirm": "Confirmar {prompt}",
|
||||||
"deprecated_command": "«{prog} {command}» está obsoleto y será eliminado en el futuro",
|
"deprecated_command": "«{prog} {command}» está obsoleto y será eliminado en el futuro",
|
||||||
"deprecated_command_alias": "«{prog} {old}» está obsoleto y se eliminará en el futuro, use «{prog} {new}» en su lugar",
|
"deprecated_command_alias": "«{prog} {old}» está obsoleto y se eliminará en el futuro, use «{prog} {new}» en su lugar",
|
||||||
|
@ -33,7 +32,7 @@
|
||||||
"error_writing_file": "Error al escribir el archivo {file}: {error}",
|
"error_writing_file": "Error al escribir el archivo {file}: {error}",
|
||||||
"error_removing": "Error al eliminar {path}: {error}",
|
"error_removing": "Error al eliminar {path}: {error}",
|
||||||
"error_changing_file_permissions": "Error al cambiar los permisos para {path}: {error}",
|
"error_changing_file_permissions": "Error al cambiar los permisos para {path}: {error}",
|
||||||
"invalid_url": "URL inválida {url} (¿Existe este sitio?)",
|
"invalid_url": "Imposible de conectarse a {url} (¿ la URL esta correcta, existe este sitio, o internet esta accesible?).",
|
||||||
"download_ssl_error": "Error SSL al conectar con {url}",
|
"download_ssl_error": "Error SSL al conectar con {url}",
|
||||||
"download_timeout": "{url} tardó demasiado en responder, abandono.",
|
"download_timeout": "{url} tardó demasiado en responder, abandono.",
|
||||||
"download_unknown_error": "Error al descargar datos desde {url} : {error}",
|
"download_unknown_error": "Error al descargar datos desde {url} : {error}",
|
||||||
|
@ -42,6 +41,7 @@
|
||||||
"info": "Información:",
|
"info": "Información:",
|
||||||
"corrupted_toml": "Lectura corrupta de TOML desde {ressource} (motivo: {error})",
|
"corrupted_toml": "Lectura corrupta de TOML desde {ressource} (motivo: {error})",
|
||||||
"warn_the_user_that_lock_is_acquired": "La otra orden recién terminó, iniciando esta orden ahora",
|
"warn_the_user_that_lock_is_acquired": "La otra orden recién terminó, iniciando esta orden ahora",
|
||||||
"warn_the_user_about_waiting_lock_again": "Aún esperando...",
|
"warn_the_user_about_waiting_lock_again": "Aún esperando…",
|
||||||
"warn_the_user_about_waiting_lock": "Otra orden de YunoHost se está ejecutando ahora, estamos esperando a que termine antes de ejecutar esta"
|
"warn_the_user_about_waiting_lock": "Otra orden de YunoHost se está ejecutando ahora, estamos esperando a que termine antes de ejecutar esta",
|
||||||
|
"edit_text_question": "{}. Editar este texto ? [sN]: "
|
||||||
}
|
}
|
|
@ -1,6 +1,47 @@
|
||||||
{
|
{
|
||||||
"argument_required": "'{argument}' argumentua beharrezkoa da",
|
"argument_required": "'{argument}' argumentua ezinbestekoa da",
|
||||||
"logged_out": "Saioa amaitu",
|
"logged_out": "Saioa amaituta",
|
||||||
"password": "Pasahitza",
|
"password": "Pasahitza",
|
||||||
"colon": "{}: "
|
"authentication_required": "Autentifikazioa behar da",
|
||||||
|
"confirm": "{prompt} baieztatu",
|
||||||
|
"edit_text_question": "{}. Testua editatu nahi al duzu? [yN]: ",
|
||||||
|
"deprecated_command": "'{prog} {command}' zaharkitua dago eta etorkizunean kenduko da",
|
||||||
|
"deprecated_command_alias": "'{prog} {old} zaharkitua dago eta etorkizunean kenduko da, erabili '{prog} {new}' haren ordez",
|
||||||
|
"error": "Errorea:",
|
||||||
|
"file_not_exist": "Fitxategia ez da existitzen: '{path}'",
|
||||||
|
"error_changing_file_permissions": "Errorea {path}-i eragiten dioten baimenak aldatzean: {error}",
|
||||||
|
"invalid_argument": "'{argument}' argumentua ez da egokia: {error}",
|
||||||
|
"success": "Arrakasta!",
|
||||||
|
"info": "Informazioa:",
|
||||||
|
"logged_in": "Saioa hasita",
|
||||||
|
"not_logged_in": "Ez duzu saiorik hasi",
|
||||||
|
"operation_interrupted": "Eragiketa geldiarazi da",
|
||||||
|
"unknown_group": "'{group}' taldea ezezaguna da",
|
||||||
|
"unknown_user": "'{user}' erabiltzailea ezezaguna da",
|
||||||
|
"cannot_write_file": "Ezinezkoa izan da {file} fitxategia idaztea (zergatia: {error})",
|
||||||
|
"download_ssl_error": "SSL errorea {url}-(e)ra konektatzean",
|
||||||
|
"corrupted_toml": "{ressource}-eko/go TOMLa kaltetuta dago (zergatia: {error})",
|
||||||
|
"warn_the_user_about_waiting_lock": "YunoHosten beste komando bat ari da exekutatzen, horrek amaitu arte gaude zain",
|
||||||
|
"warn_the_user_about_waiting_lock_again": "Zain...",
|
||||||
|
"folder_exists": "Direktorioa existitzen da dagoeneko: '{path}'",
|
||||||
|
"instance_already_running": "YunoHosten eragiketa bat exekutatzen ari da dagoeneko. Itxaron amaitu arte beste eragiketa bat abiarazi baino lehen.",
|
||||||
|
"invalid_usage": "Erabilera okerra, idatzi --help aukerak ikusteko",
|
||||||
|
"pattern_not_match": "Ez dator ereduarekin bat",
|
||||||
|
"root_required": "Ezinbestekoa da 'root' izatea eragiketa hau exekutatzeko",
|
||||||
|
"server_already_running": "Zerbitzari bat martxan dago dagoeneko ataka horretan",
|
||||||
|
"unable_authenticate": "Ezin da autentifikatu",
|
||||||
|
"values_mismatch": "Balioak ez datoz bat",
|
||||||
|
"warning": "Adi:",
|
||||||
|
"cannot_open_file": "Ezinezkoa izan da {file} fitxategia irekitzea (zergatia: {error})",
|
||||||
|
"corrupted_json": "{ressource}-eko/go JSONa kaltetuta dago (zergatia: {error})",
|
||||||
|
"corrupted_yaml": "{ressource}-eko/go YAMLa kaltetuta dago (zergatia: {error})",
|
||||||
|
"websocket_request_expected": "WebSocket eskaera bat espero zen",
|
||||||
|
"unknown_error_reading_file": "Errore ezezaguna {file} fitxategia irakurtzen saiatzerakoan (zergatia: {error})",
|
||||||
|
"download_unknown_error": "Errorea {url}(e)tik deskargatzerakoan: {error}",
|
||||||
|
"warn_the_user_that_lock_is_acquired": "Aurreko komandoa amaitu berri, orain komando hau abiarazten",
|
||||||
|
"error_writing_file": "Errorea {file} fitxategia idazterakoan: {error}",
|
||||||
|
"error_removing": "Errorea {path} ezabatzerakoan: {error}",
|
||||||
|
"download_bad_status_code": "{url} helbideak {code} egoera kodea agertu du",
|
||||||
|
"invalid_url": "{url}-(e)ra konektatzeak huts egin du... agian zerbitzua ez dago martxan, edo ez zaude IPv4/IPv6 bidez ondo konektatuta internetera.",
|
||||||
|
"download_timeout": "{url}(e)k denbora gehiegi behar izan du erantzuteko, bertan behera utzi du zerbitzariak."
|
||||||
}
|
}
|
|
@ -10,18 +10,17 @@
|
||||||
"deprecated_command_alias": "'{prog} {old}' منسوخ شده است و در آینده حذف خواهد شد ، بجای آن از '{prog} {new}' استفاده کنید",
|
"deprecated_command_alias": "'{prog} {old}' منسوخ شده است و در آینده حذف خواهد شد ، بجای آن از '{prog} {new}' استفاده کنید",
|
||||||
"deprecated_command": "'{prog} {command}' منسوخ شده است و در آینده حذف خواهد شد",
|
"deprecated_command": "'{prog} {command}' منسوخ شده است و در آینده حذف خواهد شد",
|
||||||
"confirm": "تایید کردن {prompt}",
|
"confirm": "تایید کردن {prompt}",
|
||||||
"colon": "{}: ",
|
|
||||||
"authentication_required": "احراز هویّت الزامی است",
|
"authentication_required": "احراز هویّت الزامی است",
|
||||||
"argument_required": "استدلال '{argument}' ضروری است",
|
"argument_required": "استدلال '{argument}' ضروری است",
|
||||||
"password": "کلمه عبور",
|
"password": "کلمه عبور",
|
||||||
"warn_the_user_that_lock_is_acquired": "فرمان دیگر به تازگی تکمیل شده است ، اکنون این دستور را شروع کنید",
|
"warn_the_user_that_lock_is_acquired": "فرمان دیگر به تازگی تکمیل شده است ، اکنون این دستور را شروع کنید",
|
||||||
"warn_the_user_about_waiting_lock_again": "هنوز در انتظار...",
|
"warn_the_user_about_waiting_lock_again": "هنوز در انتظار…",
|
||||||
"warn_the_user_about_waiting_lock": "یکی دیگر از دستورات YunoHost در حال اجرا است ، ما منتظر هستیم تا قبل از اجرای این دستور به پایان برسد",
|
"warn_the_user_about_waiting_lock": "یکی دیگر از دستورات YunoHost در حال اجرا است ، ما منتظر هستیم تا قبل از اجرای این دستور به پایان برسد",
|
||||||
"download_bad_status_code": "{url} کد وضعیّت بازگشتی {code}",
|
"download_bad_status_code": "{url} کد وضعیّت بازگشتی {code}",
|
||||||
"download_unknown_error": "خطا هنگام بارگیری داده ها از {url}: {error}",
|
"download_unknown_error": "خطا هنگام بارگیری داده ها از {url}: {error}",
|
||||||
"download_timeout": "پاسخ {url} خیلی طول کشید ، منصرف شو.",
|
"download_timeout": "پاسخ {url} خیلی طول کشید ، منصرف شو.",
|
||||||
"download_ssl_error": "خطای SSL هنگام اتصال به {url}",
|
"download_ssl_error": "خطای SSL هنگام اتصال به {url}",
|
||||||
"invalid_url": "اتصال به {url} انجام نشد ... شاید سرویس خاموش باشد یا در IPv4/IPv6 به درستی به اینترنت متصل نشده باشید.",
|
"invalid_url": "اتصال به {url} انجام نشد … شاید سرویس خاموش باشد یا در IPv4/IPv6 به درستی به اینترنت متصل نشده باشید.",
|
||||||
"error_changing_file_permissions": "خطا هنگام تغییر مجوزهای {path}: {error}",
|
"error_changing_file_permissions": "خطا هنگام تغییر مجوزهای {path}: {error}",
|
||||||
"error_removing": "خطا هنگام حذف {path}: {error}",
|
"error_removing": "خطا هنگام حذف {path}: {error}",
|
||||||
"error_writing_file": "خطا هنگام نوشتن فایل {file}: {error}",
|
"error_writing_file": "خطا هنگام نوشتن فایل {file}: {error}",
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
{}
|
{
|
||||||
|
"password": "Salasana",
|
||||||
|
"logged_out": "Kirjauduttu ulos"
|
||||||
|
}
|
|
@ -1,15 +1,14 @@
|
||||||
{
|
{
|
||||||
"argument_required": "L'argument '{argument}' est requis",
|
"argument_required": "L'argument '{argument}' est requis",
|
||||||
"authentication_required": "Authentification requise",
|
"authentication_required": "Authentification requise",
|
||||||
"colon": "{} : ",
|
|
||||||
"confirm": "Confirmez {prompt}",
|
"confirm": "Confirmez {prompt}",
|
||||||
"deprecated_command": "'{prog} {command}' est déprécié et sera bientôt supprimé",
|
"deprecated_command": "'{prog} {command}' est déprécié et sera bientôt supprimé",
|
||||||
"deprecated_command_alias": "'{prog} {old}' est déprécié et sera bientôt supprimé, utilisez '{prog} {new}' à la place",
|
"deprecated_command_alias": "'{prog} {old}' est déprécié et sera bientôt supprimé, utilisez '{prog} {new}' à la place",
|
||||||
"error": "Erreur :",
|
"error": "Erreur :",
|
||||||
"file_not_exist": "Le fichier '{path}' n'existe pas",
|
"file_not_exist": "Le fichier '{path}' n'existe pas",
|
||||||
"folder_exists": "Le dossier existe déjà : '{path}'",
|
"folder_exists": "Le dossier existe déjà : '{path}'",
|
||||||
"instance_already_running": "Une instance est déjà en cours d'exécution, merci d'attendre sa fin avant d'en lancer une autre.",
|
"instance_already_running": "Une instance est déjà en cours d'exécution, merci d'attendre sa fin avant d'en lancer une autre.",
|
||||||
"invalid_argument": "Argument '{argument}' incorrect : {error}",
|
"invalid_argument": "Argument '{argument}' incorrect : {error}",
|
||||||
"invalid_usage": "Utilisation erronée, utilisez --help pour accéder à l'aide",
|
"invalid_usage": "Utilisation erronée, utilisez --help pour accéder à l'aide",
|
||||||
"logged_in": "Connecté",
|
"logged_in": "Connecté",
|
||||||
"logged_out": "Déconnecté",
|
"logged_out": "Déconnecté",
|
||||||
|
@ -17,32 +16,32 @@
|
||||||
"operation_interrupted": "Opération interrompue",
|
"operation_interrupted": "Opération interrompue",
|
||||||
"password": "Mot de passe",
|
"password": "Mot de passe",
|
||||||
"pattern_not_match": "Ne correspond pas au motif",
|
"pattern_not_match": "Ne correspond pas au motif",
|
||||||
"root_required": "Vous devez être super-utilisateur pour exécuter cette action",
|
"root_required": "Vous devez avoir les droits d'administration pour exécuter cette action",
|
||||||
"server_already_running": "Un serveur est déjà en cours d'exécution sur ce port",
|
"server_already_running": "Un serveur est déjà en cours d'exécution sur ce port",
|
||||||
"success": "Succès !",
|
"success": "Succès !",
|
||||||
"unable_authenticate": "Impossible de vous authentifier",
|
"unable_authenticate": "Impossible de vous authentifier",
|
||||||
"unknown_group": "Le groupe '{group}' est inconnu",
|
"unknown_group": "Le groupe '{group}' est inconnu",
|
||||||
"unknown_user": "L'utilisateur '{user}' est inconnu",
|
"unknown_user": "Le compte '{user}' est inconnu",
|
||||||
"values_mismatch": "Les valeurs ne correspondent pas",
|
"values_mismatch": "Les valeurs ne correspondent pas",
|
||||||
"warning": "Attention :",
|
"warning": "Attention :",
|
||||||
"websocket_request_expected": "Une requête WebSocket est attendue",
|
"websocket_request_expected": "Une requête WebSocket est attendue",
|
||||||
"cannot_open_file": "Impossible d'ouvrir le fichier {file} (raison : {error})",
|
"cannot_open_file": "Impossible d'ouvrir le fichier {file} (raison : {error})",
|
||||||
"cannot_write_file": "Ne peut pas écrire le fichier {file} (raison : {error})",
|
"cannot_write_file": "Ne peut pas écrire le fichier {file} (raison : {error})",
|
||||||
"unknown_error_reading_file": "Erreur inconnue en essayant de lire le fichier {file} (raison :{error})",
|
"unknown_error_reading_file": "Erreur inconnue en essayant de lire le fichier {file} (raison :{error})",
|
||||||
"corrupted_json": "Fichier JSON corrompu en lecture depuis {ressource} (raison : {error})",
|
"corrupted_json": "Fichier JSON corrompu en lecture depuis {ressource} (raison : {error})",
|
||||||
"error_writing_file": "Erreur en écrivant le fichier {file} : {error}",
|
"error_writing_file": "Erreur en écrivant le fichier {file} : {error}",
|
||||||
"error_removing": "Erreur lors de la suppression {path} : {error}",
|
"error_removing": "Erreur lors de la suppression {path} : {error}",
|
||||||
"error_changing_file_permissions": "Erreur lors de la modification des autorisations pour {path} : {error}",
|
"error_changing_file_permissions": "Erreur lors de la modification des autorisations pour {path} : {error}",
|
||||||
"invalid_url": "Impossible de se connecter à {url}... peut-être que le service est hors service/indisponible/interrompu, ou que vous n'êtes pas correctement connecté à Internet en IPv4/IPv6.",
|
"invalid_url": "Impossible de se connecter à {url}... peut-être que le service est en panne ou que vous n'êtes pas correctement connecté à Internet en IPv4/IPv6.",
|
||||||
"download_ssl_error": "Erreur SSL lors de la connexion à {url}",
|
"download_ssl_error": "Erreur SSL lors de la connexion à {url}",
|
||||||
"download_timeout": "{url} a pris trop de temps pour répondre : abandon.",
|
"download_timeout": "{url} a pris trop de temps pour répondre : abandon.",
|
||||||
"download_unknown_error": "Erreur lors du téléchargement des données à partir de {url} : {error}",
|
"download_unknown_error": "Erreur lors du téléchargement des données à partir de {url} : {error}",
|
||||||
"download_bad_status_code": "{url} renvoie le code d'état {code}",
|
"download_bad_status_code": "{url} renvoie le code d'état {code}",
|
||||||
"corrupted_yaml": "Fichier YAML corrompu en lecture depuis {ressource} (raison : {error})",
|
"corrupted_yaml": "Fichier YAML corrompu en lecture depuis {ressource} (raison : {error})",
|
||||||
"info": "Info :",
|
"info": "Info :",
|
||||||
"corrupted_toml": "Fichier TOML corrompu en lecture depuis {ressource} (raison : {error})",
|
"corrupted_toml": "Fichier TOML corrompu en lecture depuis {ressource} (raison : {error})",
|
||||||
"warn_the_user_about_waiting_lock": "Une autre commande YunoHost est actuellement en cours, nous attendons qu'elle se termine avant de démarrer celle là",
|
"warn_the_user_about_waiting_lock": "Une autre commande YunoHost est actuellement en cours, nous attendons qu'elle se termine avant de démarrer celle là",
|
||||||
"warn_the_user_about_waiting_lock_again": "Toujours en attente...",
|
"warn_the_user_about_waiting_lock_again": "Toujours en attente...",
|
||||||
"warn_the_user_that_lock_is_acquired": "La commande précédente vient de se terminer, lancement de cette nouvelle commande",
|
"warn_the_user_that_lock_is_acquired": "La commande précédente vient de se terminer, lancement de cette nouvelle commande",
|
||||||
"edit_text_question": "{}. Modifier ce texte ? [yN] : "
|
"edit_text_question": "{}. Modifier ce texte ? [yN] : "
|
||||||
}
|
}
|
|
@ -9,7 +9,6 @@
|
||||||
"deprecated_command_alias": "'{prog} {old}' xa non se utiliza e será eliminado no futuro, usa '{prog} {new}' no seu lugar",
|
"deprecated_command_alias": "'{prog} {old}' xa non se utiliza e será eliminado no futuro, usa '{prog} {new}' no seu lugar",
|
||||||
"deprecated_command": "'{prog} {command}' xa non se utiliza e xa non se usará no futuro",
|
"deprecated_command": "'{prog} {command}' xa non se utiliza e xa non se usará no futuro",
|
||||||
"confirm": "Confirma {prompt}",
|
"confirm": "Confirma {prompt}",
|
||||||
"colon": "{}: ",
|
|
||||||
"authentication_required": "Autenticación requerida",
|
"authentication_required": "Autenticación requerida",
|
||||||
"argument_required": "O argumento '{argument}' é requerido",
|
"argument_required": "O argumento '{argument}' é requerido",
|
||||||
"logged_out": "Sesión pechada",
|
"logged_out": "Sesión pechada",
|
||||||
|
@ -24,16 +23,16 @@
|
||||||
"root_required": "Tes que ser root para facer esta acción",
|
"root_required": "Tes que ser root para facer esta acción",
|
||||||
"pattern_not_match": "Non concorda co patrón",
|
"pattern_not_match": "Non concorda co patrón",
|
||||||
"operation_interrupted": "Interrumpeuse a operación",
|
"operation_interrupted": "Interrumpeuse a operación",
|
||||||
"not_logged_in": "Non estás conectada",
|
"not_logged_in": "Non iniciaches sesión",
|
||||||
"logged_in": "Conectada",
|
"logged_in": "Sesión iniciada",
|
||||||
"warn_the_user_that_lock_is_acquired": "O outro comando rematou, agora executarase este",
|
"warn_the_user_that_lock_is_acquired": "O outro comando rematou, agora executarase este",
|
||||||
"warn_the_user_about_waiting_lock_again": "Agardando...",
|
"warn_the_user_about_waiting_lock_again": "Agardando…",
|
||||||
"warn_the_user_about_waiting_lock": "Estase executando outro comando de YunoHost neste intre, estamos agardando a que remate para executar este",
|
"warn_the_user_about_waiting_lock": "Estase executando outro comando de YunoHost neste intre, estamos agardando a que remate para executar este",
|
||||||
"download_bad_status_code": "{url} devolveu o código de estado {code}",
|
"download_bad_status_code": "{url} devolveu o código de estado {code}",
|
||||||
"download_unknown_error": "Erro ao descargar os datos desde {url}: {error}",
|
"download_unknown_error": "Erro ao descargar os datos desde {url}: {error}",
|
||||||
"download_timeout": "{url} está tardando en responder, deixámolo.",
|
"download_timeout": "{url} está tardando en responder, deixámolo.",
|
||||||
"download_ssl_error": "Erro SSL ao conectar con {url}",
|
"download_ssl_error": "Erro SSL ao conectar con {url}",
|
||||||
"invalid_url": "Fallou a conexión con {url} ... pode que o servizo esté caído, ou que non teñas conexión a Internet con IPv4/IPv6.",
|
"invalid_url": "Fallou a conexión con {url}... pode que o servizo estea caído, ou que non teñas conexión a Internet con IPv4/IPv6.",
|
||||||
"error_changing_file_permissions": "Erro ao cambiar os permisos de {path}: {error}",
|
"error_changing_file_permissions": "Erro ao cambiar os permisos de {path}: {error}",
|
||||||
"error_removing": "Erro ao eliminar {path}: {error}",
|
"error_removing": "Erro ao eliminar {path}: {error}",
|
||||||
"error_writing_file": "Erro ao escribir o ficheiro {file}: {error}",
|
"error_writing_file": "Erro ao escribir o ficheiro {file}: {error}",
|
||||||
|
|
1
locales/he.json
Normal file
1
locales/he.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
"argument_required": "तर्क '{argument}' आवश्यक है",
|
"argument_required": "तर्क '{argument}' आवश्यक है",
|
||||||
"authentication_required": "प्रमाणीकरण आवश्यक",
|
"authentication_required": "प्रमाणीकरण आवश्यक",
|
||||||
"colon": "{}: ",
|
|
||||||
"confirm": "पुष्टि करें {prompt}",
|
"confirm": "पुष्टि करें {prompt}",
|
||||||
"deprecated_command": "'{prog}' '{command}' का प्रयोग न करे, भविष्य में इसे हटा दिया जाएगा",
|
"deprecated_command": "'{prog}' '{command}' का प्रयोग न करे, भविष्य में इसे हटा दिया जाएगा",
|
||||||
"deprecated_command_alias": "'{prog} {old}' अब पुराना हो गया है और इसे भविष्य में हटा दिया जाएगा, इस की जगह '{prog} {new}' का प्रयोग करें",
|
"deprecated_command_alias": "'{prog} {old}' अब पुराना हो गया है और इसे भविष्य में हटा दिया जाएगा, इस की जगह '{prog} {new}' का प्रयोग करें",
|
||||||
|
|
|
@ -1 +1,47 @@
|
||||||
{}
|
{
|
||||||
|
"argument_required": "Argumen '{argument}' dibutuhkan",
|
||||||
|
"authentication_required": "Autentikasi dibutuhkan",
|
||||||
|
"deprecated_command": "'{prog} {command}' sudah usang dan akan dihapus nanti",
|
||||||
|
"logged_out": "Berhasil keluar",
|
||||||
|
"password": "Kata sandi",
|
||||||
|
"deprecated_command_alias": "'{prog} {old}' sudah usang dan akan dihapus nanti, gunakan '{prog} {new}'",
|
||||||
|
"info": "Informasi:",
|
||||||
|
"instance_already_running": "Sudah ada tindakan YunoHost yang sedang berjalan. Tunggu itu selesai sebelum menjalankan yang lain.",
|
||||||
|
"logged_in": "Berhasil masuk",
|
||||||
|
"warning": "Peringatan:",
|
||||||
|
"cannot_open_file": "Tidak dapat membuka berkas {file} (alasan: {error})",
|
||||||
|
"error_removing": "Terjadi galat ketika menghapus {path}: {error}",
|
||||||
|
"success": "Berhasil!",
|
||||||
|
"warn_the_user_about_waiting_lock": "Perintah YunoHost lain sedang berjalan saat ini, kami sedang menunggu itu selesai sebelum menjalankan yang ini",
|
||||||
|
"warn_the_user_about_waiting_lock_again": "Masih menunggu...",
|
||||||
|
"unable_authenticate": "Tidak dapat mengautentikasi",
|
||||||
|
"warn_the_user_that_lock_is_acquired": "Perintah yang tadi baru saja selesai, akan memulai perintah ini",
|
||||||
|
"server_already_running": "Sebuah peladen telah berjalan di porta tersebut",
|
||||||
|
"unknown_group": "Kelompok '{group}' tidak diketahui",
|
||||||
|
"unknown_user": "Pengguna '{user}' tidak diketahui",
|
||||||
|
"values_mismatch": "Nnilai berbeda",
|
||||||
|
"cannot_write_file": "Tidak dapat menyimpan berkas {file} (alasan: {error})",
|
||||||
|
"unknown_error_reading_file": "Galat yang tidak diketahui ketika membaca berkas {file} (alasan: {error})",
|
||||||
|
"invalid_url": "Gagal terhubung dengan {url}... mungkin layanan tersebut sedang turun, atau Anda tidak terhubung dengan benar ke internet di IPv4/IPv6.",
|
||||||
|
"download_timeout": "{url} memakan waktu yang lama untuk menjawab, menyerah.",
|
||||||
|
"download_unknown_error": "Galat ketika mengunduh data dari {url}: {error}",
|
||||||
|
"download_bad_status_code": "{url} menjawab dengan kode status {code}",
|
||||||
|
"confirm": "Konfirmasi {prompt}",
|
||||||
|
"edit_text_question": "{}. Sunting teks ini ? [yN]: ",
|
||||||
|
"error": "Galat:",
|
||||||
|
"file_not_exist": "Berkas tidak ada: '{path}'",
|
||||||
|
"folder_exists": "Berkas sudah ada: '{path}'",
|
||||||
|
"invalid_argument": "Argumen tidak valid '{argument}': {error}",
|
||||||
|
"invalid_usage": "Tidak valid, ikutkan --help untuk melihat bantuan",
|
||||||
|
"not_logged_in": "Anda belum masuk",
|
||||||
|
"operation_interrupted": "Tindakan terganggu",
|
||||||
|
"error_writing_file": "Galat ketika menyimpan berkas {file}: {error}",
|
||||||
|
"error_changing_file_permissions": "Galat ketika mengubah izin untuk {path}: {error}",
|
||||||
|
"download_ssl_error": "Galat SSL ketika menghubungi {url}",
|
||||||
|
"pattern_not_match": "Tidak cocok dengan pola",
|
||||||
|
"root_required": "Anda harus berada di root untuk melakukan tindakan ini",
|
||||||
|
"corrupted_yaml": "Pembacaan rusak untuk YAML {ressource} (alasan: {error})",
|
||||||
|
"corrupted_toml": "Pembacaan rusak untuk TOML {ressource} (alasan: {error})",
|
||||||
|
"corrupted_json": "Pembacaan rusak untuk JSON {ressource} (alasan: {error})",
|
||||||
|
"websocket_request_expected": "Mengharapkan permintaan WebSocket"
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"argument_required": "L'argomento '{argument}' è richiesto",
|
"argument_required": "L'argomento '{argument}' è richiesto",
|
||||||
"authentication_required": "Autenticazione richiesta",
|
"authentication_required": "Autenticazione richiesta",
|
||||||
"colon": "{}: ",
|
|
||||||
"confirm": "Confermare {prompt}",
|
"confirm": "Confermare {prompt}",
|
||||||
"deprecated_command": "'{prog} {command}' è deprecato e sarà rimosso in futuro",
|
"deprecated_command": "'{prog} {command}' è deprecato e sarà rimosso in futuro",
|
||||||
"deprecated_command_alias": "'{prog} {old}' è deprecato e sarà rimosso in futuro, usa invece '{prog} {new}'",
|
"deprecated_command_alias": "'{prog} {old}' è deprecato e sarà rimosso in futuro, usa invece '{prog} {new}'",
|
||||||
|
@ -34,14 +33,15 @@
|
||||||
"error_writing_file": "Errore durante la scrittura del file {file}: {error}",
|
"error_writing_file": "Errore durante la scrittura del file {file}: {error}",
|
||||||
"error_removing": "Errore durante la rimozione {path}: {error}",
|
"error_removing": "Errore durante la rimozione {path}: {error}",
|
||||||
"error_changing_file_permissions": "Errore durante il cambio di permessi per {path}: {error}",
|
"error_changing_file_permissions": "Errore durante il cambio di permessi per {path}: {error}",
|
||||||
"invalid_url": "URL non valido {url} (il sito esiste?)",
|
"invalid_url": "Fallita connessione a {url}… magari il servizio è down, o non sei connesso correttamente ad internet con IPv4/IPv6.",
|
||||||
"download_ssl_error": "Errore SSL durante la connessione a {url}",
|
"download_ssl_error": "Errore SSL durante la connessione a {url}",
|
||||||
"download_timeout": "{url} ci ha messo troppo a rispondere, abbandonato.",
|
"download_timeout": "{url} ci ha messo troppo a rispondere, abbandonato.",
|
||||||
"download_unknown_error": "Errore durante il download di dati da {url} : {error}",
|
"download_unknown_error": "Errore durante il download di dati da {url} : {error}",
|
||||||
"download_bad_status_code": "{url} ha restituito il codice di stato {code}",
|
"download_bad_status_code": "{url} ha restituito il codice di stato {code}",
|
||||||
"info": "Info:",
|
"info": "Info:",
|
||||||
"warn_the_user_that_lock_is_acquired": "L'altro comando è appena completato, ora avvio questo comando",
|
"warn_the_user_that_lock_is_acquired": "L'altro comando è appena completato, ora avvio questo comando",
|
||||||
"warn_the_user_about_waiting_lock_again": "Sto ancora aspettando ...",
|
"warn_the_user_about_waiting_lock_again": "Sto ancora aspettando…",
|
||||||
"warn_the_user_about_waiting_lock": "Un altro comando YunoHost è in esecuzione in questo momento, stiamo aspettando che finisca prima di eseguire questo",
|
"warn_the_user_about_waiting_lock": "Un altro comando YunoHost è in esecuzione in questo momento, stiamo aspettando che finisca prima di eseguire questo",
|
||||||
"corrupted_toml": "TOML corrotto da {ressource} (motivo: {error})"
|
"corrupted_toml": "TOML corrotto da {ressource} (motivo: {error})",
|
||||||
|
"edit_text_question": "{}. Modificare il testo? [yN]: "
|
||||||
}
|
}
|
47
locales/ja.json
Normal file
47
locales/ja.json
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"logged_out": "ログアウトしました",
|
||||||
|
"password": "パスワード",
|
||||||
|
"argument_required": "引数 '{argument}' が必要です",
|
||||||
|
"authentication_required": "認証が必要",
|
||||||
|
"confirm": "{prompt}の確認",
|
||||||
|
"deprecated_command": "'{prog} {command}' は非推奨であり、将来削除される予定です",
|
||||||
|
"deprecated_command_alias": "'{prog} {old}' は非推奨であり、今後削除される予定です。代わりに '{prog} {new}' を使用してください",
|
||||||
|
"edit_text_question": "{}.このテキストを編集しますか?[yN]: ",
|
||||||
|
"error": "エラー:",
|
||||||
|
"info": "インフォメーション:",
|
||||||
|
"download_unknown_error": "{url}からデータをダウンロードする際のエラー:{error}",
|
||||||
|
"download_bad_status_code": "{url}は状態コード {code} を返しました",
|
||||||
|
"warn_the_user_about_waiting_lock": "別のYunoHostコマンドが現在実行されているため、完了するのを待っています",
|
||||||
|
"warn_the_user_about_waiting_lock_again": "まだ待っています…",
|
||||||
|
"warn_the_user_that_lock_is_acquired": "他のコマンドが完了しました。このコマンドが開始されました",
|
||||||
|
"file_not_exist": "ファイルが存在しません: '{path}'",
|
||||||
|
"folder_exists": "フォルダは既に存在します: '{path}'",
|
||||||
|
"instance_already_running": "YunoHost 操作が既に実行されています。他の操作が完了するのを待ってください。",
|
||||||
|
"invalid_argument": "無効な引数 '{argument}': {error}",
|
||||||
|
"invalid_usage": "無効な使用法です。--help を渡してヘルプを表示します",
|
||||||
|
"logged_in": "ログイン済み",
|
||||||
|
"not_logged_in": "ログインしていません",
|
||||||
|
"operation_interrupted": "操作が中断されました",
|
||||||
|
"pattern_not_match": "パターンと一致しない",
|
||||||
|
"root_required": "このアクションを実行するには、rootである必要があります",
|
||||||
|
"server_already_running": "サーバーはそのポートで既に実行されています",
|
||||||
|
"success": "成功!",
|
||||||
|
"unable_authenticate": "認証できません",
|
||||||
|
"unknown_group": "不明な '{group}' グループ",
|
||||||
|
"unknown_user": "不明な '{user}' ユーザー",
|
||||||
|
"values_mismatch": "値が一致しない",
|
||||||
|
"warning": "警告:",
|
||||||
|
"websocket_request_expected": "WebSocket 要求が必要です",
|
||||||
|
"cannot_open_file": "ファイル{file}を開けませんでした(理由:{error})",
|
||||||
|
"cannot_write_file": "ファイル {file}を書き込めませんでした (理由: {error})",
|
||||||
|
"unknown_error_reading_file": "ファイル{file}を読み取ろうとしているときに不明なエラーが発生しました(理由:{error})",
|
||||||
|
"corrupted_json": "{ressource}から読み取られたJSONは破損していました(理由:{error})",
|
||||||
|
"corrupted_yaml": "破損した YAML が{ressource}から読み取られました (理由: {error})",
|
||||||
|
"corrupted_toml": "破損した TOML が{ressource}から読み取られました (理由: {error})",
|
||||||
|
"error_writing_file": "ファイル{file}書き込み時のエラー:{error}",
|
||||||
|
"error_removing": "{path}を削除するときのエラー:{error}",
|
||||||
|
"error_changing_file_permissions": "{path}のアクセス許可変更時のエラー: {error}",
|
||||||
|
"invalid_url": "{url}に接続できませんでした…サービスがダウンしているか、IPv4 / IPv6でインターネットに正しく接続されていない可能性があります。",
|
||||||
|
"download_ssl_error": "{url}への接続時のSSLエラー",
|
||||||
|
"download_timeout": "{url}は応答に時間がかかりすぎたため、あきらめました。"
|
||||||
|
}
|
8
locales/kab.json
Normal file
8
locales/kab.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"error": "Erreur :",
|
||||||
|
"success": "Akka d rrbeḥ !",
|
||||||
|
"password": "Awal n uɛeddi",
|
||||||
|
"logged_in": "Taqqneḍ",
|
||||||
|
"logged_out": "Yeffeɣ",
|
||||||
|
"authentication_required": "Tlaq tuqqna"
|
||||||
|
}
|
1
locales/ko.json
Normal file
1
locales/ko.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
1
locales/lt.json
Normal file
1
locales/lt.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -7,14 +7,13 @@
|
||||||
"unknown_user": "Ukjent '{user}' bruker",
|
"unknown_user": "Ukjent '{user}' bruker",
|
||||||
"unknown_group": "Ukjent '{group}' gruppe",
|
"unknown_group": "Ukjent '{group}' gruppe",
|
||||||
"unable_authenticate": "Kunne ikke identitetsbekrefte",
|
"unable_authenticate": "Kunne ikke identitetsbekrefte",
|
||||||
"success": "Vellykket.",
|
"success": "Vellykket!",
|
||||||
"operation_interrupted": "Operasjon forstyrret",
|
"operation_interrupted": "Operasjon forstyrret",
|
||||||
"not_logged_in": "Du er ikke innlogget",
|
"not_logged_in": "Du er ikke innlogget",
|
||||||
"logged_in": "Innlogget",
|
"logged_in": "Innlogget",
|
||||||
"info": "Info:",
|
"info": "Info:",
|
||||||
"error": "Feil:",
|
"error": "Feil:",
|
||||||
"confirm": "Bekreft {prompt}",
|
"confirm": "Bekreft {prompt}",
|
||||||
"colon": "{}: ",
|
|
||||||
"logged_out": "Utlogget",
|
"logged_out": "Utlogget",
|
||||||
"password": "Passord"
|
"password": "Passord"
|
||||||
}
|
}
|
|
@ -4,7 +4,6 @@
|
||||||
"deprecated_command_alias": "'{prog} {old}' अस्वीकृत गरिएको छ र भविष्यमा हटाइनेछ, यसको सट्टा '{prog} {new}' प्रयोग गर्नुहोस्।",
|
"deprecated_command_alias": "'{prog} {old}' अस्वीकृत गरिएको छ र भविष्यमा हटाइनेछ, यसको सट्टा '{prog} {new}' प्रयोग गर्नुहोस्।",
|
||||||
"deprecated_command": "'{prog} {command}' अस्वीकृत गरिएको छ र भविष्यमा हटाइनेछ",
|
"deprecated_command": "'{prog} {command}' अस्वीकृत गरिएको छ र भविष्यमा हटाइनेछ",
|
||||||
"confirm": "कन्फर्म {prompt}",
|
"confirm": "कन्फर्म {prompt}",
|
||||||
"colon": "{}: ",
|
|
||||||
"authentication_required": "प्रमाणीकरण आवाश्यक छ",
|
"authentication_required": "प्रमाणीकरण आवाश्यक छ",
|
||||||
"argument_required": "तर्क '{argument}' आवश्यक छ"
|
"argument_required": "तर्क '{argument}' आवश्यक छ"
|
||||||
}
|
}
|
|
@ -1,12 +1,11 @@
|
||||||
{
|
{
|
||||||
"argument_required": "Argument {argument} is vereist",
|
"argument_required": "Argument {argument} is vereist",
|
||||||
"authentication_required": "Aanmelding vereist",
|
"authentication_required": "Aanmelding vereist",
|
||||||
"colon": "{}: ",
|
|
||||||
"confirm": "Bevestig {prompt}",
|
"confirm": "Bevestig {prompt}",
|
||||||
"error": "Fout:",
|
"error": "Fout:",
|
||||||
"file_not_exist": "Bestand bestaat niet: '{path}'",
|
"file_not_exist": "Bestand bestaat niet: '{path}'",
|
||||||
"folder_exists": "Deze map bestaat al: '{path}'",
|
"folder_exists": "Deze map bestaat al: '{path}'",
|
||||||
"instance_already_running": "Er is al een instantie actief, bedankt om te wachten tot deze afgesloten is alvorens een andere te starten.",
|
"instance_already_running": "Er is al een bewerking bezig. Even geduld tot deze afgesloten is, alvorens een andere te starten.",
|
||||||
"invalid_argument": "Ongeldig argument '{argument}': {error}",
|
"invalid_argument": "Ongeldig argument '{argument}': {error}",
|
||||||
"invalid_usage": "Ongeldig gebruik, doe --help om de hulptekst te lezen",
|
"invalid_usage": "Ongeldig gebruik, doe --help om de hulptekst te lezen",
|
||||||
"logged_in": "Ingelogd",
|
"logged_in": "Ingelogd",
|
||||||
|
@ -29,19 +28,20 @@
|
||||||
"cannot_open_file": "Niet mogelijk om bestand {file} te openen (reden: {error})",
|
"cannot_open_file": "Niet mogelijk om bestand {file} te openen (reden: {error})",
|
||||||
"cannot_write_file": "Niet gelukt om bestand {file} te schrijven (reden: {error})",
|
"cannot_write_file": "Niet gelukt om bestand {file} te schrijven (reden: {error})",
|
||||||
"unknown_error_reading_file": "Ongekende fout tijdens het lezen van bestand {file} (cause:{error})",
|
"unknown_error_reading_file": "Ongekende fout tijdens het lezen van bestand {file} (cause:{error})",
|
||||||
"corrupted_json": "Corrupte json gelezen van {ressource} (reden: {error})",
|
"corrupted_json": "Corrupte JSON gelezen van {ressource} (reden: {error})",
|
||||||
"error_writing_file": "Fout tijdens het schrijven van bestand {file}: {error}",
|
"error_writing_file": "Fout tijdens het schrijven van bestand {file}: {error}",
|
||||||
"error_removing": "Fout tijdens het verwijderen van {path}: {error}",
|
"error_removing": "Fout tijdens het verwijderen van {path}: {error}",
|
||||||
"error_changing_file_permissions": "Fout tijdens het veranderen van machtiging voor {path}: {error}",
|
"error_changing_file_permissions": "Fout tijdens het veranderen van machtiging voor {path}: {error}",
|
||||||
"invalid_url": "Ongeldige URL {url} (bestaat deze website?)",
|
"invalid_url": "Kon niet verbinden met {url}… misschien is de dienst uit de lucht, of ben je niet goed verbonden via IPv4 of IPv6.",
|
||||||
"download_ssl_error": "SSL fout gedurende verbinding met {url}",
|
"download_ssl_error": "SSL fout gedurende verbinding met {url}",
|
||||||
"download_timeout": "{url} neemt te veel tijd om te antwoorden, we geven het op.",
|
"download_timeout": "{url} neemt te veel tijd om te antwoorden, we geven het op.",
|
||||||
"download_unknown_error": "Fout tijdens het downloaden van data van {url}: {error}",
|
"download_unknown_error": "Fout tijdens het downloaden van data van {url}: {error}",
|
||||||
"download_bad_status_code": "{url} stuurt status code {code}",
|
"download_bad_status_code": "{url} stuurt status code {code}",
|
||||||
"warn_the_user_that_lock_is_acquired": "de andere opdracht is zojuist voltooid en start nu deze opdracht",
|
"warn_the_user_that_lock_is_acquired": "De andere opdracht is zojuist voltooid; nu wordt nu deze opdracht gestart",
|
||||||
"warn_the_user_about_waiting_lock_again": "Nog steeds aan het wachten...",
|
"warn_the_user_about_waiting_lock_again": "Nog steeds aan het wachten…",
|
||||||
"warn_the_user_about_waiting_lock": "Een ander YunoHost commando wordt uitgevoerd, we wachten tot het gedaan is alovrens dit te starten",
|
"warn_the_user_about_waiting_lock": "Een ander YunoHost commando wordt uitgevoerd, we wachten tot het gedaan is alovrens dit te starten",
|
||||||
"corrupted_toml": "Ongeldige TOML werd gelezen op {ressource} (reason: {error})",
|
"corrupted_toml": "Ongeldige TOML werd gelezen van {ressource} (reason: {error})",
|
||||||
"corrupted_yaml": "Ongeldig YAML bestand op {ressource} (reason: {error})",
|
"corrupted_yaml": "Ongeldig YAML bestand op {ressource} (reden: {error})",
|
||||||
"info": "Ter info:"
|
"info": "Ter info:",
|
||||||
|
"edit_text_question": "{}. Deze tekst bewerken ? [yN]: "
|
||||||
}
|
}
|
|
@ -4,7 +4,6 @@
|
||||||
"logged_in": "Connectat",
|
"logged_in": "Connectat",
|
||||||
"logged_out": "Desconnectat",
|
"logged_out": "Desconnectat",
|
||||||
"password": "Senhal",
|
"password": "Senhal",
|
||||||
"colon": "{}: ",
|
|
||||||
"confirm": "Confirmatz : {prompt}",
|
"confirm": "Confirmatz : {prompt}",
|
||||||
"deprecated_command": "« {prog} {command} » es despreciat e serà lèu suprimit",
|
"deprecated_command": "« {prog} {command} » es despreciat e serà lèu suprimit",
|
||||||
"deprecated_command_alias": "« {prog} {old} » es despreciat e serà lèu suprimit, utilizatz « {prog} {new} » allòc",
|
"deprecated_command_alias": "« {prog} {old} » es despreciat e serà lèu suprimit, utilizatz « {prog} {new} » allòc",
|
||||||
|
@ -43,5 +42,6 @@
|
||||||
"corrupted_toml": "Fichièr TOML corromput en lectura de {ressource} estant (rason : {error})",
|
"corrupted_toml": "Fichièr TOML corromput en lectura de {ressource} estant (rason : {error})",
|
||||||
"warn_the_user_about_waiting_lock": "Una autra comanda YunoHost es en execucion, sèm a esperar qu’acabe abans d’aviar aquesta d’aquí",
|
"warn_the_user_about_waiting_lock": "Una autra comanda YunoHost es en execucion, sèm a esperar qu’acabe abans d’aviar aquesta d’aquí",
|
||||||
"warn_the_user_about_waiting_lock_again": "Encara en espèra…",
|
"warn_the_user_about_waiting_lock_again": "Encara en espèra…",
|
||||||
"warn_the_user_that_lock_is_acquired": "l’autra comanda ven d’acabar, ara lançament d’aquesta comanda"
|
"warn_the_user_that_lock_is_acquired": "l’autra comanda ven d’acabar, ara lançament d’aquesta comanda",
|
||||||
|
"edit_text_question": "{}. Modificar aqueste tèxte ? [yN]: "
|
||||||
}
|
}
|
|
@ -2,17 +2,17 @@
|
||||||
"logged_out": "Wylogowano",
|
"logged_out": "Wylogowano",
|
||||||
"password": "Hasło",
|
"password": "Hasło",
|
||||||
"warn_the_user_that_lock_is_acquired": "Inne polecenie właśnie się zakończyło, teraz uruchamiam to polecenie",
|
"warn_the_user_that_lock_is_acquired": "Inne polecenie właśnie się zakończyło, teraz uruchamiam to polecenie",
|
||||||
"warn_the_user_about_waiting_lock_again": "Wciąż czekam...",
|
"warn_the_user_about_waiting_lock_again": "Wciąż czekam…",
|
||||||
"warn_the_user_about_waiting_lock": "Kolejne polecenie YunoHost jest teraz uruchomione, czekamy na jego zakończenie przed uruchomieniem tego",
|
"warn_the_user_about_waiting_lock": "Kolejne polecenie YunoHost jest obecnie uruchomione, czekamy na jego zakończenie przed uruchomieniem tego",
|
||||||
"download_bad_status_code": "{url} zwrócił kod stanu {code}",
|
"download_bad_status_code": "{url} zwrócił kod stanu {code}",
|
||||||
"download_unknown_error": "Błąd podczas pobierania danych z {url}: {error}",
|
"download_unknown_error": "Błąd podczas pobierania danych z {url}: {error}",
|
||||||
"download_timeout": "{url} odpowiedział zbyt długo, poddał się.",
|
"download_timeout": "{url} potrzebował zbyt dużo czasu na odpowiedź, rezygnacja.",
|
||||||
"download_ssl_error": "Błąd SSL podczas łączenia z {url}",
|
"download_ssl_error": "Błąd SSL podczas łączenia z {url}",
|
||||||
"invalid_url": "Nieprawidłowy adres URL {url} (czy ta strona istnieje?)",
|
"invalid_url": "Nie udało się połączyć z {url}… być może strona nie jest dostępna, lub nie jesteś prawidłowo połączony z Internetem po IPv4/IPv6.",
|
||||||
"error_changing_file_permissions": "Błąd podczas zmiany uprawnień dla {path}: {error}",
|
"error_changing_file_permissions": "Błąd podczas zmiany uprawnień dla {path}: {error}",
|
||||||
"error_removing": "Błąd podczas usuwania {path}: {error}",
|
"error_removing": "Błąd podczas usuwania {path}: {error}",
|
||||||
"error_writing_file": "Błąd podczas zapisywania pliku {file}: {error}",
|
"error_writing_file": "Błąd podczas zapisywania pliku {file}: {error}",
|
||||||
"corrupted_toml": "Uszkodzony TOML z {ressource: s} (reason: {error})",
|
"corrupted_toml": "Uszkodzony TOML odczytany z {ressource} (reason: {error})",
|
||||||
"corrupted_yaml": "Uszkodzony YAML odczytany z {ressource} (reason: {error})",
|
"corrupted_yaml": "Uszkodzony YAML odczytany z {ressource} (reason: {error})",
|
||||||
"corrupted_json": "Uszkodzony JSON odczytany z {ressource} (reason: {error})",
|
"corrupted_json": "Uszkodzony JSON odczytany z {ressource} (reason: {error})",
|
||||||
"unknown_error_reading_file": "Nieznany błąd podczas próby odczytania pliku {file} (przyczyna: {error})",
|
"unknown_error_reading_file": "Nieznany błąd podczas próby odczytania pliku {file} (przyczyna: {error})",
|
||||||
|
@ -31,17 +31,17 @@
|
||||||
"operation_interrupted": "Operacja przerwana",
|
"operation_interrupted": "Operacja przerwana",
|
||||||
"not_logged_in": "Nie jesteś zalogowany",
|
"not_logged_in": "Nie jesteś zalogowany",
|
||||||
"logged_in": "Zalogowany",
|
"logged_in": "Zalogowany",
|
||||||
"invalid_usage": "Nieprawidłowe użycie. Przejdź --help, aby wyświetlić pomoc",
|
"invalid_usage": "Nieprawidłowe użycie, wprowadź --help, aby wyświetlić pomoc",
|
||||||
"invalid_argument": "Nieprawidłowy argument „{argument}”: {error}",
|
"invalid_argument": "Nieprawidłowy argument „{argument}”: {error}",
|
||||||
"instance_already_running": "Trwa już operacja YunoHost. Zaczekaj na zakończenie, zanim uruchomisz kolejny.",
|
"instance_already_running": "Trwa już operacja YunoHost. Zaczekaj na jej zakończenie, zanim uruchomisz kolejną.",
|
||||||
"info": "Informacje:",
|
"info": "Info:",
|
||||||
"folder_exists": "Folder już istnieje: „{path}”",
|
"folder_exists": "Folder już istnieje: „{path}”",
|
||||||
"file_not_exist": "Plik nie istnieje: „{path}”",
|
"file_not_exist": "Plik nie istnieje: „{path}”",
|
||||||
"error": "Błąd:",
|
"error": "Błąd:",
|
||||||
"deprecated_command_alias": "„{prog} {old}” jest przestarzałe i zostanie usunięte w przyszłości, zamiast tego użyj „{prog} {new}”",
|
"deprecated_command_alias": "„{prog} {old}” jest przestarzałe i zostanie usunięte w przyszłości, zamiast tego użyj „{prog} {new}”",
|
||||||
"deprecated_command": "„{prog} {command}” jest przestarzałe i zostanie usunięte w przyszłości",
|
"deprecated_command": "„{prog} {command}” jest przestarzałe i zostanie usunięte w przyszłości",
|
||||||
"confirm": "Potwierdź {prompt}",
|
"confirm": "Potwierdź {prompt}",
|
||||||
"colon": "{}: ",
|
|
||||||
"authentication_required": "Wymagane uwierzytelnienie",
|
"authentication_required": "Wymagane uwierzytelnienie",
|
||||||
"argument_required": "Argument „{argument}” jest wymagany"
|
"argument_required": "Argument „{argument}” jest wymagany",
|
||||||
|
"edit_text_question": "{}. Zedytować ten tekst? [tN]: "
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
"argument_required": "O argumento '{argument}' é obrigatório",
|
"argument_required": "O argumento '{argument}' é obrigatório",
|
||||||
"authentication_required": "Autenticação obrigatória",
|
"authentication_required": "Autenticação obrigatória",
|
||||||
"colon": "{}: ",
|
|
||||||
"confirm": "Confirmar {prompt}",
|
"confirm": "Confirmar {prompt}",
|
||||||
"error": "Erro:",
|
"error": "Erro:",
|
||||||
"file_not_exist": "O ficheiro não existe: '{path}'",
|
"file_not_exist": "O ficheiro não existe: '{path}'",
|
||||||
|
@ -40,8 +39,9 @@
|
||||||
"corrupted_json": "JSON corrompido lido do {ressource} (motivo: {error})",
|
"corrupted_json": "JSON corrompido lido do {ressource} (motivo: {error})",
|
||||||
"corrupted_yaml": "YAML corrompido lido do {ressource} (motivo: {error})",
|
"corrupted_yaml": "YAML corrompido lido do {ressource} (motivo: {error})",
|
||||||
"warn_the_user_that_lock_is_acquired": "O outro comando acabou de concluir, agora iniciando este comando",
|
"warn_the_user_that_lock_is_acquired": "O outro comando acabou de concluir, agora iniciando este comando",
|
||||||
"warn_the_user_about_waiting_lock_again": "Ainda esperando...",
|
"warn_the_user_about_waiting_lock_again": "Ainda esperando…",
|
||||||
"warn_the_user_about_waiting_lock": "Outro comando YunoHost está sendo executado agora, estamos aguardando o término antes de executar este",
|
"warn_the_user_about_waiting_lock": "Outro comando YunoHost está sendo executado agora, estamos aguardando o término antes de executar este",
|
||||||
"corrupted_toml": "TOML corrompido lido em {ressource} (motivo: {error})",
|
"corrupted_toml": "TOML corrompido lido em {ressource} (motivo: {error})",
|
||||||
"info": "Informações:"
|
"info": "Informações:",
|
||||||
|
"edit_text_question": "{}. Editar este texto ? [yN]: "
|
||||||
}
|
}
|
1
locales/pt_BR.json
Normal file
1
locales/pt_BR.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
"argument_required": "Требуется'{argument}' аргумент",
|
"argument_required": "Требуется аргумент «{argument}»",
|
||||||
"authentication_required": "Требуется аутентификация",
|
"authentication_required": "Требуется аутентификация",
|
||||||
"colon": "{}: ",
|
|
||||||
"confirm": "Подтвердить {prompt}",
|
"confirm": "Подтвердить {prompt}",
|
||||||
"deprecated_command": "'{prog} {command}' устарела и будет удалена",
|
"deprecated_command": "'{prog} {command}' устарела и будет удалена",
|
||||||
"deprecated_command_alias": "'{prog} {old}' устарела и будет удалена, вместо неё используйте '{prog} {new}'",
|
"deprecated_command_alias": "'{prog} {old}' устарела и будет удалена, вместо неё используйте '{prog} {new}'",
|
||||||
|
@ -11,7 +10,7 @@
|
||||||
"invalid_argument": "Неправильный аргумент '{argument}': {error}",
|
"invalid_argument": "Неправильный аргумент '{argument}': {error}",
|
||||||
"logged_in": "Вы вошли",
|
"logged_in": "Вы вошли",
|
||||||
"logged_out": "Вы вышли из системы",
|
"logged_out": "Вы вышли из системы",
|
||||||
"not_logged_in": "Вы не залогинились",
|
"not_logged_in": "Вы не вошли в систему",
|
||||||
"operation_interrupted": "Действие прервано",
|
"operation_interrupted": "Действие прервано",
|
||||||
"password": "Пароль",
|
"password": "Пароль",
|
||||||
"pattern_not_match": "Не соответствует образцу",
|
"pattern_not_match": "Не соответствует образцу",
|
||||||
|
@ -26,22 +25,23 @@
|
||||||
"cannot_open_file": "Не могу открыть файл {file} (причина: {error})",
|
"cannot_open_file": "Не могу открыть файл {file} (причина: {error})",
|
||||||
"cannot_write_file": "Не могу записать файл {file} (причина: {error})",
|
"cannot_write_file": "Не могу записать файл {file} (причина: {error})",
|
||||||
"unknown_error_reading_file": "Неизвестная ошибка при попытке прочитать файл {file} (причина: {error})",
|
"unknown_error_reading_file": "Неизвестная ошибка при попытке прочитать файл {file} (причина: {error})",
|
||||||
"corrupted_yaml": "Повреждённой yaml получен от {ressource} (причина: {error})",
|
"corrupted_yaml": "Повреждённой YAML получен от {ressource} (причина: {error})",
|
||||||
"error_writing_file": "Ошибка при записи файла {file}: {error}",
|
"error_writing_file": "Ошибка при записи файла {file}: {error}",
|
||||||
"error_removing": "Ошибка при удалении {path}: {error}",
|
"error_removing": "Ошибка при удалении {path}: {error}",
|
||||||
"invalid_url": "Неправильный url {url} (этот сайт существует ?)",
|
"invalid_url": "Не удалось подключиться к {url}... возможно этот сервис недоступен или вы не подключены к Интернету через IPv4/IPv6.",
|
||||||
"download_ssl_error": "Ошибка SSL при соединении с {url}",
|
"download_ssl_error": "Ошибка SSL при соединении с {url}",
|
||||||
"download_timeout": "Превышено время ожидания ответа от {url}.",
|
"download_timeout": "Превышено время ожидания ответа от {url}.",
|
||||||
"download_unknown_error": "Ошибка при загрузке данных с {url} : {error}",
|
"download_unknown_error": "Ошибка при загрузке данных с {url} : {error}",
|
||||||
"instance_already_running": "Операция YunoHost уже запущена. Пожалуйста, подождите, пока он закончится, прежде чем запускать другой.",
|
"instance_already_running": "Операция YunoHost уже запущена. Пожалуйста, подождите, пока он закончится, прежде чем запускать другой.",
|
||||||
"root_required": "Чтобы выполнить это действие, вы должны иметь права root",
|
"root_required": "Чтобы выполнить это действие, вы должны иметь права root",
|
||||||
"corrupted_json": "Повреждённый json получен от {ressource} (причина: {error})",
|
"corrupted_json": "Повреждённый json получен от {ressource} (причина: {error})",
|
||||||
"warn_the_user_that_lock_is_acquired": "другая команда только что завершилась, теперь запускает эту команду",
|
"warn_the_user_that_lock_is_acquired": "Другая команда только что завершилась, теперь запускается эта команда",
|
||||||
"warn_the_user_about_waiting_lock_again": "Все еще жду...",
|
"warn_the_user_about_waiting_lock_again": "Все еще жду...",
|
||||||
"warn_the_user_about_waiting_lock": "Сейчас запускается еще одна команда YunoHost, мы ждем ее завершения, прежде чем запустить эту",
|
"warn_the_user_about_waiting_lock": "Сейчас запускается еще одна команда YunoHost, мы ждем ее завершения, прежде чем запустить эту",
|
||||||
"download_bad_status_code": "{url} вернул код состояния {code}",
|
"download_bad_status_code": "{url} вернул код состояния {code}",
|
||||||
"error_changing_file_permissions": "Ошибка при изменении разрешений для {path}: {error}",
|
"error_changing_file_permissions": "Ошибка при изменении разрешений для {path}: {error}",
|
||||||
"corrupted_toml": "Поврежденный том, прочитанный из {ressource} (причина: {error})",
|
"corrupted_toml": "Поврежденный TOML, прочитанный из {ressource} (причина: {error})",
|
||||||
"invalid_usage": "Неправильное использование, передайте --help, чтобы увидеть помощь",
|
"invalid_usage": "Неправильное использование, передайте --help, чтобы увидеть помощь",
|
||||||
"info": "Информация:"
|
"info": "Информация:",
|
||||||
|
"edit_text_question": "{}. Изменить этот текст? [yN]: "
|
||||||
}
|
}
|
47
locales/sk.json
Normal file
47
locales/sk.json
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"authentication_required": "Vyžadované overenie",
|
||||||
|
"confirm": "Potvrdiť {prompt}",
|
||||||
|
"password": "Heslo",
|
||||||
|
"argument_required": "Je vyžadovaný argument '{argument}'",
|
||||||
|
"deprecated_command": "'{prog} {command}' je zastaraný a v ďalších verziách bude odstránený",
|
||||||
|
"deprecated_command_alias": "'{prog} {old}' je zastaraný a v ďalších verziách bude odstránený, použite miesto neho '{prog} {new}'",
|
||||||
|
"edit_text_question": "{}. Upraviť tento text? [yN]: ",
|
||||||
|
"error": "Chyba:",
|
||||||
|
"file_not_exist": "Súbor neexistuje: '{path}'",
|
||||||
|
"folder_exists": "Adresár už existuje: '{path}'",
|
||||||
|
"info": "Informácie:",
|
||||||
|
"instance_already_running": "Práve prebieha iná YunoHost operácia. Pred spustením ďalšej operácie počkajte na jej dokončenie.",
|
||||||
|
"invalid_argument": "Nesprávny argument '{argument}': {error}",
|
||||||
|
"invalid_usage": "Nesprávne použitie, pridajte --help pre zobrazenie pomocníka",
|
||||||
|
"logged_in": "Prihlásený",
|
||||||
|
"logged_out": "Boli ste odhlásený",
|
||||||
|
"not_logged_in": "Nie ste prihlásený",
|
||||||
|
"operation_interrupted": "Operácia bola prerušená",
|
||||||
|
"pattern_not_match": "Nezodpovedá výrazu",
|
||||||
|
"root_required": "Pre vykonanie tejto akcie musíte byť root",
|
||||||
|
"server_already_running": "Na tomto porte už server beží",
|
||||||
|
"success": "Podarilo sa!",
|
||||||
|
"unable_authenticate": "Nie je možné overiť",
|
||||||
|
"unknown_group": "Neznáma skupina '{group}'",
|
||||||
|
"unknown_user": "Neznámy používateľ '{user}'",
|
||||||
|
"values_mismatch": "Hodnoty nesúhlasia",
|
||||||
|
"warning": "Varovanie:",
|
||||||
|
"websocket_request_expected": "Očakávaná WebSocket požiadavka",
|
||||||
|
"cannot_open_file": "Nedá sa otvoriť súbor {file} (príčina: {error})",
|
||||||
|
"cannot_write_file": "Nedá sa zapísať do súboru {file} (príčina: {error})",
|
||||||
|
"unknown_error_reading_file": "Vyskytla sa neznáma chyba pri čítaní súboru {file} (príčina: {error})",
|
||||||
|
"corrupted_json": "Nepodarilo sa načítať JSON {ressource} (príčina: {error})",
|
||||||
|
"corrupted_yaml": "Nepodarilo sa načítať YAML z {ressource} (príčina: {error})",
|
||||||
|
"corrupted_toml": "Nepodarilo sa načítať TOML z {ressource} (príčina: {error})",
|
||||||
|
"error_writing_file": "Chyba pri zápise do súboru {file}: {error}",
|
||||||
|
"error_removing": "Chyba pri odstraňovaní {path}: {error}",
|
||||||
|
"error_changing_file_permissions": "Chyba pri nastavovaní oprávnení pre {path}: {error}",
|
||||||
|
"invalid_url": "Nepodarilo sa pripojiť k {url}… možno je služba vypnutá alebo nemáte fungujúce pripojenie k internetu prostredníctvom IPv4/IPv6.",
|
||||||
|
"download_ssl_error": "SSL chyba počas spojenia s {url}",
|
||||||
|
"download_timeout": "{url} príliš dlho neodpovedá, vzdávam to.",
|
||||||
|
"download_unknown_error": "Chyba pri sťahovaní dát z {url}: {error}",
|
||||||
|
"download_bad_status_code": "{url} vrátil stavový kód {code}",
|
||||||
|
"warn_the_user_about_waiting_lock": "Práve prebieha iná operácia YunoHost, pred spustením čakáme na jej dokončenie",
|
||||||
|
"warn_the_user_about_waiting_lock_again": "Stále čakáme…",
|
||||||
|
"warn_the_user_that_lock_is_acquired": "Predchádzajúca operácia bola dokončená, teraz spúšťame túto"
|
||||||
|
}
|
1
locales/sl.json
Normal file
1
locales/sl.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"warn_the_user_about_waiting_lock_again": "Väntar fortfarande …",
|
"warn_the_user_about_waiting_lock_again": "Väntar fortfarande…",
|
||||||
"download_bad_status_code": "{url} svarade med statuskod {code}",
|
"download_bad_status_code": "{url} svarade med statuskod {code}",
|
||||||
"download_timeout": "Gav upp eftersom {url} tog för lång tid på sig att svara.",
|
"download_timeout": "Gav upp eftersom {url} tog för lång tid på sig att svara.",
|
||||||
"download_ssl_error": "Ett SSL-fel påträffades vid anslutning till {url}",
|
"download_ssl_error": "Ett SSL-fel påträffades vid anslutning till {url}",
|
||||||
|
@ -26,7 +26,6 @@
|
||||||
"deprecated_command_alias": "'{prog} {old}' rekommenderas inte längre och kommer tas bort i framtiden, använd '{prog} {new}' istället",
|
"deprecated_command_alias": "'{prog} {old}' rekommenderas inte längre och kommer tas bort i framtiden, använd '{prog} {new}' istället",
|
||||||
"deprecated_command": "'{prog} {command}' rekommenderas inte längre och kommer tas bort i framtiden",
|
"deprecated_command": "'{prog} {command}' rekommenderas inte längre och kommer tas bort i framtiden",
|
||||||
"confirm": "Bekräfta {prompt}",
|
"confirm": "Bekräfta {prompt}",
|
||||||
"colon": "{}: ",
|
|
||||||
"argument_required": "Parametern '{argument}' krävs",
|
"argument_required": "Parametern '{argument}' krävs",
|
||||||
"password": "Lösenord",
|
"password": "Lösenord",
|
||||||
"warn_the_user_that_lock_is_acquired": "det andra kommandot har bara slutförts, nu startar du det här kommandot",
|
"warn_the_user_that_lock_is_acquired": "det andra kommandot har bara slutförts, nu startar du det här kommandot",
|
||||||
|
|
1
locales/te.json
Normal file
1
locales/te.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
"argument_required": "{argument} argümanı gerekli",
|
"argument_required": "{argument} argümanı gerekli",
|
||||||
"authentication_required": "Yetklendirme gerekli",
|
"authentication_required": "Yetklendirme gerekli",
|
||||||
"colon": "{}: ",
|
|
||||||
"confirm": "{prompt}'i doğrulayın",
|
"confirm": "{prompt}'i doğrulayın",
|
||||||
"error": "Hata:",
|
"error": "Hata:",
|
||||||
"instance_already_running": "Halihazırda bir YunoHost operasyonu var. Lütfen başka bir tane çalıştırmadan önce bitmesini bekleyin.",
|
"instance_already_running": "Halihazırda bir YunoHost operasyonu var. Lütfen başka bir tane çalıştırmadan önce bitmesini bekleyin.",
|
||||||
|
@ -19,20 +18,20 @@
|
||||||
"values_mismatch": "Değerler uyuşmuyor",
|
"values_mismatch": "Değerler uyuşmuyor",
|
||||||
"warning": "Uyarı:",
|
"warning": "Uyarı:",
|
||||||
"websocket_request_expected": "WebSocket isteği gerekli",
|
"websocket_request_expected": "WebSocket isteği gerekli",
|
||||||
"warn_the_user_that_lock_is_acquired": "diğer komut şimdi tamamlandı, şimdi bu komutu başlatıyor",
|
"warn_the_user_that_lock_is_acquired": "Diğer komut şimdi tamamlandı, şimdi bu komutu başlatıyor",
|
||||||
"warn_the_user_about_waiting_lock_again": "Hala bekliyor...",
|
"warn_the_user_about_waiting_lock_again": "Hala bekliyor…",
|
||||||
"warn_the_user_about_waiting_lock": "Başka bir YunoHost komutu şu anda çalışıyor, bunu çalıştırmadan önce bitmesini bekliyoruz",
|
"warn_the_user_about_waiting_lock": "Başka bir YunoHost komutu şu anda çalışıyor, bunu çalıştırmadan önce bitmesini bekliyoruz",
|
||||||
"download_bad_status_code": "{url} döndürülen durum kodu {code}",
|
"download_bad_status_code": "{url} döndürülen durum kodu {code}",
|
||||||
"download_unknown_error": "{url} adresinden veri indirilirken hata oluştu: {error}",
|
"download_unknown_error": "{url} adresinden veri indirilirken hata oluştu: {error}",
|
||||||
"download_timeout": "{url} yanıtlaması çok uzun sürdü, pes etti.",
|
"download_timeout": "{url} yanıtlaması çok uzun sürdü, pes etti.",
|
||||||
"download_ssl_error": "{url} ağına bağlanırken SSL hatası",
|
"download_ssl_error": "{url} ağına bağlanırken SSL hatası",
|
||||||
"invalid_url": "Geçersiz url {url} (bu site var mı?)",
|
"invalid_url": "{url} bağlanılamadı… URL çalışmıyor olabilir veya IPv4/IPv6 internete düzgün bir şekilde bağlı değilsiniz.",
|
||||||
"error_changing_file_permissions": "{path} için izinler değiştirilirken hata oluştu: {error}",
|
"error_changing_file_permissions": "{path} için izinler değiştirilirken hata oluştu: {error}",
|
||||||
"error_removing": "{path} kaldırılırken hata oluştu: {error}",
|
"error_removing": "{path} kaldırılırken hata oluştu: {error}",
|
||||||
"error_writing_file": "{file} dosyası yazılırken hata oluştu: {error}",
|
"error_writing_file": "{file} dosyası yazılırken hata oluştu: {error}",
|
||||||
"corrupted_toml": "{ressource} kaynağından okunan bozuk toml (nedeni: {error})",
|
"corrupted_toml": "{ressource} kaynağından okunan bozuk TOML(nedeni: {error})",
|
||||||
"corrupted_yaml": "{ressource} kaynağından bozuk yaml okunuyor (nedeni: {error})",
|
"corrupted_yaml": "{ressource} kaynağından bozuk YAML okunuyor (neden: {error})",
|
||||||
"corrupted_json": "{ressource} adresinden okunan bozuk json (nedeni: {error})",
|
"corrupted_json": "{ressource} adresinden okunan bozuk json (neden: {error})",
|
||||||
"unknown_error_reading_file": "{file} dosyasını okumaya çalışırken bilinmeyen hata (nedeni: {error})",
|
"unknown_error_reading_file": "{file} dosyasını okumaya çalışırken bilinmeyen hata (nedeni: {error})",
|
||||||
"cannot_write_file": "{file} dosyası yazılamadı (nedeni: {error})",
|
"cannot_write_file": "{file} dosyası yazılamadı (nedeni: {error})",
|
||||||
"cannot_open_file": "{file} dosyası açılamadı (nedeni: {error})",
|
"cannot_open_file": "{file} dosyası açılamadı (nedeni: {error})",
|
||||||
|
@ -43,5 +42,6 @@
|
||||||
"folder_exists": "Klasör zaten var: '{path}'",
|
"folder_exists": "Klasör zaten var: '{path}'",
|
||||||
"file_not_exist": "Dosya mevcut değil: '{path}'",
|
"file_not_exist": "Dosya mevcut değil: '{path}'",
|
||||||
"deprecated_command_alias": "'{prog} {old}' kullanımdan kaldırıldı ve gelecekte kaldırılacak, bunun yerine '{prog} {new}' kullanın",
|
"deprecated_command_alias": "'{prog} {old}' kullanımdan kaldırıldı ve gelecekte kaldırılacak, bunun yerine '{prog} {new}' kullanın",
|
||||||
"deprecated_command": "'{prog} {command}' kullanımdan kaldırıldı ve gelecekte kaldırılacak"
|
"deprecated_command": "'{prog} {command}' kullanımdan kaldırıldı ve gelecekte kaldırılacak",
|
||||||
|
"edit_text_question": "{}. Bu metni düzenle ? [yN]: "
|
||||||
}
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"password": "Пароль",
|
"password": "Пароль",
|
||||||
"logged_out": "Ви вийшли з системи",
|
"logged_out": "Ви вийшли з системи",
|
||||||
"invalid_url": "Помилка з'єднання із {url}... можливо, служба не працює, або ви неправильно під'єднані до Інтернету в IPv4/IPv6.",
|
"invalid_url": "Помилка з'єднання із {url}… можливо, служба не працює, або ви неправильно під'єднані до Інтернету в IPv4/IPv6.",
|
||||||
"warn_the_user_that_lock_is_acquired": "Інша команда щойно завершилася, тепер запускаємо цю команду",
|
"warn_the_user_that_lock_is_acquired": "Інша команда щойно завершилася, тепер запускаємо цю команду",
|
||||||
"warn_the_user_about_waiting_lock_again": "Досі очікуємо...",
|
"warn_the_user_about_waiting_lock_again": "Досі очікуємо…",
|
||||||
"warn_the_user_about_waiting_lock": "Зараз запускається ще одна команда YunoHost, ми чекаємо її завершення, перш ніж запустити цю",
|
"warn_the_user_about_waiting_lock": "Зараз запускається ще одна команда YunoHost, ми чекаємо її завершення, перш ніж запустити цю",
|
||||||
"download_bad_status_code": "{url} повернув код стану {code}",
|
"download_bad_status_code": "{url} повернув код стану {code}",
|
||||||
"download_unknown_error": "Помилка під час завантаження даних з {url}: {error}",
|
"download_unknown_error": "Помилка під час завантаження даних з {url}: {error}",
|
||||||
|
@ -41,7 +41,6 @@
|
||||||
"deprecated_command_alias": "'{prog} {old}' застаріла і буде видалена, замість неї використовуйте '{prog} {new}'",
|
"deprecated_command_alias": "'{prog} {old}' застаріла і буде видалена, замість неї використовуйте '{prog} {new}'",
|
||||||
"deprecated_command": "'{prog} {command}' застаріла і буде видалена",
|
"deprecated_command": "'{prog} {command}' застаріла і буде видалена",
|
||||||
"confirm": "Підтвердити {prompt}",
|
"confirm": "Підтвердити {prompt}",
|
||||||
"colon": "{}: ",
|
|
||||||
"authentication_required": "Потрібна автентифікація",
|
"authentication_required": "Потрібна автентифікація",
|
||||||
"argument_required": "Потрібен аргумент '{argument}'",
|
"argument_required": "Потрібен аргумент '{argument}'",
|
||||||
"edit_text_question": "{}. Редагувати цей текст? [yN]: "
|
"edit_text_question": "{}. Редагувати цей текст? [yN]: "
|
||||||
|
|
37
maintenance/make_changelog.sh
Normal file
37
maintenance/make_changelog.sh
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
VERSION="11.2.1"
|
||||||
|
RELEASE="stable"
|
||||||
|
REPO=$(basename $(git rev-parse --show-toplevel))
|
||||||
|
REPO_URL=$(git remote get-url origin)
|
||||||
|
ME=$(git config --get user.name)
|
||||||
|
EMAIL=$(git config --get user.email)
|
||||||
|
|
||||||
|
LAST_RELEASE=$(git tag --list 'debian/11.*' --sort="v:refname" | tail -n 1)
|
||||||
|
|
||||||
|
echo "$REPO ($VERSION) $RELEASE; urgency=low"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
git log $LAST_RELEASE.. -n 10000 --first-parent --pretty=tformat:' - %b%s (%h)' \
|
||||||
|
| sed -E "s&Merge .*#([0-9]+).*\$& \([#\1]\(http://github.com/YunoHost/$REPO/pull/\1\)\)&g" \
|
||||||
|
| sed -E "/Co-authored-by: .* <.*>/d" \
|
||||||
|
| grep -v "Translations update from Weblate" \
|
||||||
|
| tac
|
||||||
|
|
||||||
|
TRANSLATIONS=$(git log $LAST_RELEASE... -n 10000 --pretty=format:"%s" \
|
||||||
|
| grep "Translated using Weblate" \
|
||||||
|
| sed -E "s/Translated using Weblate \((.*)\)/\1/g" \
|
||||||
|
| sort | uniq | tr '\n' ', ' | sed -e 's/,$//g' -e 's/,/, /g')
|
||||||
|
[[ -z "$TRANSLATIONS" ]] || echo " - [i18n] Translations updated for $TRANSLATIONS"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
CONTRIBUTORS=$(git log -n10 --pretty=format:'%Cred%h%Creset %C(bold blue)(%an) %Creset%Cgreen(%cr)%Creset - %s %C(yellow)%d%Creset' --abbrev-commit $LAST_RELEASE... -n 10000 --pretty=format:"%an" \
|
||||||
|
| sort | uniq | grep -v "$ME" | grep -v 'yunohost-bot' | grep -vi 'weblate' \
|
||||||
|
| tr '\n' ', ' | sed -e 's/,$//g' -e 's/,/, /g')
|
||||||
|
[[ -z "$CONTRIBUTORS" ]] || echo " Thanks to all contributors <3 ! ($CONTRIBUTORS)"
|
||||||
|
echo ""
|
||||||
|
echo " -- $ME <$EMAIL> $(date -R)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# PR links can be converted to regular texts using : sed -E 's@\[(#[0-9]*)\]\([^ )]*\)@\1@g'
|
||||||
|
# Or readded with sed -E 's@#([0-9]*)@[YunoHost#\1](https://github.com/yunohost/yunohost/pull/\1)@g' | sed -E 's@\((\w+)\)@([YunoHost/\1](https://github.com/yunohost/yunohost/commit/\1))@g'
|
|
@ -3,7 +3,6 @@
|
||||||
from moulinette.core import (
|
from moulinette.core import (
|
||||||
MoulinetteError,
|
MoulinetteError,
|
||||||
Moulinette18n,
|
Moulinette18n,
|
||||||
env,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
__title__ = "moulinette"
|
__title__ = "moulinette"
|
||||||
|
@ -31,7 +30,7 @@ __all__ = ["init", "api", "cli", "m18n", "MoulinetteError", "Moulinette"]
|
||||||
m18n = Moulinette18n()
|
m18n = Moulinette18n()
|
||||||
|
|
||||||
|
|
||||||
class classproperty(object):
|
class classproperty:
|
||||||
def __init__(self, f):
|
def __init__(self, f):
|
||||||
self.f = f
|
self.f = f
|
||||||
|
|
||||||
|
@ -40,7 +39,6 @@ class classproperty(object):
|
||||||
|
|
||||||
|
|
||||||
class Moulinette:
|
class Moulinette:
|
||||||
|
|
||||||
_interface = None
|
_interface = None
|
||||||
|
|
||||||
def prompt(*args, **kwargs):
|
def prompt(*args, **kwargs):
|
||||||
|
@ -54,35 +52,8 @@ class Moulinette:
|
||||||
return cls._interface
|
return cls._interface
|
||||||
|
|
||||||
|
|
||||||
# Package functions
|
|
||||||
|
|
||||||
|
|
||||||
def init(logging_config=None, **kwargs):
|
|
||||||
"""Package initialization
|
|
||||||
|
|
||||||
Initialize directories and global variables. It must be called
|
|
||||||
before any of package method is used - even the easy access
|
|
||||||
functions.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- logging_config -- A dict containing logging configuration to load
|
|
||||||
- **kwargs -- See core.Package
|
|
||||||
|
|
||||||
At the end, the global variable 'pkg' will contain a Package
|
|
||||||
instance. See core.Package for available methods and variables.
|
|
||||||
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
from moulinette.utils.log import configure_logging
|
|
||||||
|
|
||||||
configure_logging(logging_config)
|
|
||||||
|
|
||||||
# Add library directory to python path
|
|
||||||
sys.path.insert(0, env["LIB_DIR"])
|
|
||||||
|
|
||||||
|
|
||||||
# Easy access to interfaces
|
# Easy access to interfaces
|
||||||
def api(host="localhost", port=80, routes={}):
|
def api(host="localhost", port=80, routes={}, actionsmap=None, locales_dir=None):
|
||||||
"""Web server (API) interface
|
"""Web server (API) interface
|
||||||
|
|
||||||
Run a HTTP server with the moulinette for an API usage.
|
Run a HTTP server with the moulinette for an API usage.
|
||||||
|
@ -96,8 +67,13 @@ def api(host="localhost", port=80, routes={}):
|
||||||
"""
|
"""
|
||||||
from moulinette.interfaces.api import Interface as Api
|
from moulinette.interfaces.api import Interface as Api
|
||||||
|
|
||||||
|
m18n.set_locales_dir(locales_dir)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
Api(routes=routes).run(host, port)
|
Api(
|
||||||
|
routes=routes,
|
||||||
|
actionsmap=actionsmap,
|
||||||
|
).run(host, port)
|
||||||
except MoulinetteError as e:
|
except MoulinetteError as e:
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -110,7 +86,9 @@ def api(host="localhost", port=80, routes={}):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def cli(args, top_parser, output_as=None, timeout=None):
|
def cli(
|
||||||
|
args, top_parser, output_as=None, timeout=None, actionsmap=None, locales_dir=None
|
||||||
|
):
|
||||||
"""Command line interface
|
"""Command line interface
|
||||||
|
|
||||||
Execute an action with the moulinette from the CLI and print its
|
Execute an action with the moulinette from the CLI and print its
|
||||||
|
@ -125,11 +103,15 @@ def cli(args, top_parser, output_as=None, timeout=None):
|
||||||
"""
|
"""
|
||||||
from moulinette.interfaces.cli import Interface as Cli
|
from moulinette.interfaces.cli import Interface as Cli
|
||||||
|
|
||||||
|
m18n.set_locales_dir(locales_dir)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
load_only_category = args[0] if args and not args[0].startswith("-") else None
|
load_only_category = args[0] if args and not args[0].startswith("-") else None
|
||||||
Cli(top_parser=top_parser, load_only_category=load_only_category).run(
|
Cli(
|
||||||
args, output_as=output_as, timeout=timeout
|
top_parser=top_parser,
|
||||||
)
|
load_only_category=load_only_category,
|
||||||
|
actionsmap=actionsmap,
|
||||||
|
).run(args, output_as=output_as, timeout=timeout)
|
||||||
except MoulinetteError as e:
|
except MoulinetteError as e:
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
|
@ -10,15 +10,15 @@ from typing import List, Optional
|
||||||
from time import time
|
from time import time
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
from functools import cache
|
||||||
|
|
||||||
from moulinette import m18n, Moulinette
|
from moulinette import m18n, Moulinette
|
||||||
from moulinette.core import (
|
from moulinette.core import (
|
||||||
MoulinetteError,
|
MoulinetteError,
|
||||||
MoulinetteLock,
|
MoulinetteLock,
|
||||||
MoulinetteValidationError,
|
MoulinetteValidationError,
|
||||||
env,
|
|
||||||
)
|
)
|
||||||
from moulinette.interfaces import BaseActionsMapParser, TO_RETURN_PROP
|
from moulinette.interfaces import BaseActionsMapParser
|
||||||
from moulinette.utils.log import start_action_logging
|
from moulinette.utils.log import start_action_logging
|
||||||
from moulinette.utils.filesystem import read_yaml
|
from moulinette.utils.filesystem import read_yaml
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ logger = logging.getLogger("moulinette.actionsmap")
|
||||||
# Extra parameters definition
|
# Extra parameters definition
|
||||||
|
|
||||||
|
|
||||||
class _ExtraParameter(object):
|
class _ExtraParameter:
|
||||||
"""
|
"""
|
||||||
Argument parser for an extra parameter.
|
Argument parser for an extra parameter.
|
||||||
|
|
||||||
|
@ -107,7 +107,6 @@ class CommentParameter(_ExtraParameter):
|
||||||
|
|
||||||
|
|
||||||
class AskParameter(_ExtraParameter):
|
class AskParameter(_ExtraParameter):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Ask for the argument value if possible and needed.
|
Ask for the argument value if possible and needed.
|
||||||
|
|
||||||
|
@ -146,7 +145,6 @@ class AskParameter(_ExtraParameter):
|
||||||
|
|
||||||
|
|
||||||
class PasswordParameter(AskParameter):
|
class PasswordParameter(AskParameter):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Ask for the password argument value if possible and needed.
|
Ask for the password argument value if possible and needed.
|
||||||
|
|
||||||
|
@ -169,7 +167,6 @@ class PasswordParameter(AskParameter):
|
||||||
|
|
||||||
|
|
||||||
class PatternParameter(_ExtraParameter):
|
class PatternParameter(_ExtraParameter):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Check if the argument value match a pattern.
|
Check if the argument value match a pattern.
|
||||||
|
|
||||||
|
@ -222,7 +219,6 @@ class PatternParameter(_ExtraParameter):
|
||||||
|
|
||||||
|
|
||||||
class RequiredParameter(_ExtraParameter):
|
class RequiredParameter(_ExtraParameter):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Check if a required argument is defined or not.
|
Check if a required argument is defined or not.
|
||||||
|
|
||||||
|
@ -261,8 +257,7 @@ extraparameters_list = [
|
||||||
# Extra parameters argument Parser
|
# Extra parameters argument Parser
|
||||||
|
|
||||||
|
|
||||||
class ExtraArgumentParser(object):
|
class ExtraArgumentParser:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Argument validator and parser for the extra parameters.
|
Argument validator and parser for the extra parameters.
|
||||||
|
|
||||||
|
@ -373,8 +368,7 @@ class ExtraArgumentParser(object):
|
||||||
# Main class ----------------------------------------------------------
|
# Main class ----------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class ActionsMap(object):
|
class ActionsMap:
|
||||||
|
|
||||||
"""Validate and process actions defined into an actions map
|
"""Validate and process actions defined into an actions map
|
||||||
|
|
||||||
The actions map defines the features - and their usage - of an
|
The actions map defines the features - and their usage - of an
|
||||||
|
@ -382,9 +376,6 @@ class ActionsMap(object):
|
||||||
It is composed by categories which contain one or more action(s).
|
It is composed by categories which contain one or more action(s).
|
||||||
Moreover, the action can have specific argument(s).
|
Moreover, the action can have specific argument(s).
|
||||||
|
|
||||||
This class allows to manipulate one or several actions maps
|
|
||||||
associated to a namespace.
|
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
- top_parser -- A BaseActionsMapParser-derived instance to use for
|
- top_parser -- A BaseActionsMapParser-derived instance to use for
|
||||||
parsing the actions map
|
parsing the actions map
|
||||||
|
@ -394,92 +385,82 @@ class ActionsMap(object):
|
||||||
purposes...
|
purposes...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, top_parser, load_only_category=None):
|
def __init__(self, actionsmap_yml, top_parser, load_only_category=None):
|
||||||
|
|
||||||
assert isinstance(top_parser, BaseActionsMapParser), (
|
assert isinstance(top_parser, BaseActionsMapParser), (
|
||||||
"Invalid parser class '%s'" % top_parser.__class__.__name__
|
"Invalid parser class '%s'" % top_parser.__class__.__name__
|
||||||
)
|
)
|
||||||
|
|
||||||
DATA_DIR = env["DATA_DIR"]
|
|
||||||
CACHE_DIR = env["CACHE_DIR"]
|
|
||||||
|
|
||||||
actionsmaps = OrderedDict()
|
|
||||||
|
|
||||||
self.from_cache = False
|
self.from_cache = False
|
||||||
# Iterate over actions map namespaces
|
|
||||||
for n in self.get_namespaces():
|
|
||||||
logger.debug("loading actions map namespace '%s'", n)
|
|
||||||
|
|
||||||
actionsmap_yml = "%s/actionsmap/%s.yml" % (DATA_DIR, n)
|
logger.debug("loading actions map")
|
||||||
actionsmap_yml_stat = os.stat(actionsmap_yml)
|
|
||||||
actionsmap_pkl = "%s/actionsmap/%s-%d-%d.pkl" % (
|
|
||||||
CACHE_DIR,
|
|
||||||
n,
|
|
||||||
actionsmap_yml_stat.st_size,
|
|
||||||
actionsmap_yml_stat.st_mtime,
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate_cache():
|
actionsmap_yml_dir = os.path.dirname(actionsmap_yml)
|
||||||
|
actionsmap_yml_file = os.path.basename(actionsmap_yml)
|
||||||
|
actionsmap_yml_stat = os.stat(actionsmap_yml)
|
||||||
|
|
||||||
# Iterate over actions map namespaces
|
actionsmap_pkl = f"{actionsmap_yml_dir}/.{actionsmap_yml_file}.{actionsmap_yml_stat.st_size}-{actionsmap_yml_stat.st_mtime}.pkl"
|
||||||
logger.debug("generating cache for actions map namespace '%s'", n)
|
|
||||||
|
|
||||||
# Read actions map from yaml file
|
def generate_cache():
|
||||||
actionsmap = read_yaml(actionsmap_yml)
|
logger.debug("generating cache for actions map")
|
||||||
|
|
||||||
# Delete old cache files
|
# Read actions map from yaml file
|
||||||
for old_cache in glob.glob("%s/actionsmap/%s-*.pkl" % (CACHE_DIR, n)):
|
actionsmap = read_yaml(actionsmap_yml)
|
||||||
os.remove(old_cache)
|
|
||||||
|
|
||||||
# at installation, cachedir might not exists
|
|
||||||
dir_ = os.path.dirname(actionsmap_pkl)
|
|
||||||
if not os.path.isdir(dir_):
|
|
||||||
os.makedirs(dir_)
|
|
||||||
|
|
||||||
# Cache actions map into pickle file
|
|
||||||
with open(actionsmap_pkl, "wb") as f:
|
|
||||||
pickle.dump(actionsmap, f)
|
|
||||||
|
|
||||||
|
if not actionsmap["_global"].get("cache", True):
|
||||||
return actionsmap
|
return actionsmap
|
||||||
|
|
||||||
if os.path.exists(actionsmap_pkl):
|
# Delete old cache files
|
||||||
try:
|
for old_cache in glob.glob(
|
||||||
# Attempt to load cache
|
f"{actionsmap_yml_dir}/.{actionsmap_yml_file}.*.pkl"
|
||||||
with open(actionsmap_pkl, "rb") as f:
|
):
|
||||||
actionsmaps[n] = pickle.load(f)
|
os.remove(old_cache)
|
||||||
|
|
||||||
self.from_cache = True
|
# at installation, cachedir might not exists
|
||||||
# TODO: Switch to python3 and catch proper exception
|
dir_ = os.path.dirname(actionsmap_pkl)
|
||||||
except (IOError, EOFError):
|
if not os.path.isdir(dir_):
|
||||||
actionsmaps[n] = generate_cache()
|
os.makedirs(dir_)
|
||||||
else: # cache file doesn't exists
|
|
||||||
actionsmaps[n] = generate_cache()
|
|
||||||
|
|
||||||
# If load_only_category is set, and *if* the target category
|
# Cache actions map into pickle file
|
||||||
# is in the actionsmap, we'll load only that one.
|
with open(actionsmap_pkl, "wb") as f:
|
||||||
# If we filter it even if it doesn't exist, we'll end up with a
|
pickle.dump(actionsmap, f)
|
||||||
# weird help message when we do a typo in the category name..
|
|
||||||
if load_only_category and load_only_category in actionsmaps[n]:
|
|
||||||
actionsmaps[n] = {
|
|
||||||
k: v
|
|
||||||
for k, v in actionsmaps[n].items()
|
|
||||||
if k in [load_only_category, "_global"]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load translations
|
return actionsmap
|
||||||
m18n.load_namespace(n)
|
|
||||||
|
if os.path.exists(actionsmap_pkl):
|
||||||
|
try:
|
||||||
|
# Attempt to load cache
|
||||||
|
with open(actionsmap_pkl, "rb") as f:
|
||||||
|
actionsmap = pickle.load(f)
|
||||||
|
|
||||||
|
self.from_cache = True
|
||||||
|
# TODO: Switch to python3 and catch proper exception
|
||||||
|
except (IOError, EOFError):
|
||||||
|
actionsmap = generate_cache()
|
||||||
|
else: # cache file doesn't exists
|
||||||
|
actionsmap = generate_cache()
|
||||||
|
|
||||||
|
# If load_only_category is set, and *if* the target category
|
||||||
|
# is in the actionsmap, we'll load only that one.
|
||||||
|
# If we filter it even if it doesn't exist, we'll end up with a
|
||||||
|
# weird help message when we do a typo in the category name..
|
||||||
|
if load_only_category and load_only_category in actionsmap:
|
||||||
|
actionsmap = {
|
||||||
|
k: v
|
||||||
|
for k, v in actionsmap.items()
|
||||||
|
if k in [load_only_category, "_global"]
|
||||||
|
}
|
||||||
|
|
||||||
# Generate parsers
|
# Generate parsers
|
||||||
self.extraparser = ExtraArgumentParser(top_parser.interface)
|
self.extraparser = ExtraArgumentParser(top_parser.interface)
|
||||||
self.parser = self._construct_parser(actionsmaps, top_parser)
|
self.parser = self._construct_parser(actionsmap, top_parser)
|
||||||
|
|
||||||
|
@cache
|
||||||
def get_authenticator(self, auth_method):
|
def get_authenticator(self, auth_method):
|
||||||
|
|
||||||
if auth_method == "default":
|
if auth_method == "default":
|
||||||
auth_method = self.default_authentication
|
auth_method = self.default_authentication
|
||||||
|
|
||||||
# Load and initialize the authenticator module
|
# Load and initialize the authenticator module
|
||||||
auth_module = "%s.authenticators.%s" % (self.main_namespace, auth_method)
|
auth_module = f"{self.namespace}.authenticators.{auth_method}"
|
||||||
logger.debug(f"Loading auth module {auth_module}")
|
logger.debug(f"Loading auth module {auth_module}")
|
||||||
try:
|
try:
|
||||||
mod = import_module(auth_module)
|
mod = import_module(auth_module)
|
||||||
|
@ -494,7 +475,6 @@ class ActionsMap(object):
|
||||||
return mod.Authenticator()
|
return mod.Authenticator()
|
||||||
|
|
||||||
def check_authentication_if_required(self, *args, **kwargs):
|
def check_authentication_if_required(self, *args, **kwargs):
|
||||||
|
|
||||||
auth_method = self.parser.auth_method(*args, **kwargs)
|
auth_method = self.parser.auth_method(*args, **kwargs)
|
||||||
|
|
||||||
if auth_method is None:
|
if auth_method is None:
|
||||||
|
@ -525,19 +505,17 @@ class ActionsMap(object):
|
||||||
tid = arguments.pop("_tid")
|
tid = arguments.pop("_tid")
|
||||||
arguments = self.extraparser.parse_args(tid, arguments)
|
arguments = self.extraparser.parse_args(tid, arguments)
|
||||||
|
|
||||||
# Return immediately if a value is defined
|
want_to_take_lock = self.parser.want_to_take_lock(args, **kwargs)
|
||||||
if TO_RETURN_PROP in arguments:
|
|
||||||
return arguments.get(TO_RETURN_PROP)
|
|
||||||
|
|
||||||
# Retrieve action information
|
# Retrieve action information
|
||||||
if len(tid) == 4:
|
if len(tid) == 4:
|
||||||
namespace, category, subcategory, action = tid
|
namespace, category, subcategory, action = tid
|
||||||
func_name = "%s_%s_%s" % (
|
func_name = "{}_{}_{}".format(
|
||||||
category,
|
category,
|
||||||
subcategory.replace("-", "_"),
|
subcategory.replace("-", "_"),
|
||||||
action.replace("-", "_"),
|
action.replace("-", "_"),
|
||||||
)
|
)
|
||||||
full_action_name = "%s.%s.%s.%s" % (
|
full_action_name = "{}.{}.{}.{}".format(
|
||||||
namespace,
|
namespace,
|
||||||
category,
|
category,
|
||||||
subcategory,
|
subcategory,
|
||||||
|
@ -547,22 +525,22 @@ class ActionsMap(object):
|
||||||
assert len(tid) == 3
|
assert len(tid) == 3
|
||||||
namespace, category, action = tid
|
namespace, category, action = tid
|
||||||
subcategory = None
|
subcategory = None
|
||||||
func_name = "%s_%s" % (category, action.replace("-", "_"))
|
func_name = "{}_{}".format(category, action.replace("-", "_"))
|
||||||
full_action_name = "%s.%s.%s" % (namespace, category, action)
|
full_action_name = "{}.{}.{}".format(namespace, category, action)
|
||||||
|
|
||||||
# Lock the moulinette for the namespace
|
# Lock the moulinette for the namespace
|
||||||
with MoulinetteLock(namespace, timeout):
|
with MoulinetteLock(namespace, timeout, self.enable_lock and want_to_take_lock):
|
||||||
start = time()
|
start = time()
|
||||||
try:
|
try:
|
||||||
mod = __import__(
|
mod = __import__(
|
||||||
"%s.%s" % (namespace, category),
|
"{}.{}".format(namespace, category),
|
||||||
globals=globals(),
|
globals=globals(),
|
||||||
level=0,
|
level=0,
|
||||||
fromlist=[func_name],
|
fromlist=[func_name],
|
||||||
)
|
)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"loading python module %s took %.3fs",
|
"loading python module %s took %.3fs",
|
||||||
"%s.%s" % (namespace, category),
|
"{}.{}".format(namespace, category),
|
||||||
time() - start,
|
time() - start,
|
||||||
)
|
)
|
||||||
func = getattr(mod, func_name)
|
func = getattr(mod, func_name)
|
||||||
|
@ -570,7 +548,7 @@ class ActionsMap(object):
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
error_message = "unable to load function %s.%s because: %s" % (
|
error_message = "unable to load function {}.{} because: {}".format(
|
||||||
namespace,
|
namespace,
|
||||||
func_name,
|
func_name,
|
||||||
e,
|
e,
|
||||||
|
@ -591,7 +569,6 @@ class ActionsMap(object):
|
||||||
logger.debug("processing action [%s]: %s", log_id, full_action_name)
|
logger.debug("processing action [%s]: %s", log_id, full_action_name)
|
||||||
|
|
||||||
# Load translation and process the action
|
# Load translation and process the action
|
||||||
m18n.load_namespace(namespace)
|
|
||||||
start = time()
|
start = time()
|
||||||
try:
|
try:
|
||||||
return func(**arguments)
|
return func(**arguments)
|
||||||
|
@ -599,43 +576,14 @@ class ActionsMap(object):
|
||||||
stop = time()
|
stop = time()
|
||||||
logger.debug("action [%s] executed in %.3fs", log_id, stop - start)
|
logger.debug("action [%s] executed in %.3fs", log_id, stop - start)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_namespaces():
|
|
||||||
"""
|
|
||||||
Retrieve available actions map namespaces
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of available namespaces
|
|
||||||
|
|
||||||
"""
|
|
||||||
namespaces = []
|
|
||||||
|
|
||||||
DATA_DIR = env["DATA_DIR"]
|
|
||||||
|
|
||||||
# This var is ['*'] by default but could be set for example to
|
|
||||||
# ['yunohost', 'yml_*']
|
|
||||||
NAMESPACE_PATTERNS = env["NAMESPACES"].split()
|
|
||||||
|
|
||||||
# Look for all files that match the given patterns in the actionsmap dir
|
|
||||||
for namespace_pattern in NAMESPACE_PATTERNS:
|
|
||||||
namespaces.extend(
|
|
||||||
glob.glob("%s/actionsmap/%s.yml" % (DATA_DIR, namespace_pattern))
|
|
||||||
)
|
|
||||||
|
|
||||||
# Keep only the filenames with extension
|
|
||||||
namespaces = [os.path.basename(n)[:-4] for n in namespaces]
|
|
||||||
|
|
||||||
return namespaces
|
|
||||||
|
|
||||||
# Private methods
|
# Private methods
|
||||||
|
|
||||||
def _construct_parser(self, actionsmaps, top_parser):
|
def _construct_parser(self, actionsmap, top_parser):
|
||||||
"""
|
"""
|
||||||
Construct the parser with the actions map
|
Construct the parser with the actions map
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
- actionsmaps -- A dict of multi-level dictionnary of
|
- actionsmap -- A dictionnary of categories/actions/arguments list
|
||||||
categories/actions/arguments list for each namespaces
|
|
||||||
- top_parser -- A BaseActionsMapParser-derived instance to use for
|
- top_parser -- A BaseActionsMapParser-derived instance to use for
|
||||||
parsing the actions map
|
parsing the actions map
|
||||||
|
|
||||||
|
@ -658,52 +606,86 @@ class ActionsMap(object):
|
||||||
# * namespace define the top "name", for us it will always be
|
# * namespace define the top "name", for us it will always be
|
||||||
# "yunohost" and there well be only this one
|
# "yunohost" and there well be only this one
|
||||||
# * actionsmap is the actual actionsmap that we care about
|
# * actionsmap is the actual actionsmap that we care about
|
||||||
for namespace, actionsmap in actionsmaps.items():
|
|
||||||
# Retrieve global parameters
|
|
||||||
_global = actionsmap.pop("_global", {})
|
|
||||||
|
|
||||||
if _global:
|
# Retrieve global parameters
|
||||||
if getattr(self, "main_namespace", None) is not None:
|
_global = actionsmap.pop("_global", {})
|
||||||
raise MoulinetteError(
|
|
||||||
"It is not possible to have several namespaces with a _global section"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.main_namespace = namespace
|
|
||||||
self.name = _global["name"]
|
|
||||||
self.default_authentication = _global["authentication"][
|
|
||||||
interface_type
|
|
||||||
]
|
|
||||||
|
|
||||||
if top_parser.has_global_parser():
|
self.namespace = _global["namespace"]
|
||||||
top_parser.add_global_arguments(_global["arguments"])
|
self.enable_lock = _global.get("lock", True)
|
||||||
|
self.default_authentication = _global["authentication"][interface_type]
|
||||||
|
|
||||||
if not hasattr(self, "main_namespace"):
|
# category_name is stuff like "user", "domain", "hooks"...
|
||||||
raise MoulinetteError("Did not found the main namespace", raw_msg=True)
|
# category_values is the values of this category (like actions)
|
||||||
|
for category_name, category_values in actionsmap.items():
|
||||||
|
actions = category_values.pop("actions", {})
|
||||||
|
subcategories = category_values.pop("subcategories", {})
|
||||||
|
|
||||||
for namespace, actionsmap in actionsmaps.items():
|
# Get category parser
|
||||||
# category_name is stuff like "user", "domain", "hooks"...
|
category_parser = top_parser.add_category_parser(
|
||||||
# category_values is the values of this category (like actions)
|
category_name, **category_values
|
||||||
for category_name, category_values in actionsmap.items():
|
)
|
||||||
|
|
||||||
actions = category_values.pop("actions", {})
|
# action_name is like "list" of "domain list"
|
||||||
subcategories = category_values.pop("subcategories", {})
|
# action_options are the values
|
||||||
|
for action_name, action_options in actions.items():
|
||||||
|
arguments = action_options.pop("arguments", {})
|
||||||
|
authentication = action_options.pop("authentication", {})
|
||||||
|
tid = (self.namespace, category_name, action_name)
|
||||||
|
|
||||||
# Get category parser
|
# Get action parser
|
||||||
category_parser = top_parser.add_category_parser(
|
action_parser = category_parser.add_action_parser(
|
||||||
category_name, **category_values
|
action_name, tid, **action_options
|
||||||
)
|
)
|
||||||
|
|
||||||
# action_name is like "list" of "domain list"
|
if action_parser is None: # No parser for the action
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Store action identifier and add arguments
|
||||||
|
action_parser.set_defaults(_tid=tid)
|
||||||
|
action_parser.add_arguments(
|
||||||
|
arguments,
|
||||||
|
extraparser=self.extraparser,
|
||||||
|
format_arg_names=top_parser.format_arg_names,
|
||||||
|
validate_extra=validate_extra,
|
||||||
|
)
|
||||||
|
|
||||||
|
action_parser.authentication = self.default_authentication
|
||||||
|
if interface_type in authentication:
|
||||||
|
action_parser.authentication = authentication[interface_type]
|
||||||
|
|
||||||
|
# Disable the locking mechanism for all actions that are 'GET' actions on the api
|
||||||
|
routes = action_options.get("api")
|
||||||
|
routes = [routes] if isinstance(routes, str) else routes
|
||||||
|
if routes and all(route.startswith("GET ") for route in routes):
|
||||||
|
action_parser.want_to_take_lock = False
|
||||||
|
else:
|
||||||
|
action_parser.want_to_take_lock = True
|
||||||
|
|
||||||
|
# subcategory_name is like "cert" in "domain cert status"
|
||||||
|
# subcategory_values is the values of this subcategory (like actions)
|
||||||
|
for subcategory_name, subcategory_values in subcategories.items():
|
||||||
|
actions = subcategory_values.pop("actions")
|
||||||
|
|
||||||
|
# Get subcategory parser
|
||||||
|
subcategory_parser = category_parser.add_subcategory_parser(
|
||||||
|
subcategory_name, **subcategory_values
|
||||||
|
)
|
||||||
|
|
||||||
|
# action_name is like "status" of "domain cert status"
|
||||||
# action_options are the values
|
# action_options are the values
|
||||||
for action_name, action_options in actions.items():
|
for action_name, action_options in actions.items():
|
||||||
arguments = action_options.pop("arguments", {})
|
arguments = action_options.pop("arguments", {})
|
||||||
authentication = action_options.pop("authentication", {})
|
authentication = action_options.pop("authentication", {})
|
||||||
tid = (namespace, category_name, action_name)
|
tid = (self.namespace, category_name, subcategory_name, action_name)
|
||||||
|
|
||||||
# Get action parser
|
try:
|
||||||
action_parser = category_parser.add_action_parser(
|
# Get action parser
|
||||||
action_name, tid, **action_options
|
action_parser = subcategory_parser.add_action_parser(
|
||||||
)
|
action_name, tid, **action_options
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
# No parser for the action
|
||||||
|
continue
|
||||||
|
|
||||||
if action_parser is None: # No parser for the action
|
if action_parser is None: # No parser for the action
|
||||||
continue
|
continue
|
||||||
|
@ -721,50 +703,13 @@ class ActionsMap(object):
|
||||||
if interface_type in authentication:
|
if interface_type in authentication:
|
||||||
action_parser.authentication = authentication[interface_type]
|
action_parser.authentication = authentication[interface_type]
|
||||||
|
|
||||||
# subcategory_name is like "cert" in "domain cert status"
|
# Disable the locking mechanism for all actions that are 'GET' actions on the api
|
||||||
# subcategory_values is the values of this subcategory (like actions)
|
routes = action_options.get("api")
|
||||||
for subcategory_name, subcategory_values in subcategories.items():
|
routes = [routes] if isinstance(routes, str) else routes
|
||||||
|
if routes and all(route.startswith("GET ") for route in routes):
|
||||||
actions = subcategory_values.pop("actions")
|
action_parser.want_to_take_lock = False
|
||||||
|
else:
|
||||||
# Get subcategory parser
|
action_parser.want_to_take_lock = True
|
||||||
subcategory_parser = category_parser.add_subcategory_parser(
|
|
||||||
subcategory_name, **subcategory_values
|
|
||||||
)
|
|
||||||
|
|
||||||
# action_name is like "status" of "domain cert status"
|
|
||||||
# action_options are the values
|
|
||||||
for action_name, action_options in actions.items():
|
|
||||||
arguments = action_options.pop("arguments", {})
|
|
||||||
authentication = action_options.pop("authentication", {})
|
|
||||||
tid = (namespace, category_name, subcategory_name, action_name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Get action parser
|
|
||||||
action_parser = subcategory_parser.add_action_parser(
|
|
||||||
action_name, tid, **action_options
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
# No parser for the action
|
|
||||||
continue
|
|
||||||
|
|
||||||
if action_parser is None: # No parser for the action
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Store action identifier and add arguments
|
|
||||||
action_parser.set_defaults(_tid=tid)
|
|
||||||
action_parser.add_arguments(
|
|
||||||
arguments,
|
|
||||||
extraparser=self.extraparser,
|
|
||||||
format_arg_names=top_parser.format_arg_names,
|
|
||||||
validate_extra=validate_extra,
|
|
||||||
)
|
|
||||||
|
|
||||||
action_parser.authentication = self.default_authentication
|
|
||||||
if interface_type in authentication:
|
|
||||||
action_parser.authentication = authentication[
|
|
||||||
interface_type
|
|
||||||
]
|
|
||||||
|
|
||||||
logger.debug("building parser took %.3fs", time() - start)
|
logger.debug("building parser took %.3fs", time() - start)
|
||||||
return top_parser
|
return top_parser
|
||||||
|
|
|
@ -10,8 +10,7 @@ logger = logging.getLogger("moulinette.authenticator")
|
||||||
# Base Class -----------------------------------------------------------
|
# Base Class -----------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class BaseAuthenticator(object):
|
class BaseAuthenticator:
|
||||||
|
|
||||||
"""Authenticator base representation
|
"""Authenticator base representation
|
||||||
|
|
||||||
Each authenticators must implement an Authenticator class derived
|
Each authenticators must implement an Authenticator class derived
|
||||||
|
@ -29,7 +28,6 @@ class BaseAuthenticator(object):
|
||||||
# Each authenticator classes must implement these methods.
|
# Each authenticator classes must implement these methods.
|
||||||
|
|
||||||
def authenticate_credentials(self, credentials):
|
def authenticate_credentials(self, credentials):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Attempt to authenticate
|
# Attempt to authenticate
|
||||||
auth_info = self._authenticate_credentials(credentials) or {}
|
auth_info = self._authenticate_credentials(credentials) or {}
|
||||||
|
|
|
@ -9,19 +9,6 @@ import moulinette
|
||||||
|
|
||||||
logger = logging.getLogger("moulinette.core")
|
logger = logging.getLogger("moulinette.core")
|
||||||
|
|
||||||
env = {
|
|
||||||
"DATA_DIR": "/usr/share/moulinette",
|
|
||||||
"LIB_DIR": "/usr/lib/moulinette",
|
|
||||||
"LOCALES_DIR": "/usr/share/moulinette/locale",
|
|
||||||
"CACHE_DIR": "/var/cache/moulinette",
|
|
||||||
"NAMESPACES": "*", # By default we'll load every namespace we find
|
|
||||||
}
|
|
||||||
|
|
||||||
for key in env.keys():
|
|
||||||
value_from_environ = os.environ.get(f"MOULINETTE_{key}")
|
|
||||||
if value_from_environ:
|
|
||||||
env[key] = value_from_environ
|
|
||||||
|
|
||||||
|
|
||||||
def during_unittests_run():
|
def during_unittests_run():
|
||||||
return "TESTS_RUN" in os.environ
|
return "TESTS_RUN" in os.environ
|
||||||
|
@ -30,8 +17,7 @@ def during_unittests_run():
|
||||||
# Internationalization -------------------------------------------------
|
# Internationalization -------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class Translator(object):
|
class Translator:
|
||||||
|
|
||||||
"""Internationalization class
|
"""Internationalization class
|
||||||
|
|
||||||
Provide an internationalization mechanism based on JSON files to
|
Provide an internationalization mechanism based on JSON files to
|
||||||
|
@ -51,11 +37,7 @@ class Translator(object):
|
||||||
# Attempt to load default translations
|
# Attempt to load default translations
|
||||||
if not self._load_translations(default_locale):
|
if not self._load_translations(default_locale):
|
||||||
logger.error(
|
logger.error(
|
||||||
"unable to load locale '%s' from '%s'. Does the file '%s/%s.json' exists?",
|
f"unable to load locale '{default_locale}' from '{locale_dir}'. Does the file '{locale_dir}/{default_locale}.json' exists?",
|
||||||
default_locale,
|
|
||||||
locale_dir,
|
|
||||||
locale_dir,
|
|
||||||
default_locale,
|
|
||||||
)
|
)
|
||||||
self.default_locale = default_locale
|
self.default_locale = default_locale
|
||||||
|
|
||||||
|
@ -133,7 +115,6 @@ class Translator(object):
|
||||||
self.default_locale != self.locale
|
self.default_locale != self.locale
|
||||||
and key in self._translations.get(self.default_locale, {})
|
and key in self._translations.get(self.default_locale, {})
|
||||||
):
|
):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self._translations[self.default_locale][key].format(
|
return self._translations[self.default_locale][key].format(
|
||||||
*args, **kwargs
|
*args, **kwargs
|
||||||
|
@ -190,8 +171,7 @@ class Translator(object):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class Moulinette18n(object):
|
class Moulinette18n:
|
||||||
|
|
||||||
"""Internationalization service for the moulinette
|
"""Internationalization service for the moulinette
|
||||||
|
|
||||||
Manage internationalization and access to the proper keys translation
|
Manage internationalization and access to the proper keys translation
|
||||||
|
@ -207,44 +187,22 @@ class Moulinette18n(object):
|
||||||
self.default_locale = default_locale
|
self.default_locale = default_locale
|
||||||
self.locale = default_locale
|
self.locale = default_locale
|
||||||
|
|
||||||
self.locales_dir = env["LOCALES_DIR"]
|
|
||||||
|
|
||||||
# Init global translator
|
# Init global translator
|
||||||
self._global = Translator(self.locales_dir, default_locale)
|
global_locale_dir = "/usr/share/moulinette/locales"
|
||||||
|
if during_unittests_run():
|
||||||
|
global_locale_dir = os.path.dirname(__file__) + "/../locales"
|
||||||
|
|
||||||
# Define namespace related variables
|
self._global = Translator(global_locale_dir, default_locale)
|
||||||
self._namespaces = {}
|
|
||||||
self._current_namespace = None
|
|
||||||
|
|
||||||
def load_namespace(self, namespace):
|
def set_locales_dir(self, locales_dir):
|
||||||
"""Load the namespace to use
|
self.translator = Translator(locales_dir, self.default_locale)
|
||||||
|
|
||||||
Load and set translations of a given namespace. Those translations
|
|
||||||
are accessible with Moulinette18n.n().
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- namespace -- The namespace to load
|
|
||||||
|
|
||||||
"""
|
|
||||||
if namespace not in self._namespaces:
|
|
||||||
# Create new Translator object
|
|
||||||
lib_dir = env["LIB_DIR"]
|
|
||||||
translator = Translator(
|
|
||||||
"%s/%s/locales" % (lib_dir, namespace), self.default_locale
|
|
||||||
)
|
|
||||||
translator.set_locale(self.locale)
|
|
||||||
self._namespaces[namespace] = translator
|
|
||||||
|
|
||||||
# Set current namespace
|
|
||||||
self._current_namespace = namespace
|
|
||||||
|
|
||||||
def set_locale(self, locale):
|
def set_locale(self, locale):
|
||||||
"""Set the locale to use"""
|
"""Set the locale to use"""
|
||||||
self.locale = locale
|
|
||||||
|
|
||||||
|
self.locale = locale
|
||||||
self._global.set_locale(locale)
|
self._global.set_locale(locale)
|
||||||
for n in self._namespaces.values():
|
self.translator.set_locale(locale)
|
||||||
n.set_locale(locale)
|
|
||||||
|
|
||||||
def g(self, key: str, *args, **kwargs) -> str:
|
def g(self, key: str, *args, **kwargs) -> str:
|
||||||
"""Retrieve proper translation for a moulinette key
|
"""Retrieve proper translation for a moulinette key
|
||||||
|
@ -269,7 +227,7 @@ class Moulinette18n(object):
|
||||||
- key -- The key to translate
|
- key -- The key to translate
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self._namespaces[self._current_namespace].translate(key, *args, **kwargs)
|
return self.translator.translate(key, *args, **kwargs)
|
||||||
|
|
||||||
def key_exists(self, key: str) -> bool:
|
def key_exists(self, key: str) -> bool:
|
||||||
"""Check if a key exists in the translation files
|
"""Check if a key exists in the translation files
|
||||||
|
@ -278,14 +236,13 @@ class Moulinette18n(object):
|
||||||
- key -- The key to translate
|
- key -- The key to translate
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self._namespaces[self._current_namespace].key_exists(key)
|
return self.translator.key_exists(key)
|
||||||
|
|
||||||
|
|
||||||
# Moulinette core classes ----------------------------------------------
|
# Moulinette core classes ----------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class MoulinetteError(Exception):
|
class MoulinetteError(Exception):
|
||||||
|
|
||||||
http_code = 500
|
http_code = 500
|
||||||
|
|
||||||
"""Moulinette base exception"""
|
"""Moulinette base exception"""
|
||||||
|
@ -303,17 +260,14 @@ class MoulinetteError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class MoulinetteValidationError(MoulinetteError):
|
class MoulinetteValidationError(MoulinetteError):
|
||||||
|
|
||||||
http_code = 400
|
http_code = 400
|
||||||
|
|
||||||
|
|
||||||
class MoulinetteAuthenticationError(MoulinetteError):
|
class MoulinetteAuthenticationError(MoulinetteError):
|
||||||
|
|
||||||
http_code = 401
|
http_code = 401
|
||||||
|
|
||||||
|
|
||||||
class MoulinetteLock(object):
|
class MoulinetteLock:
|
||||||
|
|
||||||
"""Locker for a moulinette instance
|
"""Locker for a moulinette instance
|
||||||
|
|
||||||
It provides a lock mechanism for a given moulinette instance. It can
|
It provides a lock mechanism for a given moulinette instance. It can
|
||||||
|
@ -330,10 +284,11 @@ class MoulinetteLock(object):
|
||||||
|
|
||||||
base_lockfile = "/var/run/moulinette_%s.lock"
|
base_lockfile = "/var/run/moulinette_%s.lock"
|
||||||
|
|
||||||
def __init__(self, namespace, timeout=None, interval=0.5):
|
def __init__(self, namespace, timeout=None, enable_lock=True, interval=0.5):
|
||||||
self.namespace = namespace
|
self.namespace = namespace
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.interval = interval
|
self.interval = interval
|
||||||
|
self.enable_lock = enable_lock
|
||||||
|
|
||||||
self._lockfile = self.base_lockfile % namespace
|
self._lockfile = self.base_lockfile % namespace
|
||||||
self._stale_checked = False
|
self._stale_checked = False
|
||||||
|
@ -359,7 +314,6 @@ class MoulinetteLock(object):
|
||||||
logger.debug("acquiring lock...")
|
logger.debug("acquiring lock...")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
||||||
lock_pids = self._lock_PIDs()
|
lock_pids = self._lock_PIDs()
|
||||||
|
|
||||||
if self._is_son_of(lock_pids):
|
if self._is_son_of(lock_pids):
|
||||||
|
@ -427,7 +381,6 @@ class MoulinetteLock(object):
|
||||||
raise MoulinetteError("root_required")
|
raise MoulinetteError("root_required")
|
||||||
|
|
||||||
def _lock_PIDs(self):
|
def _lock_PIDs(self):
|
||||||
|
|
||||||
if not os.path.isfile(self._lockfile):
|
if not os.path.isfile(self._lockfile):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@ -460,7 +413,7 @@ class MoulinetteLock(object):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
if not self._locked:
|
if self.enable_lock and not self._locked:
|
||||||
self.acquire()
|
self.acquire()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
|
@ -5,25 +5,19 @@ import logging
|
||||||
import argparse
|
import argparse
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
from collections import deque, OrderedDict
|
from collections import OrderedDict
|
||||||
from json.encoder import JSONEncoder
|
from json.encoder import JSONEncoder
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from moulinette import m18n
|
from moulinette import m18n
|
||||||
from moulinette.core import MoulinetteError
|
|
||||||
|
|
||||||
logger = logging.getLogger("moulinette.interface")
|
logger = logging.getLogger("moulinette.interface")
|
||||||
|
|
||||||
# FIXME : are these even used for anything useful ...
|
|
||||||
TO_RETURN_PROP = "_to_return"
|
|
||||||
CALLBACKS_PROP = "_callbacks"
|
|
||||||
|
|
||||||
|
|
||||||
# Base Class -----------------------------------------------------------
|
# Base Class -----------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class BaseActionsMapParser(object):
|
class BaseActionsMapParser:
|
||||||
|
|
||||||
"""Actions map's base Parser
|
"""Actions map's base Parser
|
||||||
|
|
||||||
Each interfaces must implement an ActionsMapParser class derived
|
Each interfaces must implement an ActionsMapParser class derived
|
||||||
|
@ -148,98 +142,11 @@ class BaseActionsMapParser(object):
|
||||||
"derived class '%s' must override this method" % self.__class__.__name__
|
"derived class '%s' must override this method" % self.__class__.__name__
|
||||||
)
|
)
|
||||||
|
|
||||||
# Arguments helpers
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def prepare_action_namespace(tid, namespace=None):
|
|
||||||
"""Prepare the namespace for a given action"""
|
|
||||||
# Validate tid and namespace
|
|
||||||
if not isinstance(tid, tuple) and (
|
|
||||||
namespace is None or not hasattr(namespace, TO_RETURN_PROP)
|
|
||||||
):
|
|
||||||
raise MoulinetteError("invalid_usage")
|
|
||||||
elif not tid:
|
|
||||||
tid = "_global"
|
|
||||||
|
|
||||||
# Prepare namespace
|
|
||||||
if namespace is None:
|
|
||||||
namespace = argparse.Namespace()
|
|
||||||
namespace._tid = tid
|
|
||||||
|
|
||||||
return namespace
|
|
||||||
|
|
||||||
|
|
||||||
# Argument parser ------------------------------------------------------
|
# Argument parser ------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class _CallbackAction(argparse.Action):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
option_strings,
|
|
||||||
dest,
|
|
||||||
nargs=0,
|
|
||||||
callback={},
|
|
||||||
default=argparse.SUPPRESS,
|
|
||||||
help=None,
|
|
||||||
):
|
|
||||||
if not callback or "method" not in callback:
|
|
||||||
raise ValueError("callback must be provided with at least " "a method key")
|
|
||||||
super(_CallbackAction, self).__init__(
|
|
||||||
option_strings=option_strings,
|
|
||||||
dest=dest,
|
|
||||||
nargs=nargs,
|
|
||||||
default=default,
|
|
||||||
help=help,
|
|
||||||
)
|
|
||||||
self.callback_method = callback.get("method")
|
|
||||||
self.callback_kwargs = callback.get("kwargs", {})
|
|
||||||
self.callback_return = callback.get("return", False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def callback(self):
|
|
||||||
if not hasattr(self, "_callback"):
|
|
||||||
self._retrieve_callback()
|
|
||||||
return self._callback
|
|
||||||
|
|
||||||
def _retrieve_callback(self):
|
|
||||||
# Attempt to retrieve callback method
|
|
||||||
mod_name, func_name = (self.callback_method).rsplit(".", 1)
|
|
||||||
try:
|
|
||||||
mod = __import__(mod_name, globals=globals(), level=0, fromlist=[func_name])
|
|
||||||
func = getattr(mod, func_name)
|
|
||||||
except (AttributeError, ImportError):
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
traceback.print_exc()
|
|
||||||
raise ValueError("unable to import method {0}".format(self.callback_method))
|
|
||||||
self._callback = func
|
|
||||||
|
|
||||||
def __call__(self, parser, namespace, values, option_string=None):
|
|
||||||
parser.enqueue_callback(namespace, self, values)
|
|
||||||
if self.callback_return:
|
|
||||||
setattr(namespace, TO_RETURN_PROP, {})
|
|
||||||
|
|
||||||
def execute(self, namespace, values):
|
|
||||||
try:
|
|
||||||
# Execute callback and get returned value
|
|
||||||
value = self.callback(namespace, values, **self.callback_kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
error_message = (
|
|
||||||
"cannot get value from callback method "
|
|
||||||
"'{0}': {1}".format(self.callback_method, e)
|
|
||||||
)
|
|
||||||
logger.exception(error_message)
|
|
||||||
raise MoulinetteError(error_message, raw_msg=True)
|
|
||||||
else:
|
|
||||||
if value:
|
|
||||||
if self.callback_return:
|
|
||||||
setattr(namespace, TO_RETURN_PROP, value)
|
|
||||||
else:
|
|
||||||
setattr(namespace, self.dest, value)
|
|
||||||
|
|
||||||
|
|
||||||
class _ExtendedSubParsersAction(argparse._SubParsersAction):
|
class _ExtendedSubParsersAction(argparse._SubParsersAction):
|
||||||
|
|
||||||
"""Subparsers with extended properties for argparse
|
"""Subparsers with extended properties for argparse
|
||||||
|
|
||||||
It provides the following additional properties at initialization,
|
It provides the following additional properties at initialization,
|
||||||
|
@ -261,11 +168,14 @@ class _ExtendedSubParsersAction(argparse._SubParsersAction):
|
||||||
self._deprecated_command_map = {}
|
self._deprecated_command_map = {}
|
||||||
|
|
||||||
def add_parser(self, name, type_=None, **kwargs):
|
def add_parser(self, name, type_=None, **kwargs):
|
||||||
|
hide_in_help = kwargs.pop("hide_in_help", False)
|
||||||
deprecated = kwargs.pop("deprecated", False)
|
deprecated = kwargs.pop("deprecated", False)
|
||||||
deprecated_alias = kwargs.pop("deprecated_alias", [])
|
deprecated_alias = kwargs.pop("deprecated_alias", [])
|
||||||
|
|
||||||
if deprecated:
|
if deprecated:
|
||||||
self._deprecated_command_map[name] = None
|
self._deprecated_command_map[name] = None
|
||||||
|
|
||||||
|
if deprecated or hide_in_help:
|
||||||
if "help" in kwargs:
|
if "help" in kwargs:
|
||||||
del kwargs["help"]
|
del kwargs["help"]
|
||||||
|
|
||||||
|
@ -317,35 +227,8 @@ class ExtendedArgumentParser(argparse.ArgumentParser):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Register additional actions
|
# Register additional actions
|
||||||
self.register("action", "callback", _CallbackAction)
|
|
||||||
self.register("action", "parsers", _ExtendedSubParsersAction)
|
self.register("action", "parsers", _ExtendedSubParsersAction)
|
||||||
|
|
||||||
def enqueue_callback(self, namespace, callback, values):
|
|
||||||
queue = self._get_callbacks_queue(namespace)
|
|
||||||
queue.append((callback, values))
|
|
||||||
|
|
||||||
def dequeue_callbacks(self, namespace):
|
|
||||||
queue = self._get_callbacks_queue(namespace, False)
|
|
||||||
for _i in range(len(queue)):
|
|
||||||
c, v = queue.popleft()
|
|
||||||
# FIXME: break dequeue if callback returns
|
|
||||||
c.execute(namespace, v)
|
|
||||||
try:
|
|
||||||
delattr(namespace, CALLBACKS_PROP)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _get_callbacks_queue(self, namespace, create=True):
|
|
||||||
try:
|
|
||||||
queue = getattr(namespace, CALLBACKS_PROP)
|
|
||||||
except AttributeError:
|
|
||||||
if create:
|
|
||||||
queue = deque()
|
|
||||||
setattr(namespace, CALLBACKS_PROP, queue)
|
|
||||||
else:
|
|
||||||
queue = list()
|
|
||||||
return queue
|
|
||||||
|
|
||||||
def add_arguments(
|
def add_arguments(
|
||||||
self, arguments, extraparser, format_arg_names=None, validate_extra=True
|
self, arguments, extraparser, format_arg_names=None, validate_extra=True
|
||||||
):
|
):
|
||||||
|
@ -398,11 +281,9 @@ class ExtendedArgumentParser(argparse.ArgumentParser):
|
||||||
|
|
||||||
# positionals, optionals and user-defined groups
|
# positionals, optionals and user-defined groups
|
||||||
for action_group in self._action_groups:
|
for action_group in self._action_groups:
|
||||||
|
|
||||||
# Dirty hack to separate 'subcommands'
|
# Dirty hack to separate 'subcommands'
|
||||||
# into 'actions' and 'subcategories'
|
# into 'actions' and 'subcategories'
|
||||||
if action_group.title == "subcommands":
|
if action_group.title == "subcommands":
|
||||||
|
|
||||||
# Make a copy of the "action group actions"...
|
# Make a copy of the "action group actions"...
|
||||||
choice_actions = action_group._group_actions[0]._choices_actions
|
choice_actions = action_group._group_actions[0]._choices_actions
|
||||||
actions_subparser = copy.copy(action_group._group_actions[0])
|
actions_subparser = copy.copy(action_group._group_actions[0])
|
||||||
|
@ -502,7 +383,6 @@ class PositionalsFirstHelpFormatter(argparse.HelpFormatter):
|
||||||
# wrap the usage parts if it's too long
|
# wrap the usage parts if it's too long
|
||||||
text_width = self._width - self._current_indent
|
text_width = self._width - self._current_indent
|
||||||
if len(prefix) + len(usage) > text_width:
|
if len(prefix) + len(usage) > text_width:
|
||||||
|
|
||||||
# break usage into wrappable parts
|
# break usage into wrappable parts
|
||||||
part_regexp = r"\(.*?\)+|\[.*?\]+|\S+"
|
part_regexp = r"\(.*?\)+|\[.*?\]+|\S+"
|
||||||
opt_usage = format(optionals, groups)
|
opt_usage = format(optionals, groups)
|
||||||
|
@ -562,11 +442,10 @@ class PositionalsFirstHelpFormatter(argparse.HelpFormatter):
|
||||||
usage = "\n".join(lines)
|
usage = "\n".join(lines)
|
||||||
|
|
||||||
# prefix with 'usage:'
|
# prefix with 'usage:'
|
||||||
return "%s%s\n\n" % (prefix, usage)
|
return "{}{}\n\n".format(prefix, usage)
|
||||||
|
|
||||||
|
|
||||||
class JSONExtendedEncoder(JSONEncoder):
|
class JSONExtendedEncoder(JSONEncoder):
|
||||||
|
|
||||||
"""Extended JSON encoder
|
"""Extended JSON encoder
|
||||||
|
|
||||||
Extend default JSON encoder to recognize more types and classes. It will
|
Extend default JSON encoder to recognize more types and classes. It will
|
||||||
|
@ -579,7 +458,6 @@ class JSONExtendedEncoder(JSONEncoder):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def default(self, o):
|
def default(self, o):
|
||||||
|
|
||||||
import pytz # Lazy loading, this takes like 3+ sec on a RPi2 ?!
|
import pytz # Lazy loading, this takes like 3+ sec on a RPi2 ?!
|
||||||
|
|
||||||
"""Return a serializable object"""
|
"""Return a serializable object"""
|
||||||
|
|
|
@ -29,7 +29,6 @@ from moulinette.interfaces import (
|
||||||
JSONExtendedEncoder,
|
JSONExtendedEncoder,
|
||||||
)
|
)
|
||||||
from moulinette.utils import log
|
from moulinette.utils import log
|
||||||
from moulinette.utils.text import random_ascii
|
|
||||||
|
|
||||||
logger = log.getLogger("moulinette.interface.api")
|
logger = log.getLogger("moulinette.interface.api")
|
||||||
|
|
||||||
|
@ -38,9 +37,7 @@ logger = log.getLogger("moulinette.interface.api")
|
||||||
# We define a global variable to manage in a dirty way the upload...
|
# We define a global variable to manage in a dirty way the upload...
|
||||||
UPLOAD_DIR = None
|
UPLOAD_DIR = None
|
||||||
|
|
||||||
CSRF_TYPES = set(
|
CSRF_TYPES = {"text/plain", "application/x-www-form-urlencoded", "multipart/form-data"}
|
||||||
["text/plain", "application/x-www-form-urlencoded", "multipart/form-data"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def is_csrf():
|
def is_csrf():
|
||||||
|
@ -69,14 +66,12 @@ def filter_csrf(callback):
|
||||||
|
|
||||||
|
|
||||||
class LogQueues(dict):
|
class LogQueues(dict):
|
||||||
|
|
||||||
"""Map of session ids to queue."""
|
"""Map of session ids to queue."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class APIQueueHandler(logging.Handler):
|
class APIQueueHandler(logging.Handler):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
A handler class which store logging records into a queue, to be used
|
A handler class which store logging records into a queue, to be used
|
||||||
and retrieved from the API.
|
and retrieved from the API.
|
||||||
|
@ -85,9 +80,19 @@ class APIQueueHandler(logging.Handler):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
logging.Handler.__init__(self)
|
logging.Handler.__init__(self)
|
||||||
self.queues = LogQueues()
|
self.queues = LogQueues()
|
||||||
|
# actionsmap is actually set during the interface's init ...
|
||||||
|
self.actionsmap = None
|
||||||
|
|
||||||
def emit(self, record):
|
def emit(self, record):
|
||||||
s_id = Session.get_infos(raise_if_no_session_exists=False)["id"]
|
# Prevent triggering this function while moulinette
|
||||||
|
# is being initialized with --debug
|
||||||
|
if not self.actionsmap or len(request.cookies) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
profile = request.params.get("profile", self.actionsmap.default_authentication)
|
||||||
|
authenticator = self.actionsmap.get_authenticator(profile)
|
||||||
|
|
||||||
|
s_id = authenticator.get_session_cookie(raise_if_no_session_exists=False)["id"]
|
||||||
try:
|
try:
|
||||||
queue = self.queues[s_id]
|
queue = self.queues[s_id]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -101,8 +106,7 @@ class APIQueueHandler(logging.Handler):
|
||||||
sleep(0)
|
sleep(0)
|
||||||
|
|
||||||
|
|
||||||
class _HTTPArgumentParser(object):
|
class _HTTPArgumentParser:
|
||||||
|
|
||||||
"""Argument parser for HTTP requests
|
"""Argument parser for HTTP requests
|
||||||
|
|
||||||
Object for parsing HTTP requests into Python objects. It is based
|
Object for parsing HTTP requests into Python objects. It is based
|
||||||
|
@ -227,56 +231,11 @@ class _HTTPArgumentParser(object):
|
||||||
|
|
||||||
return self._parser.parse_args(arg_strings, namespace)
|
return self._parser.parse_args(arg_strings, namespace)
|
||||||
|
|
||||||
def dequeue_callbacks(self, *args, **kwargs):
|
|
||||||
return self._parser.dequeue_callbacks(*args, **kwargs)
|
|
||||||
|
|
||||||
def _error(self, message):
|
def _error(self, message):
|
||||||
raise MoulinetteValidationError(message, raw_msg=True)
|
raise MoulinetteValidationError(message, raw_msg=True)
|
||||||
|
|
||||||
|
|
||||||
class Session:
|
class _ActionsMapPlugin:
|
||||||
|
|
||||||
secret = random_ascii()
|
|
||||||
actionsmap_name = None # This is later set to the actionsmap name
|
|
||||||
|
|
||||||
def set_infos(infos):
|
|
||||||
|
|
||||||
assert isinstance(infos, dict)
|
|
||||||
|
|
||||||
response.set_cookie(
|
|
||||||
f"session.{Session.actionsmap_name}",
|
|
||||||
infos,
|
|
||||||
secure=True,
|
|
||||||
secret=Session.secret,
|
|
||||||
httponly=True,
|
|
||||||
# samesite="strict", # Bottle 0.12 doesn't support samesite, to be added in next versions
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_infos(raise_if_no_session_exists=True):
|
|
||||||
|
|
||||||
try:
|
|
||||||
infos = request.get_cookie(
|
|
||||||
f"session.{Session.actionsmap_name}", secret=Session.secret, default={}
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
if not raise_if_no_session_exists:
|
|
||||||
return {"id": random_ascii()}
|
|
||||||
raise MoulinetteAuthenticationError("unable_authenticate")
|
|
||||||
|
|
||||||
if "id" not in infos:
|
|
||||||
infos["id"] = random_ascii()
|
|
||||||
|
|
||||||
return infos
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def delete_infos():
|
|
||||||
|
|
||||||
response.set_cookie(f"session.{Session.actionsmap_name}", "", max_age=-1)
|
|
||||||
response.delete_cookie(f"session.{Session.actionsmap_name}")
|
|
||||||
|
|
||||||
|
|
||||||
class _ActionsMapPlugin(object):
|
|
||||||
|
|
||||||
"""Actions map Bottle Plugin
|
"""Actions map Bottle Plugin
|
||||||
|
|
||||||
Process relevant action for the request using the actions map and
|
Process relevant action for the request using the actions map and
|
||||||
|
@ -291,10 +250,8 @@ class _ActionsMapPlugin(object):
|
||||||
api = 2
|
api = 2
|
||||||
|
|
||||||
def __init__(self, actionsmap, log_queues={}):
|
def __init__(self, actionsmap, log_queues={}):
|
||||||
|
|
||||||
self.actionsmap = actionsmap
|
self.actionsmap = actionsmap
|
||||||
self.log_queues = log_queues
|
self.log_queues = log_queues
|
||||||
Session.actionsmap_name = actionsmap.name
|
|
||||||
|
|
||||||
def setup(self, app):
|
def setup(self, app):
|
||||||
"""Setup plugin on the application
|
"""Setup plugin on the application
|
||||||
|
@ -331,7 +288,7 @@ class _ActionsMapPlugin(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Append routes from the actions map
|
# Append routes from the actions map
|
||||||
for (m, p) in self.actionsmap.parser.routes:
|
for m, p in self.actionsmap.parser.routes:
|
||||||
app.route(p, method=m, callback=self.process)
|
app.route(p, method=m, callback=self.process)
|
||||||
|
|
||||||
def apply(self, callback, context):
|
def apply(self, callback, context):
|
||||||
|
@ -358,7 +315,7 @@ class _ActionsMapPlugin(object):
|
||||||
params[a] = True
|
params[a] = True
|
||||||
|
|
||||||
# Append other request params
|
# Append other request params
|
||||||
req_params = list(request.params.decode().dict.items())
|
req_params = list(request.params.dict.items())
|
||||||
# TODO test special chars in filename
|
# TODO test special chars in filename
|
||||||
req_params += list(request.files.dict.items())
|
req_params += list(request.files.dict.items())
|
||||||
for k, v in req_params:
|
for k, v in req_params:
|
||||||
|
@ -393,21 +350,15 @@ class _ActionsMapPlugin(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
credentials = request.POST.credentials
|
if "credentials" not in request.params:
|
||||||
# Apparently even if the key doesn't exists, request.POST.foobar just returns empty string...
|
|
||||||
if not credentials:
|
|
||||||
raise HTTPResponse("Missing credentials parameter", 400)
|
raise HTTPResponse("Missing credentials parameter", 400)
|
||||||
|
credentials = request.params["credentials"]
|
||||||
|
|
||||||
profile = request.POST.profile
|
profile = request.params.get("profile", self.actionsmap.default_authentication)
|
||||||
if not profile:
|
|
||||||
profile = self.actionsmap.default_authentication
|
|
||||||
|
|
||||||
authenticator = self.actionsmap.get_authenticator(profile)
|
authenticator = self.actionsmap.get_authenticator(profile)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
auth_info = authenticator.authenticate_credentials(credentials)
|
auth_infos = authenticator.authenticate_credentials(credentials)
|
||||||
session_infos = Session.get_infos(raise_if_no_session_exists=False)
|
|
||||||
session_infos[profile] = auth_info
|
|
||||||
except MoulinetteError as e:
|
except MoulinetteError as e:
|
||||||
try:
|
try:
|
||||||
self.logout()
|
self.logout()
|
||||||
|
@ -415,18 +366,13 @@ class _ActionsMapPlugin(object):
|
||||||
pass
|
pass
|
||||||
raise HTTPResponse(e.strerror, 401)
|
raise HTTPResponse(e.strerror, 401)
|
||||||
else:
|
else:
|
||||||
Session.set_infos(session_infos)
|
authenticator.set_session_cookie(auth_infos)
|
||||||
return m18n.g("logged_in")
|
return m18n.g("logged_in")
|
||||||
|
|
||||||
# This is called before each time a route is going to be processed
|
# This is called before each time a route is going to be processed
|
||||||
def authenticate(self, authenticator):
|
def authenticate(self, authenticator):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
session_infos = Session.get_infos()[authenticator.name]
|
session_infos = authenticator.get_session_cookie()
|
||||||
|
|
||||||
# Here, maybe we want to re-authenticate the session via the authenticator
|
|
||||||
# For example to check that the username authenticated is still in the admin group...
|
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
msg = m18n.g("authentication_required")
|
msg = m18n.g("authentication_required")
|
||||||
raise HTTPResponse(msg, 401)
|
raise HTTPResponse(msg, 401)
|
||||||
|
@ -434,13 +380,16 @@ class _ActionsMapPlugin(object):
|
||||||
return session_infos
|
return session_infos
|
||||||
|
|
||||||
def logout(self):
|
def logout(self):
|
||||||
|
profile = request.params.get("profile", self.actionsmap.default_authentication)
|
||||||
|
authenticator = self.actionsmap.get_authenticator(profile)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
Session.get_infos()
|
authenticator.get_session_cookie()
|
||||||
except KeyError:
|
except Exception:
|
||||||
raise HTTPResponse(m18n.g("not_logged_in"), 401)
|
raise HTTPResponse(m18n.g("not_logged_in"), 401)
|
||||||
else:
|
else:
|
||||||
# Delete cookie and clean the session
|
# Delete cookie and clean the session
|
||||||
Session.delete_infos()
|
authenticator.delete_session_cookie()
|
||||||
return m18n.g("logged_out")
|
return m18n.g("logged_out")
|
||||||
|
|
||||||
def messages(self):
|
def messages(self):
|
||||||
|
@ -449,7 +398,11 @@ class _ActionsMapPlugin(object):
|
||||||
Retrieve the WebSocket stream and send to it each messages displayed by
|
Retrieve the WebSocket stream and send to it each messages displayed by
|
||||||
the display method. They are JSON encoded as a dict { style: message }.
|
the display method. They are JSON encoded as a dict { style: message }.
|
||||||
"""
|
"""
|
||||||
s_id = Session.get_infos()["id"]
|
|
||||||
|
profile = request.params.get("profile", self.actionsmap.default_authentication)
|
||||||
|
authenticator = self.actionsmap.get_authenticator(profile)
|
||||||
|
|
||||||
|
s_id = authenticator.get_session_cookie()["id"]
|
||||||
try:
|
try:
|
||||||
queue = self.log_queues[s_id]
|
queue = self.log_queues[s_id]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -508,7 +461,6 @@ class _ActionsMapPlugin(object):
|
||||||
else:
|
else:
|
||||||
return format_for_response(ret)
|
return format_for_response(ret)
|
||||||
finally:
|
finally:
|
||||||
|
|
||||||
# Clean upload directory
|
# Clean upload directory
|
||||||
# FIXME do that in a better way
|
# FIXME do that in a better way
|
||||||
global UPLOAD_DIR
|
global UPLOAD_DIR
|
||||||
|
@ -517,17 +469,25 @@ class _ActionsMapPlugin(object):
|
||||||
UPLOAD_DIR = None
|
UPLOAD_DIR = None
|
||||||
|
|
||||||
# Close opened WebSocket by putting StopIteration in the queue
|
# Close opened WebSocket by putting StopIteration in the queue
|
||||||
|
profile = request.params.get(
|
||||||
|
"profile", self.actionsmap.default_authentication
|
||||||
|
)
|
||||||
|
authenticator = self.actionsmap.get_authenticator(profile)
|
||||||
try:
|
try:
|
||||||
s_id = Session.get_infos()["id"]
|
s_id = authenticator.get_session_cookie()["id"]
|
||||||
queue = self.log_queues[s_id]
|
queue = self.log_queues[s_id]
|
||||||
|
except MoulinetteAuthenticationError:
|
||||||
|
pass
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
queue.put(StopIteration)
|
queue.put(StopIteration)
|
||||||
|
|
||||||
def display(self, message, style="info"):
|
def display(self, message, style="info"):
|
||||||
|
profile = request.params.get("profile", self.actionsmap.default_authentication)
|
||||||
|
authenticator = self.actionsmap.get_authenticator(profile)
|
||||||
|
s_id = authenticator.get_session_cookie(raise_if_no_session_exists=False)["id"]
|
||||||
|
|
||||||
s_id = Session.get_infos(raise_if_no_session_exists=False)["id"]
|
|
||||||
try:
|
try:
|
||||||
queue = self.log_queues[s_id]
|
queue = self.log_queues[s_id]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -548,7 +508,6 @@ class _ActionsMapPlugin(object):
|
||||||
|
|
||||||
|
|
||||||
def moulinette_error_to_http_response(error):
|
def moulinette_error_to_http_response(error):
|
||||||
|
|
||||||
content = error.content()
|
content = error.content()
|
||||||
if isinstance(content, dict):
|
if isinstance(content, dict):
|
||||||
return HTTPResponse(
|
return HTTPResponse(
|
||||||
|
@ -585,7 +544,6 @@ def format_for_response(content):
|
||||||
|
|
||||||
|
|
||||||
class ActionsMapParser(BaseActionsMapParser):
|
class ActionsMapParser(BaseActionsMapParser):
|
||||||
|
|
||||||
"""Actions map's Parser for the API
|
"""Actions map's Parser for the API
|
||||||
|
|
||||||
Provide actions map parsing methods for a CLI usage. The parser for
|
Provide actions map parsing methods for a CLI usage. The parser for
|
||||||
|
@ -665,36 +623,44 @@ class ActionsMapParser(BaseActionsMapParser):
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def auth_method(self, _, route):
|
def auth_method(self, _, route):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Retrieve the tid for the route
|
# Retrieve the tid for the route
|
||||||
_, parser = self._parsers[route]
|
_, parser = self._parsers[route]
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
error_message = "no argument parser found for route '%s': %s" % (route, e)
|
error_message = "no argument parser found for route '{}': {}".format(
|
||||||
|
route, e
|
||||||
|
)
|
||||||
logger.error(error_message)
|
logger.error(error_message)
|
||||||
raise MoulinetteValidationError(error_message, raw_msg=True)
|
raise MoulinetteValidationError(error_message, raw_msg=True)
|
||||||
|
|
||||||
return parser.authentication
|
return parser.authentication
|
||||||
|
|
||||||
def parse_args(self, args, route, **kwargs):
|
def want_to_take_lock(self, _, route):
|
||||||
|
_, parser = self._parsers[route]
|
||||||
|
|
||||||
|
return getattr(parser, "want_to_take_lock", True)
|
||||||
|
|
||||||
|
def parse_args(self, args, **kwargs):
|
||||||
"""Parse arguments
|
"""Parse arguments
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
- route -- The action route as a 2-tuple (method, path)
|
- route -- The action route as a 2-tuple (method, path)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
route = kwargs["route"]
|
||||||
try:
|
try:
|
||||||
# Retrieve the parser for the route
|
# Retrieve the parser for the route
|
||||||
_, parser = self._parsers[route]
|
_, parser = self._parsers[route]
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
error_message = "no argument parser found for route '%s': %s" % (route, e)
|
error_message = "no argument parser found for route '{}': {}".format(
|
||||||
|
route, e
|
||||||
|
)
|
||||||
logger.error(error_message)
|
logger.error(error_message)
|
||||||
raise MoulinetteValidationError(error_message, raw_msg=True)
|
raise MoulinetteValidationError(error_message, raw_msg=True)
|
||||||
ret = argparse.Namespace()
|
ret = argparse.Namespace()
|
||||||
|
|
||||||
# TODO: Catch errors?
|
# TODO: Catch errors?
|
||||||
ret = parser.parse_args(args, ret)
|
ret = parser.parse_args(args, ret)
|
||||||
parser.dequeue_callbacks(ret)
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
# Private methods
|
# Private methods
|
||||||
|
@ -721,7 +687,6 @@ class ActionsMapParser(BaseActionsMapParser):
|
||||||
|
|
||||||
|
|
||||||
class Interface:
|
class Interface:
|
||||||
|
|
||||||
"""Application Programming Interface for the moulinette
|
"""Application Programming Interface for the moulinette
|
||||||
|
|
||||||
Initialize a HTTP server which serves the API connected to a given
|
Initialize a HTTP server which serves the API connected to a given
|
||||||
|
@ -737,14 +702,14 @@ class Interface:
|
||||||
|
|
||||||
type = "api"
|
type = "api"
|
||||||
|
|
||||||
def __init__(self, routes={}):
|
def __init__(self, routes={}, actionsmap=None):
|
||||||
|
actionsmap = ActionsMap(actionsmap, ActionsMapParser())
|
||||||
actionsmap = ActionsMap(ActionsMapParser())
|
|
||||||
|
|
||||||
# Attempt to retrieve log queues from an APIQueueHandler
|
# Attempt to retrieve log queues from an APIQueueHandler
|
||||||
handler = log.getHandlersByClass(APIQueueHandler, limit=1)
|
handler = log.getHandlersByClass(APIQueueHandler, limit=1)
|
||||||
if handler:
|
if handler:
|
||||||
log_queues = handler.queues
|
log_queues = handler.queues
|
||||||
|
handler.actionsmap = actionsmap
|
||||||
|
|
||||||
# TODO: Return OK to 'OPTIONS' xhr requests (l173)
|
# TODO: Return OK to 'OPTIONS' xhr requests (l173)
|
||||||
app = Bottle(autojson=True)
|
app = Bottle(autojson=True)
|
||||||
|
|
|
@ -2,12 +2,10 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import getpass
|
|
||||||
import locale
|
import locale
|
||||||
import logging
|
import logging
|
||||||
import argparse
|
import argparse
|
||||||
import tempfile
|
import tempfile
|
||||||
from readline import insert_text, set_startup_hook
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from subprocess import call
|
from subprocess import call
|
||||||
|
@ -210,7 +208,6 @@ def get_locale():
|
||||||
|
|
||||||
|
|
||||||
class TTYHandler(logging.StreamHandler):
|
class TTYHandler(logging.StreamHandler):
|
||||||
|
|
||||||
"""TTY log handler
|
"""TTY log handler
|
||||||
|
|
||||||
A handler class which prints logging records for a tty. The record is
|
A handler class which prints logging records for a tty. The record is
|
||||||
|
@ -253,7 +250,7 @@ class TTYHandler(logging.StreamHandler):
|
||||||
# add translated level name before message
|
# add translated level name before message
|
||||||
level = "%s " % m18n.g(record.levelname.lower())
|
level = "%s " % m18n.g(record.levelname.lower())
|
||||||
color = self.LEVELS_COLOR.get(record.levelno, "white")
|
color = self.LEVELS_COLOR.get(record.levelno, "white")
|
||||||
msg = "{0}{1}{2}{3}".format(colors_codes[color], level, END_CLI_COLOR, msg)
|
msg = "{}{}{}{}".format(colors_codes[color], level, END_CLI_COLOR, msg)
|
||||||
if self.formatter:
|
if self.formatter:
|
||||||
# use user-defined formatter
|
# use user-defined formatter
|
||||||
record.__dict__[self.message_key] = msg
|
record.__dict__[self.message_key] = msg
|
||||||
|
@ -276,7 +273,6 @@ class TTYHandler(logging.StreamHandler):
|
||||||
|
|
||||||
|
|
||||||
class ActionsMapParser(BaseActionsMapParser):
|
class ActionsMapParser(BaseActionsMapParser):
|
||||||
|
|
||||||
"""Actions map's Parser for the CLI
|
"""Actions map's Parser for the CLI
|
||||||
|
|
||||||
Provide actions map parsing methods for a CLI usage. The parser for
|
Provide actions map parsing methods for a CLI usage. The parser for
|
||||||
|
@ -291,13 +287,12 @@ class ActionsMapParser(BaseActionsMapParser):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, parent=None, parser=None, subparser_kwargs=None, top_parser=None, **kwargs
|
self, parent=None, parser=None, subparser_kwargs=None, top_parser=None
|
||||||
):
|
):
|
||||||
super(ActionsMapParser, self).__init__(parent)
|
super(ActionsMapParser, self).__init__(parent)
|
||||||
|
|
||||||
if subparser_kwargs is None:
|
if subparser_kwargs is None:
|
||||||
subparser_kwargs = {"title": "categories", "required": False}
|
subparser_kwargs = {"title": "categories", "required": False}
|
||||||
|
|
||||||
self._parser = parser or ExtendedArgumentParser()
|
self._parser = parser or ExtendedArgumentParser()
|
||||||
self._subparsers = self._parser.add_subparsers(**subparser_kwargs)
|
self._subparsers = self._parser.add_subparsers(**subparser_kwargs)
|
||||||
self.global_parser = parent.global_parser if parent else None
|
self.global_parser = parent.global_parser if parent else None
|
||||||
|
@ -338,7 +333,11 @@ class ActionsMapParser(BaseActionsMapParser):
|
||||||
parser = self._subparsers.add_parser(
|
parser = self._subparsers.add_parser(
|
||||||
name, description=category_help, help=category_help, **kwargs
|
name, description=category_help, help=category_help, **kwargs
|
||||||
)
|
)
|
||||||
return self.__class__(self, parser, {"title": "subcommands", "required": True})
|
return self.__class__(
|
||||||
|
parent=self,
|
||||||
|
parser=parser,
|
||||||
|
subparser_kwargs={"title": "subcommands", "required": True},
|
||||||
|
)
|
||||||
|
|
||||||
def add_subcategory_parser(self, name, subcategory_help=None, **kwargs):
|
def add_subcategory_parser(self, name, subcategory_help=None, **kwargs):
|
||||||
"""Add a parser for a subcategory
|
"""Add a parser for a subcategory
|
||||||
|
@ -357,7 +356,11 @@ class ActionsMapParser(BaseActionsMapParser):
|
||||||
help=subcategory_help,
|
help=subcategory_help,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
return self.__class__(self, parser, {"title": "actions", "required": True})
|
return self.__class__(
|
||||||
|
parent=self,
|
||||||
|
parser=parser,
|
||||||
|
subparser_kwargs={"title": "actions", "required": True},
|
||||||
|
)
|
||||||
|
|
||||||
def add_action_parser(
|
def add_action_parser(
|
||||||
self,
|
self,
|
||||||
|
@ -366,6 +369,7 @@ class ActionsMapParser(BaseActionsMapParser):
|
||||||
action_help=None,
|
action_help=None,
|
||||||
deprecated=False,
|
deprecated=False,
|
||||||
deprecated_alias=[],
|
deprecated_alias=[],
|
||||||
|
hide_in_help=False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""Add a parser for an action
|
"""Add a parser for an action
|
||||||
|
@ -386,37 +390,12 @@ class ActionsMapParser(BaseActionsMapParser):
|
||||||
description=action_help,
|
description=action_help,
|
||||||
deprecated=deprecated,
|
deprecated=deprecated,
|
||||||
deprecated_alias=deprecated_alias,
|
deprecated_alias=deprecated_alias,
|
||||||
|
hide_in_help=hide_in_help,
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_global_arguments(self, arguments):
|
|
||||||
for argument_name, argument_options in arguments.items():
|
|
||||||
# will adapt arguments name for cli or api context
|
|
||||||
names = self.format_arg_names(
|
|
||||||
str(argument_name), argument_options.pop("full", None)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.global_parser.add_argument(*names, **argument_options)
|
|
||||||
|
|
||||||
def auth_method(self, args):
|
def auth_method(self, args):
|
||||||
# FIXME? idk .. this try/except is duplicated from parse_args below
|
ret = self.parse_args(args)
|
||||||
# Just to be able to obtain the tid
|
tid = getattr(ret, "_tid", [])
|
||||||
try:
|
|
||||||
ret = self._parser.parse_args(args)
|
|
||||||
except SystemExit:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
error_message = "unable to parse arguments '%s' because: %s" % (
|
|
||||||
" ".join(args),
|
|
||||||
e,
|
|
||||||
)
|
|
||||||
logger.exception(error_message)
|
|
||||||
raise MoulinetteValidationError(error_message, raw_msg=True)
|
|
||||||
|
|
||||||
tid = getattr(ret, "_tid", None)
|
|
||||||
|
|
||||||
# Ugh that's for yunohost --version ...
|
|
||||||
if tid is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# We go down in the subparser tree until we find the leaf
|
# We go down in the subparser tree until we find the leaf
|
||||||
# corresponding to the tid with a defined authentication
|
# corresponding to the tid with a defined authentication
|
||||||
|
@ -429,28 +408,42 @@ class ActionsMapParser(BaseActionsMapParser):
|
||||||
else:
|
else:
|
||||||
_p = _p._actions[1]
|
_p = _p._actions[1]
|
||||||
|
|
||||||
|
if tid == []:
|
||||||
|
return None
|
||||||
|
|
||||||
raise MoulinetteError(f"Authentication undefined for {tid} ?", raw_msg=True)
|
raise MoulinetteError(f"Authentication undefined for {tid} ?", raw_msg=True)
|
||||||
|
|
||||||
def parse_args(self, args, **kwargs):
|
def parse_args(self, args, **kwargs):
|
||||||
try:
|
try:
|
||||||
ret = self._parser.parse_args(args)
|
return self._parser.parse_args(args)
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_message = "unable to parse arguments '%s' because: %s" % (
|
error_message = "unable to parse arguments '{}' because: {}".format(
|
||||||
" ".join(args),
|
" ".join(args),
|
||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
logger.exception(error_message)
|
logger.exception(error_message)
|
||||||
raise MoulinetteValidationError(error_message, raw_msg=True)
|
raise MoulinetteValidationError(error_message, raw_msg=True)
|
||||||
else:
|
|
||||||
self.prepare_action_namespace(getattr(ret, "_tid", None), ret)
|
def want_to_take_lock(self, args):
|
||||||
self._parser.dequeue_callbacks(ret)
|
ret = self.parse_args(args)
|
||||||
return ret
|
tid = getattr(ret, "_tid", [])
|
||||||
|
if len(tid) == 3:
|
||||||
|
_p = self._subparsers.choices[tid[1]]._actions[1].choices[tid[2]]
|
||||||
|
elif len(tid) == 4:
|
||||||
|
_p = (
|
||||||
|
self._subparsers.choices[tid[1]]
|
||||||
|
._actions[1]
|
||||||
|
.choices[tid[2]]
|
||||||
|
._actions[1]
|
||||||
|
.choices[tid[3]]
|
||||||
|
)
|
||||||
|
|
||||||
|
return getattr(_p, "want_to_take_lock", True)
|
||||||
|
|
||||||
|
|
||||||
class Interface:
|
class Interface:
|
||||||
|
|
||||||
"""Command-line Interface for the moulinette
|
"""Command-line Interface for the moulinette
|
||||||
|
|
||||||
Initialize an interface connected to the standard input/output
|
Initialize an interface connected to the standard input/output
|
||||||
|
@ -463,12 +456,18 @@ class Interface:
|
||||||
|
|
||||||
type = "cli"
|
type = "cli"
|
||||||
|
|
||||||
def __init__(self, top_parser=None, load_only_category=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
top_parser=None,
|
||||||
|
load_only_category=None,
|
||||||
|
actionsmap=None,
|
||||||
|
locales_dir=None,
|
||||||
|
):
|
||||||
# Set user locale
|
# Set user locale
|
||||||
m18n.set_locale(get_locale())
|
m18n.set_locale(get_locale())
|
||||||
|
|
||||||
self.actionsmap = ActionsMap(
|
self.actionsmap = ActionsMap(
|
||||||
|
actionsmap,
|
||||||
ActionsMapParser(top_parser=top_parser),
|
ActionsMapParser(top_parser=top_parser),
|
||||||
load_only_category=load_only_category,
|
load_only_category=load_only_category,
|
||||||
)
|
)
|
||||||
|
@ -494,6 +493,9 @@ class Interface:
|
||||||
if output_as and output_as not in ["json", "plain", "none"]:
|
if output_as and output_as not in ["json", "plain", "none"]:
|
||||||
raise MoulinetteValidationError("invalid_usage")
|
raise MoulinetteValidationError("invalid_usage")
|
||||||
|
|
||||||
|
if not args:
|
||||||
|
raise MoulinetteValidationError("invalid_usage")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ret = self.actionsmap.process(args, timeout=timeout)
|
ret = self.actionsmap.process(args, timeout=timeout)
|
||||||
except (KeyboardInterrupt, EOFError):
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
@ -533,6 +535,8 @@ class Interface:
|
||||||
color="blue",
|
color="blue",
|
||||||
prefill="",
|
prefill="",
|
||||||
is_multiline=False,
|
is_multiline=False,
|
||||||
|
autocomplete=[],
|
||||||
|
help=None,
|
||||||
):
|
):
|
||||||
"""Prompt for a value
|
"""Prompt for a value
|
||||||
|
|
||||||
|
@ -546,17 +550,42 @@ class Interface:
|
||||||
)
|
)
|
||||||
|
|
||||||
def _prompt(message):
|
def _prompt(message):
|
||||||
|
if not is_multiline:
|
||||||
|
import prompt_toolkit
|
||||||
|
from prompt_toolkit.completion import WordCompleter
|
||||||
|
from prompt_toolkit.styles import Style
|
||||||
|
|
||||||
|
autocomplete_ = WordCompleter(autocomplete)
|
||||||
|
style = Style.from_dict(
|
||||||
|
{
|
||||||
|
"": "",
|
||||||
|
"message": f"#ansi{color} bold",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if help:
|
||||||
|
|
||||||
|
def bottom_toolbar():
|
||||||
|
return [("class:", help)]
|
||||||
|
|
||||||
|
else:
|
||||||
|
bottom_toolbar = None
|
||||||
|
|
||||||
|
colored_message = [
|
||||||
|
("class:message", message),
|
||||||
|
("class:", ": "),
|
||||||
|
]
|
||||||
|
|
||||||
|
return prompt_toolkit.prompt(
|
||||||
|
colored_message,
|
||||||
|
bottom_toolbar=bottom_toolbar,
|
||||||
|
style=style,
|
||||||
|
default=prefill,
|
||||||
|
completer=autocomplete_,
|
||||||
|
complete_while_typing=True,
|
||||||
|
is_password=is_password,
|
||||||
|
)
|
||||||
|
|
||||||
if is_password:
|
|
||||||
return getpass.getpass(colorize(m18n.g("colon", message), color))
|
|
||||||
elif not is_multiline:
|
|
||||||
print(colorize(m18n.g("colon", message), color), end="")
|
|
||||||
set_startup_hook(lambda: insert_text(prefill))
|
|
||||||
try:
|
|
||||||
value = input()
|
|
||||||
finally:
|
|
||||||
set_startup_hook()
|
|
||||||
return value
|
|
||||||
else:
|
else:
|
||||||
while True:
|
while True:
|
||||||
value = input(
|
value = input(
|
||||||
|
|
|
@ -15,7 +15,7 @@ from moulinette.core import MoulinetteError
|
||||||
# Files & directories --------------------------------------------------
|
# Files & directories --------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def read_file(file_path):
|
def read_file(file_path, file_mode="r"):
|
||||||
"""
|
"""
|
||||||
Read a regular text file
|
Read a regular text file
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ def read_file(file_path):
|
||||||
"""
|
"""
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
file_path, str
|
file_path, str
|
||||||
), "Error: file_path '%s' should be a string but is of type '%s' instead" % (
|
), "Error: file_path '{}' should be a string but is of type '{}' instead".format(
|
||||||
file_path,
|
file_path,
|
||||||
type(file_path),
|
type(file_path),
|
||||||
)
|
)
|
||||||
|
@ -35,7 +35,7 @@ def read_file(file_path):
|
||||||
|
|
||||||
# Open file and read content
|
# Open file and read content
|
||||||
try:
|
try:
|
||||||
with open(file_path, "r") as f:
|
with open(file_path, file_mode) as f:
|
||||||
file_content = f.read()
|
file_content = f.read()
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
raise MoulinetteError("cannot_open_file", file=file_path, error=str(e))
|
raise MoulinetteError("cannot_open_file", file=file_path, error=str(e))
|
||||||
|
@ -121,7 +121,7 @@ def write_to_file(file_path, data, file_mode="w"):
|
||||||
"""
|
"""
|
||||||
assert (
|
assert (
|
||||||
isinstance(data, str) or isinstance(data, bytes) or isinstance(data, list)
|
isinstance(data, str) or isinstance(data, bytes) or isinstance(data, list)
|
||||||
), "Error: data '%s' should be either a string or a list but is of type '%s'" % (
|
), "Error: data '{}' should be either a string or a list but is of type '{}'".format(
|
||||||
data,
|
data,
|
||||||
type(data),
|
type(data),
|
||||||
)
|
)
|
||||||
|
@ -130,7 +130,7 @@ def write_to_file(file_path, data, file_mode="w"):
|
||||||
)
|
)
|
||||||
assert os.path.isdir(
|
assert os.path.isdir(
|
||||||
os.path.dirname(file_path)
|
os.path.dirname(file_path)
|
||||||
), "Error: the path ('%s') base dir ('%s') is not a dir" % (
|
), "Error: the path ('{}') base dir ('{}') is not a dir".format(
|
||||||
file_path,
|
file_path,
|
||||||
os.path.dirname(file_path),
|
os.path.dirname(file_path),
|
||||||
)
|
)
|
||||||
|
@ -140,7 +140,7 @@ def write_to_file(file_path, data, file_mode="w"):
|
||||||
for element in data:
|
for element in data:
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
element, str
|
element, str
|
||||||
), "Error: element '%s' should be a string but is of type '%s' instead" % (
|
), "Error: element '{}' should be a string but is of type '{}' instead".format(
|
||||||
element,
|
element,
|
||||||
type(element),
|
type(element),
|
||||||
)
|
)
|
||||||
|
@ -179,13 +179,13 @@ def write_to_json(file_path, data, sort_keys=False, indent=None):
|
||||||
# Assumptions
|
# Assumptions
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
file_path, str
|
file_path, str
|
||||||
), "Error: file_path '%s' should be a string but is of type '%s' instead" % (
|
), "Error: file_path '{}' should be a string but is of type '{}' instead".format(
|
||||||
file_path,
|
file_path,
|
||||||
type(file_path),
|
type(file_path),
|
||||||
)
|
)
|
||||||
assert isinstance(data, dict) or isinstance(
|
assert isinstance(data, dict) or isinstance(
|
||||||
data, list
|
data, list
|
||||||
), "Error: data '%s' should be a dict or a list but is of type '%s' instead" % (
|
), "Error: data '{}' should be a dict or a list but is of type '{}' instead".format(
|
||||||
data,
|
data,
|
||||||
type(data),
|
type(data),
|
||||||
)
|
)
|
||||||
|
@ -194,7 +194,7 @@ def write_to_json(file_path, data, sort_keys=False, indent=None):
|
||||||
)
|
)
|
||||||
assert os.path.isdir(
|
assert os.path.isdir(
|
||||||
os.path.dirname(file_path)
|
os.path.dirname(file_path)
|
||||||
), "Error: the path ('%s') base dir ('%s') is not a dir" % (
|
), "Error: the path ('{}') base dir ('{}') is not a dir".format(
|
||||||
file_path,
|
file_path,
|
||||||
os.path.dirname(file_path),
|
os.path.dirname(file_path),
|
||||||
)
|
)
|
||||||
|
@ -365,3 +365,10 @@ def rm(path, recursive=False, force=False):
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if not force:
|
if not force:
|
||||||
raise MoulinetteError("error_removing", path=path, error=str(e))
|
raise MoulinetteError("error_removing", path=path, error=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def cp(source, dest, recursive=False, **kwargs):
|
||||||
|
if recursive and os.path.isdir(source):
|
||||||
|
return shutil.copytree(source, dest, symlinks=True, **kwargs)
|
||||||
|
else:
|
||||||
|
return shutil.copy2(source, dest, follow_symlinks=False, **kwargs)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import os
|
import os
|
||||||
import logging
|
|
||||||
|
|
||||||
# import all constants because other modules try to import them from this
|
# import all constants because other modules try to import them from this
|
||||||
# module because SUCCESS is defined in this module
|
# module because SUCCESS is defined in this module
|
||||||
|
@ -70,8 +69,11 @@ def configure_logging(logging_config=None):
|
||||||
|
|
||||||
def getHandlersByClass(classinfo, limit=0):
|
def getHandlersByClass(classinfo, limit=0):
|
||||||
"""Retrieve registered handlers of a given class."""
|
"""Retrieve registered handlers of a given class."""
|
||||||
|
|
||||||
|
from logging import _handlers
|
||||||
|
|
||||||
handlers = []
|
handlers = []
|
||||||
for ref in logging._handlers.itervaluerefs():
|
for ref in _handlers.itervaluerefs():
|
||||||
o = ref()
|
o = ref()
|
||||||
if o is not None and isinstance(o, classinfo):
|
if o is not None and isinstance(o, classinfo):
|
||||||
if limit == 1:
|
if limit == 1:
|
||||||
|
@ -83,7 +85,6 @@ def getHandlersByClass(classinfo, limit=0):
|
||||||
|
|
||||||
|
|
||||||
class MoulinetteLogger(Logger):
|
class MoulinetteLogger(Logger):
|
||||||
|
|
||||||
"""Custom logger class
|
"""Custom logger class
|
||||||
|
|
||||||
Extend base Logger class to provide the SUCCESS custom log level with
|
Extend base Logger class to provide the SUCCESS custom log level with
|
||||||
|
@ -102,14 +103,17 @@ class MoulinetteLogger(Logger):
|
||||||
|
|
||||||
def findCaller(self, *args):
|
def findCaller(self, *args):
|
||||||
"""Override findCaller method to consider this source file."""
|
"""Override findCaller method to consider this source file."""
|
||||||
f = logging.currentframe()
|
|
||||||
|
from logging import currentframe, _srcfile
|
||||||
|
|
||||||
|
f = currentframe()
|
||||||
if f is not None:
|
if f is not None:
|
||||||
f = f.f_back
|
f = f.f_back
|
||||||
rv = "(unknown file)", 0, "(unknown function)"
|
rv = "(unknown file)", 0, "(unknown function)"
|
||||||
while hasattr(f, "f_code"):
|
while hasattr(f, "f_code"):
|
||||||
co = f.f_code
|
co = f.f_code
|
||||||
filename = os.path.normcase(co.co_filename)
|
filename = os.path.normcase(co.co_filename)
|
||||||
if filename == logging._srcfile or filename == __file__:
|
if filename == _srcfile or filename == __file__:
|
||||||
f = f.f_back
|
f = f.f_back
|
||||||
continue
|
continue
|
||||||
rv = (co.co_filename, f.f_lineno, co.co_name)
|
rv = (co.co_filename, f.f_lineno, co.co_name)
|
||||||
|
@ -167,8 +171,7 @@ def getActionLogger(name=None, logger=None, action_id=None):
|
||||||
return logger
|
return logger
|
||||||
|
|
||||||
|
|
||||||
class ActionFilter(object):
|
class ActionFilter:
|
||||||
|
|
||||||
"""Extend log record for an optionnal action
|
"""Extend log record for an optionnal action
|
||||||
|
|
||||||
Filter a given record and look for an `action_id` key. If it is not found
|
Filter a given record and look for an `action_id` key. If it is not found
|
||||||
|
|
|
@ -80,7 +80,6 @@ def call_async_output(args, callback, **kwargs):
|
||||||
p = subprocess.Popen(args, **kwargs)
|
p = subprocess.Popen(args, **kwargs)
|
||||||
|
|
||||||
while p.poll() is None:
|
while p.poll() is None:
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
callback, message = log_queue.get(True, 1)
|
callback, message = log_queue.get(True, 1)
|
||||||
|
@ -201,7 +200,6 @@ def run_commands(cmds, callback=None, separate_stderr=False, shell=True, **kwarg
|
||||||
# Iterate over commands
|
# Iterate over commands
|
||||||
error = 0
|
error = 0
|
||||||
for cmd in cmds:
|
for cmd in cmds:
|
||||||
|
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
cmd, stdout=subprocess.PIPE, stderr=_stderr, shell=shell, **kwargs
|
cmd, stdout=subprocess.PIPE, stderr=_stderr, shell=shell, **kwargs
|
||||||
)
|
)
|
||||||
|
|
|
@ -59,12 +59,12 @@ def searchf(pattern, path, count=0, flags=re.MULTILINE):
|
||||||
def prependlines(text, prepend):
|
def prependlines(text, prepend):
|
||||||
"""Prepend a string to each line of a text"""
|
"""Prepend a string to each line of a text"""
|
||||||
lines = text.splitlines(True)
|
lines = text.splitlines(True)
|
||||||
return "%s%s" % (prepend, prepend.join(lines))
|
return "{}{}".format(prepend, prepend.join(lines))
|
||||||
|
|
||||||
|
|
||||||
# Randomize ------------------------------------------------------------
|
# Randomize ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def random_ascii(length=20):
|
def random_ascii(length=40):
|
||||||
"""Return a random ascii string"""
|
"""Return a random ascii string"""
|
||||||
return binascii.hexlify(os.urandom(length)).decode("ascii")
|
return binascii.hexlify(os.urandom(length)).decode("ascii")[:length]
|
||||||
|
|
|
@ -3,5 +3,4 @@ addopts = --cov=moulinette -s -v --no-cov-on-fail
|
||||||
norecursedirs = dist doc build .tox .eggs
|
norecursedirs = dist doc build .tox .eggs
|
||||||
testpaths = test/
|
testpaths = test/
|
||||||
env =
|
env =
|
||||||
MOULINETTE_LOCALES_DIR = {PWD}/locales
|
|
||||||
TESTS_RUN = True
|
TESTS_RUN = True
|
||||||
|
|
|
@ -4,5 +4,7 @@ ignore =
|
||||||
E128,
|
E128,
|
||||||
E731,
|
E731,
|
||||||
E722,
|
E722,
|
||||||
W503 # Black formatter conflict
|
# Black formatter conflict
|
||||||
E203 # Black formatter conflict
|
W503,
|
||||||
|
# Black formatter conflict
|
||||||
|
E203
|
24
setup.py
24
setup.py
|
@ -2,11 +2,17 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
from moulinette import env
|
|
||||||
|
|
||||||
|
version = (
|
||||||
LOCALES_DIR = env["LOCALES_DIR"]
|
subprocess.check_output(
|
||||||
|
"head debian/changelog -n1 | awk '{print $2}' | tr -d '()'", shell=True
|
||||||
|
)
|
||||||
|
.decode()
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
|
||||||
# Extend installation
|
# Extend installation
|
||||||
locale_files = []
|
locale_files = []
|
||||||
|
@ -24,6 +30,8 @@ install_deps = [
|
||||||
"toml",
|
"toml",
|
||||||
"gevent-websocket",
|
"gevent-websocket",
|
||||||
"bottle",
|
"bottle",
|
||||||
|
"prompt-toolkit>=3.0",
|
||||||
|
"pygments",
|
||||||
]
|
]
|
||||||
|
|
||||||
test_deps = [
|
test_deps = [
|
||||||
|
@ -36,23 +44,23 @@ test_deps = [
|
||||||
"requests-mock",
|
"requests-mock",
|
||||||
"webtest",
|
"webtest",
|
||||||
]
|
]
|
||||||
|
|
||||||
extras = {
|
extras = {
|
||||||
"install": install_deps,
|
"install": install_deps,
|
||||||
"tests": test_deps,
|
"tests": test_deps,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="Moulinette",
|
name="Moulinette",
|
||||||
version="2.0.0",
|
version=version,
|
||||||
description="Prototype interfaces quickly and easily",
|
description="Prototype interfaces quickly and easily",
|
||||||
author="Yunohost Team",
|
author="Yunohost Team",
|
||||||
author_email="yunohost@yunohost.org",
|
author_email="yunohost@yunohost.org",
|
||||||
url="http://yunohost.org",
|
url="https://yunohost.org",
|
||||||
license="AGPL",
|
license="AGPL",
|
||||||
packages=find_packages(exclude=["test"]),
|
packages=find_packages(exclude=["test"]),
|
||||||
data_files=[(LOCALES_DIR, locale_files)],
|
data_files=[("/usr/share/moulinette/locales", locale_files)],
|
||||||
python_requires=">=3.7.*, <3.8",
|
python_requires=">=3.7.0,<3.10",
|
||||||
install_requires=install_deps,
|
install_requires=install_deps,
|
||||||
tests_require=test_deps,
|
tests_require=test_deps,
|
||||||
extras_require=extras,
|
extras_require=extras,
|
||||||
|
|
|
@ -3,25 +3,10 @@
|
||||||
# Global parameters #
|
# Global parameters #
|
||||||
#############################
|
#############################
|
||||||
_global:
|
_global:
|
||||||
name: moulitest
|
namespace: moulitest
|
||||||
authentication:
|
authentication:
|
||||||
api: dummy
|
api: dummy
|
||||||
cli: dummy
|
cli: dummy
|
||||||
arguments:
|
|
||||||
-v:
|
|
||||||
full: --version
|
|
||||||
help: Display Yoloswag versions
|
|
||||||
action: callback
|
|
||||||
callback:
|
|
||||||
method: test.src.testauth.yoloswag_version
|
|
||||||
return: true
|
|
||||||
-w:
|
|
||||||
full: --wersion
|
|
||||||
help: Not existing function
|
|
||||||
action: callback
|
|
||||||
callback:
|
|
||||||
method: test.src.testauth.not_existing_function
|
|
||||||
return: true
|
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
# Test Actions #
|
# Test Actions #
|
||||||
|
|
|
@ -12,13 +12,11 @@ reference = json.loads(open(locale_folder + "en.json").read())
|
||||||
|
|
||||||
|
|
||||||
def fix_locale(locale_file):
|
def fix_locale(locale_file):
|
||||||
|
|
||||||
this_locale = json.loads(open(locale_folder + locale_file).read())
|
this_locale = json.loads(open(locale_folder + locale_file).read())
|
||||||
fixed_stuff = False
|
fixed_stuff = False
|
||||||
|
|
||||||
# We iterate over all keys/string in en.json
|
# We iterate over all keys/string in en.json
|
||||||
for key, string in reference.items():
|
for key, string in reference.items():
|
||||||
|
|
||||||
# Ignore check if there's no translation yet for this key
|
# Ignore check if there's no translation yet for this key
|
||||||
if key not in this_locale:
|
if key not in this_locale:
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -1,24 +1,15 @@
|
||||||
"""Pytest fixtures for testing."""
|
"""Pytest fixtures for testing."""
|
||||||
|
|
||||||
|
import sys
|
||||||
import toml
|
import toml
|
||||||
import yaml
|
import yaml
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import shutil
|
import shutil
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def patch_init(moulinette):
|
|
||||||
"""Configure moulinette to use the YunoHost namespace."""
|
|
||||||
old_init = moulinette.core.Moulinette18n.__init__
|
|
||||||
|
|
||||||
def monkey_path_i18n_init(self, package, default_locale="en"):
|
|
||||||
old_init(self, package, default_locale)
|
|
||||||
self.load_namespace("moulinette")
|
|
||||||
|
|
||||||
moulinette.core.Moulinette18n.__init__ = monkey_path_i18n_init
|
|
||||||
|
|
||||||
|
|
||||||
def patch_translate(moulinette):
|
def patch_translate(moulinette):
|
||||||
"""Configure translator to raise errors when there are missing keys."""
|
"""Configure translator to raise errors when there are missing keys."""
|
||||||
old_translate = moulinette.core.Translator.translate
|
old_translate = moulinette.core.Translator.translate
|
||||||
|
@ -38,9 +29,9 @@ def patch_translate(moulinette):
|
||||||
moulinette.core.Moulinette18n.g = new_m18nn
|
moulinette.core.Moulinette18n.g = new_m18nn
|
||||||
|
|
||||||
|
|
||||||
def patch_logging(moulinette):
|
def logging_configuration(moulinette):
|
||||||
"""Configure logging to use the custom logger."""
|
"""Configure logging to use the custom logger."""
|
||||||
handlers = set(["tty", "api"])
|
handlers = {"tty", "api"}
|
||||||
root_handlers = set(handlers)
|
root_handlers = set(handlers)
|
||||||
|
|
||||||
level = "INFO"
|
level = "INFO"
|
||||||
|
@ -86,33 +77,32 @@ def patch_lock(moulinette):
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
def moulinette(tmp_path_factory):
|
def moulinette(tmp_path_factory):
|
||||||
import moulinette
|
import moulinette
|
||||||
|
import moulinette.core
|
||||||
|
from moulinette.utils.log import configure_logging
|
||||||
|
|
||||||
# Can't call the namespace just 'test' because
|
# Can't call the namespace just 'test' because
|
||||||
# that would lead to some "import test" not importing the right stuff
|
# that would lead to some "import test" not importing the right stuff
|
||||||
namespace = "moulitest"
|
namespace = "moulitest"
|
||||||
tmp_cache = str(tmp_path_factory.mktemp("cache"))
|
tmp_dir = str(tmp_path_factory.mktemp(namespace))
|
||||||
tmp_data = str(tmp_path_factory.mktemp("data"))
|
shutil.copy("./test/actionsmap/moulitest.yml", f"{tmp_dir}/moulitest.yml")
|
||||||
tmp_lib = str(tmp_path_factory.mktemp("lib"))
|
shutil.copytree("./test/src", f"{tmp_dir}/lib/{namespace}/")
|
||||||
moulinette.env["CACHE_DIR"] = tmp_cache
|
shutil.copytree("./test/locales", f"{tmp_dir}/locales")
|
||||||
moulinette.env["DATA_DIR"] = tmp_data
|
sys.path.insert(0, f"{tmp_dir}/lib")
|
||||||
moulinette.env["LIB_DIR"] = tmp_lib
|
|
||||||
shutil.copytree("./test/actionsmap", "%s/actionsmap" % tmp_data)
|
|
||||||
shutil.copytree("./test/src", "%s/%s" % (tmp_lib, namespace))
|
|
||||||
shutil.copytree("./test/locales", "%s/%s/locales" % (tmp_lib, namespace))
|
|
||||||
|
|
||||||
patch_init(moulinette)
|
|
||||||
patch_translate(moulinette)
|
patch_translate(moulinette)
|
||||||
patch_lock(moulinette)
|
patch_lock(moulinette)
|
||||||
logging = patch_logging(moulinette)
|
|
||||||
|
|
||||||
moulinette.init(logging_config=logging, _from_source=False)
|
configure_logging(logging_configuration(moulinette))
|
||||||
|
moulinette.m18n.set_locales_dir(f"{tmp_dir}/locales")
|
||||||
|
|
||||||
|
# Dirty hack to pass this path to Api() and Cli() init later
|
||||||
|
moulinette._actionsmap_path = f"{tmp_dir}/moulitest.yml"
|
||||||
|
|
||||||
return moulinette
|
return moulinette
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def moulinette_webapi(moulinette):
|
def moulinette_webapi(moulinette):
|
||||||
|
|
||||||
from webtest import TestApp
|
from webtest import TestApp
|
||||||
from webtest.app import CookiePolicy
|
from webtest.app import CookiePolicy
|
||||||
|
|
||||||
|
@ -125,7 +115,7 @@ def moulinette_webapi(moulinette):
|
||||||
|
|
||||||
from moulinette.interfaces.api import Interface as Api
|
from moulinette.interfaces.api import Interface as Api
|
||||||
|
|
||||||
return TestApp(Api(routes={})._app)
|
return TestApp(Api(routes={}, actionsmap=moulinette._actionsmap_path)._app)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -142,7 +132,7 @@ def moulinette_cli(moulinette, mocker):
|
||||||
mocker.patch("os.isatty", return_value=True)
|
mocker.patch("os.isatty", return_value=True)
|
||||||
from moulinette.interfaces.cli import Interface as Cli
|
from moulinette.interfaces.cli import Interface as Cli
|
||||||
|
|
||||||
cli = Cli(top_parser=parser)
|
cli = Cli(top_parser=parser, actionsmap=moulinette._actionsmap_path)
|
||||||
mocker.stopall()
|
mocker.stopall()
|
||||||
|
|
||||||
return cli
|
return cli
|
||||||
|
|
|
@ -2,7 +2,6 @@ import re
|
||||||
|
|
||||||
|
|
||||||
def reformat(lang, transformations):
|
def reformat(lang, transformations):
|
||||||
|
|
||||||
locale = open(f"locales/{lang}.json").read()
|
locale = open(f"locales/{lang}.json").read()
|
||||||
for pattern, replace in transformations.items():
|
for pattern, replace in transformations.items():
|
||||||
locale = re.compile(pattern).sub(replace, locale)
|
locale = re.compile(pattern).sub(replace, locale)
|
||||||
|
@ -25,8 +24,8 @@ godamn_spaces_of_hell = [
|
||||||
"\u2008",
|
"\u2008",
|
||||||
"\u2009",
|
"\u2009",
|
||||||
"\u200A",
|
"\u200A",
|
||||||
"\u202f",
|
# "\u202f",
|
||||||
"\u202F",
|
# "\u202F",
|
||||||
"\u3000",
|
"\u3000",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ locale_files.remove("en.json")
|
||||||
reference = json.loads(open(locale_folder + "en.json").read())
|
reference = json.loads(open(locale_folder + "en.json").read())
|
||||||
|
|
||||||
for locale_file in locale_files:
|
for locale_file in locale_files:
|
||||||
|
|
||||||
print(locale_file)
|
print(locale_file)
|
||||||
this_locale = json.loads(
|
this_locale = json.loads(
|
||||||
open(locale_folder + locale_file).read(), object_pairs_hook=OrderedDict
|
open(locale_folder + locale_file).read(), object_pairs_hook=OrderedDict
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from moulinette.core import MoulinetteError
|
from moulinette.utils.text import random_ascii
|
||||||
|
from moulinette.core import MoulinetteError, MoulinetteAuthenticationError
|
||||||
from moulinette.authentication import BaseAuthenticator
|
from moulinette.authentication import BaseAuthenticator
|
||||||
|
|
||||||
logger = logging.getLogger("moulinette.authenticator.dummy")
|
logger = logging.getLogger("moulinette.authenticator.yoloswag")
|
||||||
|
|
||||||
# Dummy authenticator implementation
|
# Dummy authenticator implementation
|
||||||
|
|
||||||
|
session_secret = random_ascii()
|
||||||
|
|
||||||
|
|
||||||
class Authenticator(BaseAuthenticator):
|
class Authenticator(BaseAuthenticator):
|
||||||
|
|
||||||
"""Dummy authenticator used for tests"""
|
"""Dummy authenticator used for tests"""
|
||||||
|
|
||||||
name = "dummy"
|
name = "dummy"
|
||||||
|
@ -19,8 +21,50 @@ class Authenticator(BaseAuthenticator):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _authenticate_credentials(self, credentials=None):
|
def _authenticate_credentials(self, credentials=None):
|
||||||
|
|
||||||
if not credentials == self.name:
|
if not credentials == self.name:
|
||||||
raise MoulinetteError("invalid_password", raw_msg=True)
|
raise MoulinetteError("invalid_password", raw_msg=True)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def set_session_cookie(self, infos):
|
||||||
|
from bottle import response
|
||||||
|
|
||||||
|
assert isinstance(infos, dict)
|
||||||
|
|
||||||
|
# This allows to generate a new session id or keep the existing one
|
||||||
|
current_infos = self.get_session_cookie(raise_if_no_session_exists=False)
|
||||||
|
new_infos = {"id": current_infos["id"]}
|
||||||
|
new_infos.update(infos)
|
||||||
|
|
||||||
|
response.set_cookie(
|
||||||
|
"moulitest",
|
||||||
|
new_infos,
|
||||||
|
secure=True,
|
||||||
|
secret=session_secret,
|
||||||
|
httponly=True,
|
||||||
|
# samesite="strict", # Bottle 0.12 doesn't support samesite, to be added in next versions
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_session_cookie(self, raise_if_no_session_exists=True):
|
||||||
|
from bottle import request
|
||||||
|
|
||||||
|
try:
|
||||||
|
infos = request.get_cookie("moulitest", secret=session_secret, default={})
|
||||||
|
except Exception:
|
||||||
|
if not raise_if_no_session_exists:
|
||||||
|
return {"id": random_ascii()}
|
||||||
|
raise MoulinetteAuthenticationError("unable_authenticate")
|
||||||
|
|
||||||
|
if not infos and raise_if_no_session_exists:
|
||||||
|
raise MoulinetteAuthenticationError("unable_authenticate")
|
||||||
|
|
||||||
|
if "id" not in infos:
|
||||||
|
infos["id"] = random_ascii()
|
||||||
|
|
||||||
|
return infos
|
||||||
|
|
||||||
|
def delete_session_cookie(self):
|
||||||
|
from bottle import response
|
||||||
|
|
||||||
|
response.set_cookie("moulitest", "", max_age=-1)
|
||||||
|
response.delete_cookie("moulitest")
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from moulinette.core import MoulinetteError
|
from moulinette.utils.text import random_ascii
|
||||||
|
from moulinette.core import MoulinetteError, MoulinetteAuthenticationError
|
||||||
from moulinette.authentication import BaseAuthenticator
|
from moulinette.authentication import BaseAuthenticator
|
||||||
|
|
||||||
logger = logging.getLogger("moulinette.authenticator.yoloswag")
|
logger = logging.getLogger("moulinette.authenticator.yoloswag")
|
||||||
|
|
||||||
# Dummy authenticator implementation
|
# Dummy authenticator implementation
|
||||||
|
|
||||||
|
session_secret = random_ascii()
|
||||||
|
|
||||||
|
|
||||||
class Authenticator(BaseAuthenticator):
|
class Authenticator(BaseAuthenticator):
|
||||||
|
|
||||||
"""Dummy authenticator used for tests"""
|
"""Dummy authenticator used for tests"""
|
||||||
|
|
||||||
name = "yoloswag"
|
name = "yoloswag"
|
||||||
|
@ -19,8 +21,50 @@ class Authenticator(BaseAuthenticator):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _authenticate_credentials(self, credentials=None):
|
def _authenticate_credentials(self, credentials=None):
|
||||||
|
|
||||||
if not credentials == self.name:
|
if not credentials == self.name:
|
||||||
raise MoulinetteError("invalid_password", raw_msg=True)
|
raise MoulinetteError("invalid_password", raw_msg=True)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def set_session_cookie(self, infos):
|
||||||
|
from bottle import response
|
||||||
|
|
||||||
|
assert isinstance(infos, dict)
|
||||||
|
|
||||||
|
# This allows to generate a new session id or keep the existing one
|
||||||
|
current_infos = self.get_session_cookie(raise_if_no_session_exists=False)
|
||||||
|
new_infos = {"id": current_infos["id"]}
|
||||||
|
new_infos.update(infos)
|
||||||
|
|
||||||
|
response.set_cookie(
|
||||||
|
"moulitest",
|
||||||
|
new_infos,
|
||||||
|
secure=True,
|
||||||
|
secret=session_secret,
|
||||||
|
httponly=True,
|
||||||
|
# samesite="strict", # Bottle 0.12 doesn't support samesite, to be added in next versions
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_session_cookie(self, raise_if_no_session_exists=True):
|
||||||
|
from bottle import request
|
||||||
|
|
||||||
|
try:
|
||||||
|
infos = request.get_cookie("moulitest", secret=session_secret, default={})
|
||||||
|
except Exception:
|
||||||
|
if not raise_if_no_session_exists:
|
||||||
|
return {"id": random_ascii()}
|
||||||
|
raise MoulinetteAuthenticationError("unable_authenticate")
|
||||||
|
|
||||||
|
if not infos and raise_if_no_session_exists:
|
||||||
|
raise MoulinetteAuthenticationError("unable_authenticate")
|
||||||
|
|
||||||
|
if "id" not in infos:
|
||||||
|
infos["id"] = random_ascii()
|
||||||
|
|
||||||
|
return infos
|
||||||
|
|
||||||
|
def delete_session_cookie(self):
|
||||||
|
from bottle import response
|
||||||
|
|
||||||
|
response.set_cookie("moulitest", "", max_age=-1)
|
||||||
|
response.delete_cookie("moulitest")
|
||||||
|
|
|
@ -44,7 +44,3 @@ def testauth_with_extra_str_only(only_a_str):
|
||||||
|
|
||||||
def testauth_with_type_int(only_an_int):
|
def testauth_with_type_int(only_an_int):
|
||||||
return only_an_int
|
return only_an_int
|
||||||
|
|
||||||
|
|
||||||
def yoloswag_version(*args, **kwargs):
|
|
||||||
return "666"
|
|
||||||
|
|
|
@ -161,10 +161,9 @@ def test_required_paremeter_missing_value(iface, caplog):
|
||||||
|
|
||||||
|
|
||||||
def test_actions_map_unknown_authenticator(monkeypatch, tmp_path):
|
def test_actions_map_unknown_authenticator(monkeypatch, tmp_path):
|
||||||
|
|
||||||
from moulinette.interfaces.api import ActionsMapParser
|
from moulinette.interfaces.api import ActionsMapParser
|
||||||
|
|
||||||
amap = ActionsMap(ActionsMapParser())
|
amap = ActionsMap("test/actionsmap/moulitest.yml", ActionsMapParser())
|
||||||
|
|
||||||
with pytest.raises(MoulinetteError) as exception:
|
with pytest.raises(MoulinetteError) as exception:
|
||||||
amap.get_authenticator("unknown")
|
amap.get_authenticator("unknown")
|
||||||
|
@ -192,10 +191,12 @@ def test_extra_argument_parser_add_argument_bad_arg(iface):
|
||||||
with pytest.raises(MoulinetteError) as exception:
|
with pytest.raises(MoulinetteError) as exception:
|
||||||
extra_argument_parse.add_argument("_global", "foo", {"ask": 1})
|
extra_argument_parse.add_argument("_global", "foo", {"ask": 1})
|
||||||
|
|
||||||
expected_msg = "unable to validate extra parameter '%s' for argument '%s': %s" % (
|
expected_msg = (
|
||||||
"ask",
|
"unable to validate extra parameter '{}' for argument '{}': {}".format(
|
||||||
"foo",
|
"ask",
|
||||||
"parameter value must be a string, got 1",
|
"foo",
|
||||||
|
"parameter value must be a string, got 1",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
assert expected_msg in str(exception)
|
assert expected_msg in str(exception)
|
||||||
|
|
||||||
|
@ -233,9 +234,9 @@ def test_actions_map_api():
|
||||||
from moulinette.interfaces.api import ActionsMapParser
|
from moulinette.interfaces.api import ActionsMapParser
|
||||||
|
|
||||||
parser = ActionsMapParser()
|
parser = ActionsMapParser()
|
||||||
amap = ActionsMap(parser)
|
amap = ActionsMap("test/actionsmap/moulitest.yml", parser)
|
||||||
|
|
||||||
assert amap.main_namespace == "moulitest"
|
assert amap.namespace == "moulitest"
|
||||||
assert amap.default_authentication == "dummy"
|
assert amap.default_authentication == "dummy"
|
||||||
assert ("GET", "/test-auth/default") in amap.parser.routes
|
assert ("GET", "/test-auth/default") in amap.parser.routes
|
||||||
assert ("POST", "/test-auth/subcat/post") in amap.parser.routes
|
assert ("POST", "/test-auth/subcat/post") in amap.parser.routes
|
||||||
|
@ -248,7 +249,7 @@ def test_actions_map_api():
|
||||||
def test_actions_map_import_error(mocker):
|
def test_actions_map_import_error(mocker):
|
||||||
from moulinette.interfaces.api import ActionsMapParser
|
from moulinette.interfaces.api import ActionsMapParser
|
||||||
|
|
||||||
amap = ActionsMap(ActionsMapParser())
|
amap = ActionsMap("test/actionsmap/moulitest.yml", ActionsMapParser())
|
||||||
|
|
||||||
from moulinette.core import MoulinetteLock
|
from moulinette.core import MoulinetteLock
|
||||||
|
|
||||||
|
@ -266,7 +267,7 @@ def test_actions_map_import_error(mocker):
|
||||||
with pytest.raises(MoulinetteError) as exception:
|
with pytest.raises(MoulinetteError) as exception:
|
||||||
amap.process({}, timeout=30, route=("GET", "/test-auth/none"))
|
amap.process({}, timeout=30, route=("GET", "/test-auth/none"))
|
||||||
|
|
||||||
expected_msg = "unable to load function % s.%s because: %s" % (
|
expected_msg = "unable to load function {}.{} because: {}".format(
|
||||||
"moulitest",
|
"moulitest",
|
||||||
"testauth_none",
|
"testauth_none",
|
||||||
"Yoloswag",
|
"Yoloswag",
|
||||||
|
@ -287,9 +288,9 @@ def test_actions_map_cli():
|
||||||
)
|
)
|
||||||
|
|
||||||
parser = ActionsMapParser(top_parser=top_parser)
|
parser = ActionsMapParser(top_parser=top_parser)
|
||||||
amap = ActionsMap(parser)
|
amap = ActionsMap("test/actionsmap/moulitest.yml", parser)
|
||||||
|
|
||||||
assert amap.main_namespace == "moulitest"
|
assert amap.namespace == "moulitest"
|
||||||
assert amap.default_authentication == "dummy"
|
assert amap.default_authentication == "dummy"
|
||||||
assert "testauth" in amap.parser._subparsers.choices
|
assert "testauth" in amap.parser._subparsers.choices
|
||||||
assert "none" in amap.parser._subparsers.choices["testauth"]._actions[1].choices
|
assert "none" in amap.parser._subparsers.choices["testauth"]._actions[1].choices
|
||||||
|
|
|
@ -66,7 +66,7 @@ class TestAuthAPI:
|
||||||
def test_login(self, moulinette_webapi):
|
def test_login(self, moulinette_webapi):
|
||||||
assert self.login(moulinette_webapi).text == "Logged in"
|
assert self.login(moulinette_webapi).text == "Logged in"
|
||||||
|
|
||||||
assert "session.moulitest" in moulinette_webapi.cookies
|
assert "moulitest" in moulinette_webapi.cookies
|
||||||
|
|
||||||
def test_login_bad_password(self, moulinette_webapi):
|
def test_login_bad_password(self, moulinette_webapi):
|
||||||
assert (
|
assert (
|
||||||
|
@ -74,7 +74,7 @@ class TestAuthAPI:
|
||||||
== "invalid_password"
|
== "invalid_password"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert "session.moulitest" not in moulinette_webapi.cookies
|
assert "moulitest" not in moulinette_webapi.cookies
|
||||||
|
|
||||||
def test_login_csrf_attempt(self, moulinette_webapi):
|
def test_login_csrf_attempt(self, moulinette_webapi):
|
||||||
# C.f.
|
# C.f.
|
||||||
|
@ -85,9 +85,7 @@ class TestAuthAPI:
|
||||||
"CSRF protection"
|
"CSRF protection"
|
||||||
in self.login(moulinette_webapi, csrf=True, status=403).text
|
in self.login(moulinette_webapi, csrf=True, status=403).text
|
||||||
)
|
)
|
||||||
assert not any(
|
assert not any(c.name == "moulitest" for c in moulinette_webapi.cookiejar)
|
||||||
c.name == "session.moulitest" for c in moulinette_webapi.cookiejar
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_login_then_legit_request_without_cookies(self, moulinette_webapi):
|
def test_login_then_legit_request_without_cookies(self, moulinette_webapi):
|
||||||
self.login(moulinette_webapi)
|
self.login(moulinette_webapi)
|
||||||
|
@ -99,7 +97,7 @@ class TestAuthAPI:
|
||||||
def test_login_then_legit_request(self, moulinette_webapi):
|
def test_login_then_legit_request(self, moulinette_webapi):
|
||||||
self.login(moulinette_webapi)
|
self.login(moulinette_webapi)
|
||||||
|
|
||||||
assert "session.moulitest" in moulinette_webapi.cookies
|
assert "moulitest" in moulinette_webapi.cookies
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
moulinette_webapi.get("/test-auth/default", status=200).text
|
moulinette_webapi.get("/test-auth/default", status=200).text
|
||||||
|
@ -124,7 +122,7 @@ class TestAuthAPI:
|
||||||
def test_login_other_profile(self, moulinette_webapi):
|
def test_login_other_profile(self, moulinette_webapi):
|
||||||
self.login(moulinette_webapi, profile="yoloswag", password="yoloswag")
|
self.login(moulinette_webapi, profile="yoloswag", password="yoloswag")
|
||||||
|
|
||||||
assert "session.moulitest" in moulinette_webapi.cookies
|
assert "moulitest" in moulinette_webapi.cookies
|
||||||
|
|
||||||
def test_login_wrong_profile(self, moulinette_webapi):
|
def test_login_wrong_profile(self, moulinette_webapi):
|
||||||
self.login(moulinette_webapi)
|
self.login(moulinette_webapi)
|
||||||
|
@ -188,7 +186,7 @@ class TestAuthAPI:
|
||||||
class TestAuthCLI:
|
class TestAuthCLI:
|
||||||
def test_login(self, moulinette_cli, capsys, mocker):
|
def test_login(self, moulinette_cli, capsys, mocker):
|
||||||
mocker.patch("os.isatty", return_value=True)
|
mocker.patch("os.isatty", return_value=True)
|
||||||
mocker.patch("getpass.getpass", return_value="dummy")
|
mocker.patch("prompt_toolkit.prompt", return_value="dummy")
|
||||||
moulinette_cli.run(["testauth", "default"], output_as="plain")
|
moulinette_cli.run(["testauth", "default"], output_as="plain")
|
||||||
message = capsys.readouterr()
|
message = capsys.readouterr()
|
||||||
|
|
||||||
|
@ -201,25 +199,25 @@ class TestAuthCLI:
|
||||||
|
|
||||||
def test_login_bad_password(self, moulinette_cli, capsys, mocker):
|
def test_login_bad_password(self, moulinette_cli, capsys, mocker):
|
||||||
mocker.patch("os.isatty", return_value=True)
|
mocker.patch("os.isatty", return_value=True)
|
||||||
mocker.patch("getpass.getpass", return_value="Bad Password")
|
mocker.patch("prompt_toolkit.prompt", return_value="Bad Password")
|
||||||
with pytest.raises(MoulinetteError):
|
with pytest.raises(MoulinetteError):
|
||||||
moulinette_cli.run(["testauth", "default"], output_as="plain")
|
moulinette_cli.run(["testauth", "default"], output_as="plain")
|
||||||
|
|
||||||
mocker.patch("os.isatty", return_value=True)
|
mocker.patch("os.isatty", return_value=True)
|
||||||
mocker.patch("getpass.getpass", return_value="Bad Password")
|
mocker.patch("prompt_toolkit.prompt", return_value="Bad Password")
|
||||||
with pytest.raises(MoulinetteError):
|
with pytest.raises(MoulinetteError):
|
||||||
moulinette_cli.run(["testauth", "default"], output_as="plain")
|
moulinette_cli.run(["testauth", "default"], output_as="plain")
|
||||||
|
|
||||||
def test_login_wrong_profile(self, moulinette_cli, mocker):
|
def test_login_wrong_profile(self, moulinette_cli, mocker):
|
||||||
mocker.patch("os.isatty", return_value=True)
|
mocker.patch("os.isatty", return_value=True)
|
||||||
mocker.patch("getpass.getpass", return_value="dummy")
|
mocker.patch("prompt_toolkit.prompt", return_value="dummy")
|
||||||
with pytest.raises(MoulinetteError) as exception:
|
with pytest.raises(MoulinetteError) as exception:
|
||||||
moulinette_cli.run(["testauth", "other-profile"], output_as="none")
|
moulinette_cli.run(["testauth", "other-profile"], output_as="none")
|
||||||
|
|
||||||
assert "invalid_password" in str(exception)
|
assert "invalid_password" in str(exception)
|
||||||
|
|
||||||
mocker.patch("os.isatty", return_value=True)
|
mocker.patch("os.isatty", return_value=True)
|
||||||
mocker.patch("getpass.getpass", return_value="yoloswag")
|
mocker.patch("prompt_toolkit.prompt", return_value="yoloswag")
|
||||||
with pytest.raises(MoulinetteError) as exception:
|
with pytest.raises(MoulinetteError) as exception:
|
||||||
moulinette_cli.run(["testauth", "default"], output_as="none")
|
moulinette_cli.run(["testauth", "default"], output_as="none")
|
||||||
|
|
||||||
|
@ -239,7 +237,7 @@ class TestAuthCLI:
|
||||||
|
|
||||||
def test_request_only_cli(self, capsys, moulinette_cli, mocker):
|
def test_request_only_cli(self, capsys, moulinette_cli, mocker):
|
||||||
mocker.patch("os.isatty", return_value=True)
|
mocker.patch("os.isatty", return_value=True)
|
||||||
mocker.patch("getpass.getpass", return_value="dummy")
|
mocker.patch("prompt_toolkit.prompt", return_value="dummy")
|
||||||
moulinette_cli.run(["testauth", "only-cli"], output_as="plain")
|
moulinette_cli.run(["testauth", "only-cli"], output_as="plain")
|
||||||
|
|
||||||
message = capsys.readouterr()
|
message = capsys.readouterr()
|
||||||
|
@ -248,7 +246,7 @@ class TestAuthCLI:
|
||||||
|
|
||||||
def test_request_not_logged_only_cli(self, capsys, moulinette_cli, mocker):
|
def test_request_not_logged_only_cli(self, capsys, moulinette_cli, mocker):
|
||||||
mocker.patch("os.isatty", return_value=True)
|
mocker.patch("os.isatty", return_value=True)
|
||||||
mocker.patch("getpass.getpass")
|
mocker.patch("prompt_toolkit.prompt")
|
||||||
with pytest.raises(MoulinetteError) as exception:
|
with pytest.raises(MoulinetteError) as exception:
|
||||||
moulinette_cli.run(["testauth", "only-cli"], output_as="plain")
|
moulinette_cli.run(["testauth", "only-cli"], output_as="plain")
|
||||||
|
|
||||||
|
@ -257,28 +255,9 @@ class TestAuthCLI:
|
||||||
|
|
||||||
assert "invalid_password" in str(exception)
|
assert "invalid_password" in str(exception)
|
||||||
|
|
||||||
def test_request_with_callback(self, moulinette_cli, capsys, mocker):
|
|
||||||
mocker.patch("os.isatty", return_value=True)
|
|
||||||
mocker.patch("getpass.getpass", return_value="dummy")
|
|
||||||
moulinette_cli.run(["--version"], output_as="plain")
|
|
||||||
message = capsys.readouterr()
|
|
||||||
|
|
||||||
assert "666" in message.out
|
|
||||||
|
|
||||||
moulinette_cli.run(["-v"], output_as="plain")
|
|
||||||
message = capsys.readouterr()
|
|
||||||
|
|
||||||
assert "666" in message.out
|
|
||||||
|
|
||||||
with pytest.raises(MoulinetteError):
|
|
||||||
moulinette_cli.run(["--wersion"], output_as="plain")
|
|
||||||
message = capsys.readouterr()
|
|
||||||
|
|
||||||
assert "cannot get value from callback method" in message.err
|
|
||||||
|
|
||||||
def test_request_with_arg(self, moulinette_cli, capsys, mocker):
|
def test_request_with_arg(self, moulinette_cli, capsys, mocker):
|
||||||
mocker.patch("os.isatty", return_value=True)
|
mocker.patch("os.isatty", return_value=True)
|
||||||
mocker.patch("getpass.getpass", return_value="dummy")
|
mocker.patch("prompt_toolkit.prompt", return_value="dummy")
|
||||||
moulinette_cli.run(["testauth", "with_arg", "yoloswag"], output_as="plain")
|
moulinette_cli.run(["testauth", "with_arg", "yoloswag"], output_as="plain")
|
||||||
message = capsys.readouterr()
|
message = capsys.readouterr()
|
||||||
|
|
||||||
|
@ -286,7 +265,7 @@ class TestAuthCLI:
|
||||||
|
|
||||||
def test_request_arg_with_extra(self, moulinette_cli, capsys, mocker):
|
def test_request_arg_with_extra(self, moulinette_cli, capsys, mocker):
|
||||||
mocker.patch("os.isatty", return_value=True)
|
mocker.patch("os.isatty", return_value=True)
|
||||||
mocker.patch("getpass.getpass", return_value="dummy")
|
mocker.patch("prompt_toolkit.prompt", return_value="dummy")
|
||||||
moulinette_cli.run(
|
moulinette_cli.run(
|
||||||
["testauth", "with_extra_str_only", "YoLoSwAg"], output_as="plain"
|
["testauth", "with_extra_str_only", "YoLoSwAg"], output_as="plain"
|
||||||
)
|
)
|
||||||
|
@ -306,7 +285,7 @@ class TestAuthCLI:
|
||||||
|
|
||||||
def test_request_arg_with_type(self, moulinette_cli, capsys, mocker):
|
def test_request_arg_with_type(self, moulinette_cli, capsys, mocker):
|
||||||
mocker.patch("os.isatty", return_value=True)
|
mocker.patch("os.isatty", return_value=True)
|
||||||
mocker.patch("getpass.getpass", return_value="dummy")
|
mocker.patch("prompt_toolkit.prompt", return_value="dummy")
|
||||||
moulinette_cli.run(["testauth", "with_type_int", "12345"], output_as="plain")
|
moulinette_cli.run(["testauth", "with_type_int", "12345"], output_as="plain")
|
||||||
message = capsys.readouterr()
|
message = capsys.readouterr()
|
||||||
|
|
||||||
|
|
|
@ -330,7 +330,6 @@ def test_mkdir(tmp_path):
|
||||||
|
|
||||||
|
|
||||||
def test_mkdir_with_permission(tmp_path, mocker):
|
def test_mkdir_with_permission(tmp_path, mocker):
|
||||||
|
|
||||||
# This test only make sense when not being root
|
# This test only make sense when not being root
|
||||||
if os.getuid() == 0:
|
if os.getuid() == 0:
|
||||||
return
|
return
|
||||||
|
|
|
@ -13,12 +13,10 @@ reference = json.loads(open(locale_folder + "en.json").read())
|
||||||
|
|
||||||
|
|
||||||
def find_inconsistencies(locale_file):
|
def find_inconsistencies(locale_file):
|
||||||
|
|
||||||
this_locale = json.loads(open(locale_folder + locale_file).read())
|
this_locale = json.loads(open(locale_folder + locale_file).read())
|
||||||
|
|
||||||
# We iterate over all keys/string in en.json
|
# We iterate over all keys/string in en.json
|
||||||
for key, string in reference.items():
|
for key, string in reference.items():
|
||||||
|
|
||||||
# Ignore check if there's no translation yet for this key
|
# Ignore check if there's no translation yet for this key
|
||||||
if key not in this_locale:
|
if key not in this_locale:
|
||||||
continue
|
continue
|
||||||
|
@ -26,10 +24,10 @@ def find_inconsistencies(locale_file):
|
||||||
# Then we check that every "{stuff}" (for python's .format())
|
# Then we check that every "{stuff}" (for python's .format())
|
||||||
# should also be in the translated string, otherwise the .format
|
# should also be in the translated string, otherwise the .format
|
||||||
# will trigger an exception!
|
# will trigger an exception!
|
||||||
subkeys_in_ref = set(k[0] for k in re.findall(r"{(\w+)(:\w)?}", string))
|
subkeys_in_ref = {k[0] for k in re.findall(r"{(\w+)(:\w)?}", string)}
|
||||||
subkeys_in_this_locale = set(
|
subkeys_in_this_locale = {
|
||||||
k[0] for k in re.findall(r"{(\w+)(:\w)?}", this_locale[key])
|
k[0] for k in re.findall(r"{(\w+)(:\w)?}", this_locale[key])
|
||||||
)
|
}
|
||||||
|
|
||||||
if any(k not in subkeys_in_ref for k in subkeys_in_this_locale):
|
if any(k not in subkeys_in_ref for k in subkeys_in_this_locale):
|
||||||
yield """\n
|
yield """\n
|
||||||
|
|
|
@ -11,7 +11,6 @@ import json
|
||||||
|
|
||||||
|
|
||||||
def find_expected_string_keys():
|
def find_expected_string_keys():
|
||||||
|
|
||||||
# Try to find :
|
# Try to find :
|
||||||
# m18n.g( "foo"
|
# m18n.g( "foo"
|
||||||
# MoulinetteError("foo"
|
# MoulinetteError("foo"
|
||||||
|
@ -69,7 +68,6 @@ def test_undefined_i18n_keys():
|
||||||
|
|
||||||
|
|
||||||
def test_unused_i18n_keys():
|
def test_unused_i18n_keys():
|
||||||
|
|
||||||
unused_keys = keys_defined.difference(expected_string_keys)
|
unused_keys = keys_defined.difference(expected_string_keys)
|
||||||
unused_keys = sorted(unused_keys)
|
unused_keys = sorted(unused_keys)
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,6 @@ def test_run_shell_kwargs():
|
||||||
|
|
||||||
|
|
||||||
def test_call_async_output(test_file):
|
def test_call_async_output(test_file):
|
||||||
|
|
||||||
mock_callback_stdout = mock.Mock()
|
mock_callback_stdout = mock.Mock()
|
||||||
mock_callback_stderr = mock.Mock()
|
mock_callback_stderr = mock.Mock()
|
||||||
|
|
||||||
|
@ -118,7 +117,6 @@ def test_call_async_output(test_file):
|
||||||
|
|
||||||
|
|
||||||
def test_call_async_output_kwargs(test_file, mocker):
|
def test_call_async_output_kwargs(test_file, mocker):
|
||||||
|
|
||||||
mock_callback_stdout = mock.Mock()
|
mock_callback_stdout = mock.Mock()
|
||||||
mock_callback_stdinfo = mock.Mock()
|
mock_callback_stdinfo = mock.Mock()
|
||||||
mock_callback_stderr = mock.Mock()
|
mock_callback_stderr = mock.Mock()
|
||||||
|
|
|
@ -5,7 +5,7 @@ from moulinette.interfaces import JSONExtendedEncoder
|
||||||
def test_json_extended_encoder(caplog):
|
def test_json_extended_encoder(caplog):
|
||||||
encoder = JSONExtendedEncoder()
|
encoder = JSONExtendedEncoder()
|
||||||
|
|
||||||
assert encoder.default(set([1, 2, 3])) == [1, 2, 3]
|
assert encoder.default({1, 2, 3}) == [1, 2, 3]
|
||||||
|
|
||||||
assert encoder.default(dt(1917, 3, 8)) == "1917-03-08T00:00:00+00:00"
|
assert encoder.default(dt(1917, 3, 8)) == "1917-03-08T00:00:00+00:00"
|
||||||
|
|
||||||
|
|
|
@ -20,3 +20,4 @@ def test_prependlines():
|
||||||
|
|
||||||
def test_random_ascii():
|
def test_random_ascii():
|
||||||
assert isinstance(random_ascii(length=2), str)
|
assert isinstance(random_ascii(length=2), str)
|
||||||
|
assert len(random_ascii(length=10)) == 10
|
||||||
|
|
|
@ -13,12 +13,10 @@ reference = json.loads(open(locale_folder + "en.json").read())
|
||||||
|
|
||||||
|
|
||||||
def find_inconsistencies(locale_file):
|
def find_inconsistencies(locale_file):
|
||||||
|
|
||||||
this_locale = json.loads(open(locale_folder + locale_file).read())
|
this_locale = json.loads(open(locale_folder + locale_file).read())
|
||||||
|
|
||||||
# We iterate over all keys/string in en.json
|
# We iterate over all keys/string in en.json
|
||||||
for key, string in reference.items():
|
for key, string in reference.items():
|
||||||
|
|
||||||
# Ignore check if there's no translation yet for this key
|
# Ignore check if there's no translation yet for this key
|
||||||
if key not in this_locale:
|
if key not in this_locale:
|
||||||
continue
|
continue
|
||||||
|
@ -26,10 +24,10 @@ def find_inconsistencies(locale_file):
|
||||||
# Then we check that every "{stuff}" (for python's .format())
|
# Then we check that every "{stuff}" (for python's .format())
|
||||||
# should also be in the translated string, otherwise the .format
|
# should also be in the translated string, otherwise the .format
|
||||||
# will trigger an exception!
|
# will trigger an exception!
|
||||||
subkeys_in_ref = set(k[0] for k in re.findall(r"{(\w+)(:\w)?}", string))
|
subkeys_in_ref = {k[0] for k in re.findall(r"{(\w+)(:\w)?}", string)}
|
||||||
subkeys_in_this_locale = set(
|
subkeys_in_this_locale = {
|
||||||
k[0] for k in re.findall(r"{(\w+)(:\w)?}", this_locale[key])
|
k[0] for k in re.findall(r"{(\w+)(:\w)?}", this_locale[key])
|
||||||
)
|
}
|
||||||
|
|
||||||
if any(k not in subkeys_in_ref for k in subkeys_in_this_locale):
|
if any(k not in subkeys_in_ref for k in subkeys_in_this_locale):
|
||||||
yield """\n
|
yield """\n
|
||||||
|
|
18
tox.ini
18
tox.ini
|
@ -1,6 +1,6 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
py37-{pytest,lint,invalidcode,mypy}
|
py{37,39}-{pytest,lint,invalidcode,mypy}
|
||||||
format
|
format
|
||||||
format-check
|
format-check
|
||||||
docs
|
docs
|
||||||
|
@ -11,15 +11,15 @@ usedevelop = True
|
||||||
passenv = *
|
passenv = *
|
||||||
extras = tests
|
extras = tests
|
||||||
deps =
|
deps =
|
||||||
py37-pytest: .[tests]
|
py{37,39}-pytest: .[tests]
|
||||||
py37-lint: flake8
|
py{37,39}-lint: flake8
|
||||||
py37-invalidcode: flake8
|
py{37,39}-invalidcode: flake8
|
||||||
py37-mypy: mypy >= 0.761
|
py{37,39}-mypy: mypy >= 0.761
|
||||||
commands =
|
commands =
|
||||||
py37-pytest: pytest {posargs} -c pytest.ini
|
py{37,39}-pytest: pytest {posargs} -c pytest.ini
|
||||||
py37-lint: flake8 moulinette test
|
py{37,39}-lint: flake8 moulinette test
|
||||||
py37-invalidcode: flake8 moulinette test --select F
|
py{37,39}-invalidcode: flake8 moulinette test --select F
|
||||||
py37-mypy: mypy --ignore-missing-imports --install-types --non-interactive moulinette/
|
py{37,39}-mypy: mypy --ignore-missing-imports --install-types --non-interactive moulinette/
|
||||||
|
|
||||||
[gh-actions]
|
[gh-actions]
|
||||||
python =
|
python =
|
||||||
|
|
Loading…
Add table
Reference in a new issue