Compare commits

...

231 commits

Author SHA1 Message Date
Alexandre Aubin
8de98670e9
Merge pull request #358 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2024-08-20 16:28:26 +02:00
xabirequejo
068d6d369a Translated using Weblate (Basque)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/eu/
2024-08-16 17:54:48 +02:00
Ivan Davydov
3c7f55c610 Translated using Weblate (Russian)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/ru/
2024-08-05 12:13:55 +02:00
cjdw
531c972bed Translated using Weblate (Indonesian)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/id/
2024-07-16 11:03:01 +02:00
Ivan Davydov
7c378210fa Translated using Weblate (Russian)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/ru/
2024-07-04 20:54:46 +02:00
OniriCorpe
497ea8a4af chores: update changelog for 11.2.1 & add make_changelog script 2024-05-20 00:33:05 +02:00
Alexandre Aubin
65d694280c
Merge pull request #357 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2024-05-19 23:38:56 +02:00
xabirequejo
dc1ce5a9ab Translated using Weblate (Basque)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/eu/
2024-05-10 17:59:06 +02:00
rosbeef andino
663d80666a Translated using Weblate (Spanish)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/es/
2024-05-10 17:59:06 +02:00
José M
36c20d5582 Translated using Weblate (Galician)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/gl/
2024-05-08 12:13:00 +02:00
OniriCorpe
95503ca2e6
Merge pull request #356 from YunoHost/actions/black 2024-05-08 01:53:22 +02:00
OniriCorpe
96539ce5c7 🎨 Format Python code with Black 2024-05-07 23:52:31 +00:00
OniriCorpe
578fb497e9
i18n: accept U+202F as an okay char 2024-05-08 01:52:11 +02:00
OniriCorpe
954d608a61
Merge pull request #354 from yunohost-bot/weblate-yunohost-moulinette 2024-05-08 01:31:46 +02:00
OniriCorpe
ac447a1f9d Translated using Weblate (French)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/fr/
2024-05-08 01:11:39 +02:00
Tagada
c63634f86f
Merge pull request #353 from YunoHost/actions/i18nreformat
Reformat locale files
2024-03-24 16:58:12 +01:00
OniriCorpe
14bbff41ad 🤖 Reformat locale files 2024-03-23 19:09:18 +00:00
OniriCorpe
291ed50843
Merge pull request #352 from yunohost-bot/weblate-yunohost-moulinette 2024-03-23 20:09:07 +01:00
OniriCorpe
5cb073bb75 Translated using Weblate (Japanese)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/ja/
2024-03-23 20:04:43 +01:00
OniriCorpe
8eb3ce3377 Translated using Weblate (Indonesian)
Currently translated at 93.3% (42 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/id/
2024-03-23 20:04:42 +01:00
OniriCorpe
d1a7445773 Translated using Weblate (Persian)
Currently translated at 95.5% (43 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/fa/
2024-03-23 20:04:42 +01:00
OniriCorpe
41e170a214 Translated using Weblate (Ukrainian)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/uk/
2024-03-23 20:04:42 +01:00
OniriCorpe
dbcadf0c87 Translated using Weblate (Swedish)
Currently translated at 84.4% (38 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/sv/
2024-03-23 20:04:42 +01:00
OniriCorpe
7f2563e65a Translated using Weblate (Polish)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/pl/
2024-03-23 20:04:42 +01:00
OniriCorpe
e8b46f8c9b Translated using Weblate (Turkish)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/tr/
2024-03-23 20:04:42 +01:00
OniriCorpe
e196ed34c4 Translated using Weblate (Russian)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/ru/
2024-03-23 20:04:41 +01:00
OniriCorpe
750aab64da Translated using Weblate (Dutch)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/nl/
2024-03-23 20:04:41 +01:00
OniriCorpe
1cac05b9fc Translated using Weblate (Italian)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/it/
2024-03-23 20:04:41 +01:00
OniriCorpe
f4712d9300 Translated using Weblate (French)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/fr/
2024-03-23 20:04:41 +01:00
OniriCorpe
e96c380d29 Translated using Weblate (Esperanto)
Currently translated at 95.5% (43 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/eo/
2024-03-23 20:04:41 +01:00
OniriCorpe
d81b4578e9 Translated using Weblate (German)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/de/
2024-03-23 20:04:41 +01:00
OniriCorpe
0ffda1a0db Translated using Weblate (Chinese (Simplified))
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/zh_Hans/
2024-03-23 20:04:40 +01:00
OniriCorpe
b2b912e7b9 Translated using Weblate (Arabic)
Currently translated at 95.5% (43 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/ar/
2024-03-23 20:04:40 +01:00
OniriCorpe
4a52f20417 Translated using Weblate (English)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/en/
2024-03-23 20:04:40 +01:00
OniriCorpe
cdd5ee6134 Translated using Weblate (Japanese)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/ja/
2024-03-23 19:01:36 +01:00
OniriCorpe
efc7d23738 Translated using Weblate (Indonesian)
Currently translated at 93.3% (42 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/id/
2024-03-23 19:01:35 +01:00
OniriCorpe
2a2bb468fc Translated using Weblate (Persian)
Currently translated at 95.5% (43 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/fa/
2024-03-23 19:01:35 +01:00
OniriCorpe
f7a9678b59 Translated using Weblate (Ukrainian)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/uk/
2024-03-23 19:01:35 +01:00
OniriCorpe
50ce4c9b03 Translated using Weblate (Czech)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/cs/
2024-03-23 19:01:35 +01:00
OniriCorpe
3bed227f6d Translated using Weblate (Nepali)
Currently translated at 13.3% (6 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/ne/
2024-03-23 19:01:35 +01:00
OniriCorpe
45b1a92743 Translated using Weblate (Polish)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/pl/
2024-03-23 19:01:34 +01:00
OniriCorpe
257e7095e5 Translated using Weblate (Turkish)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/tr/
2024-03-23 19:01:34 +01:00
OniriCorpe
4dfa2ee710 Translated using Weblate (Russian)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/ru/
2024-03-23 19:01:34 +01:00
OniriCorpe
080d244f44 Translated using Weblate (Portuguese)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/pt/
2024-03-23 19:01:34 +01:00
OniriCorpe
39113a8ed5 Translated using Weblate (Dutch)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/nl/
2024-03-23 19:01:34 +01:00
OniriCorpe
fe607a2ef1 Translated using Weblate (Italian)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/it/
2024-03-23 19:01:34 +01:00
OniriCorpe
d59f142019 Translated using Weblate (French)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/fr/
2024-03-23 19:01:33 +01:00
OniriCorpe
c59b869286 Translated using Weblate (Spanish)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/es/
2024-03-23 19:01:33 +01:00
OniriCorpe
8c38478d02 Translated using Weblate (Esperanto)
Currently translated at 93.3% (42 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/eo/
2024-03-23 19:01:33 +01:00
OniriCorpe
e8e206b482 Translated using Weblate (German)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/de/
2024-03-23 19:01:33 +01:00
OniriCorpe
21ecfbcd69 Translated using Weblate (Chinese (Simplified))
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/zh_Hans/
2024-03-23 19:01:33 +01:00
José M
8bde8eee14 Translated using Weblate (Galician)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/gl/
2024-03-23 14:39:29 +01:00
ppr
0ee77f922e Translated using Weblate (French)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/fr/
2024-03-23 14:39:29 +01:00
xabirequejo
97a24dbbce Translated using Weblate (Basque)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/eu/
2024-03-23 14:39:29 +01:00
xaloc33
6e5429ce00 Translated using Weblate (Catalan)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/ca/
2024-03-23 14:39:29 +01:00
OniriCorpe
0ad9eac48c Translated using Weblate (French)
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/fr/
2024-03-23 06:05:24 +01:00
OniriCorpe
e00be08bee Translated using Weblate (English)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/en/
2024-03-23 06:05:24 +01:00
Alexandre Aubin
9316db6054
Merge pull request #351 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2024-03-20 18:41:11 +01:00
Alexandre Aubin
6e434cddc6
Update locales/fr.json 2024-03-20 18:41:00 +01:00
OniriCorpe
c698807f12 Translated using Weblate (French)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/fr/
2024-03-09 05:14:55 +01:00
OniriCorpe
95f7e6804a Translated using Weblate (Norwegian Bokmål)
Currently translated at 37.7% (17 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/nb_NO/
2024-03-07 23:09:11 +01:00
OniriCorpe
c8193819c7 Translated using Weblate (Hindi)
Currently translated at 57.7% (26 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/hi/
2024-03-07 23:09:11 +01:00
Psycojoker
cadb449528 🎨 Format Python code with Black 2024-03-07 02:37:34 +01:00
Laurent Peuch
518e5ebb85 ci(autoblacks): add step to check if black reformatted some code 2024-03-07 02:36:05 +01:00
Laurent Peuch
d09e1a2fab ci(autoblack): Input 'black_args' has been deprecated with message: Input is deprecated. Use and instead. 2024-03-07 01:51:23 +01:00
Laurent Peuch
f1010fb8a3 fix: port doc/ldif2dot-0.1.py to python3 for black 2024-03-07 01:45:38 +01:00
OniriCorpe
7a46e1499d ci(autoblack): use the now available official psf/black github action 2024-03-07 01:39:48 +01:00
OniriCorpe
1b28bca0c0 chores: update actions to nodejs 20
Also replaces rickstaa/action-black by psf/black
2024-03-07 01:39:48 +01:00
OniriCorpe
d0089dbdf9 revert x___x 2024-02-25 07:18:12 +01:00
OniriCorpe
ecdc43a09a chores: update actions to nodejs 20 2024-02-25 07:13:47 +01:00
Alexandre Aubin
38d83520b5
Merge pull request #344 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2024-02-02 22:02:20 +01:00
Francescc
1d3908c9a5 Translated using Weblate (Catalan)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/ca/
2024-01-29 03:35:41 +01:00
Weblate
a69b276188 Added translation using Weblate (Korean) 2024-01-25 13:39:25 +01:00
Alexandre Aubin
04a688a141
Merge pull request #343 from methbkts/dev
chore: update actions version to use node 16 version
2023-10-31 12:59:45 +01:00
Metin Bektas
ed0cd88da9 chore: update actions version to use node 16 version 2023-10-31 10:51:49 +01:00
Alexandre Aubin
b4e79bd278 Update changelog for 11.2 2023-07-17 16:33:14 +02:00
Alexandre Aubin
5ec28b19c0
Merge pull request #335 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2023-07-17 15:53:09 +02:00
motcha
64a39cc595 Translated using Weblate (Japanese)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/ja/
2023-07-12 01:35:12 +02:00
Alexandre Aubin
e5fa7ab734 Update changelog for 11.1.5 2023-07-10 21:34:12 +02:00
Alexandre Aubin
d66fd94f68
Merge pull request #334 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2023-07-10 21:32:02 +02:00
motcha
145fd9a91a Translated using Weblate (Japanese)
Currently translated at 4.4% (2 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/ja/
2023-07-10 21:29:06 +02:00
Weblate
0c71095db3 Added translation using Weblate (Japanese) 2023-07-10 21:29:06 +02:00
Alexandre Aubin
c06e1a91c9 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 2023-07-10 21:28:54 +02:00
Alexandre Aubin
d4769ec0a5
Merge pull request #332 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2023-05-08 15:47:02 +02:00
Neko Nekowazarashi
9078c881ea Translated using Weblate (Indonesian)
Currently translated at 95.5% (43 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/id/
2023-04-25 12:11:42 +02:00
Neko Nekowazarashi
f6234577c4 Translated using Weblate (Indonesian)
Currently translated at 95.5% (43 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/id/
2023-03-27 03:04:42 +02:00
Neko Nekowazarashi
6ec06b4ce3 Translated using Weblate (Indonesian)
Currently translated at 88.8% (40 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/id/
2023-03-27 02:29:51 +02:00
liimee
d1e10e23d4 Translated using Weblate (Indonesian)
Currently translated at 88.8% (40 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/id/
2023-03-07 23:31:01 +01:00
Neko Nekowazarashi
c5aeb1acad Translated using Weblate (Indonesian)
Currently translated at 88.8% (40 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/id/
2023-02-27 19:14:59 +01:00
Alexandre Aubin
2373a7fa5e
setup.py: fix version specifier in python_requires, python tooling not happy with * i guess 2023-02-06 21:05:57 +01:00
Alexandre Aubin
d1827d1a41
Merge pull request #331 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2023-02-06 21:05:01 +01:00
Poesty Li
f1e7984fc9 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/zh_Hans/
2023-02-06 20:30:26 +01:00
Alexandre Aubin
1fb77feec3
Merge pull request #330 from YunoHost/actions/black
Format Python code with Black
2023-02-01 20:33:26 +01:00
alexAubin
52bbab7df1 🎨 Format Python code with Black 2023-02-01 19:30:09 +00:00
Alexandre Aubin
aaf7f764e3 Update changelog for 11.1.4 2023-02-01 20:29:27 +01:00
Alexandre Aubin
4d2694ef8d Update changelog for 11.1.2.1 2023-01-30 16:34:59 +01:00
Alexandre Aubin
2539ea9c70
Merge pull request #329 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2023-01-30 15:49:45 +01:00
Weblate
d7162b209a Added translation using Weblate (Lithuanian) 2023-01-14 15:42:32 +01:00
ButterflyOfFire
cabe9a728b Translated using Weblate (Arabic)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/ar/
2023-01-11 20:17:03 +01:00
Alexandre Aubin
4c03e16de9 Handle calling 'yunohost' with no args more gracefully 2023-01-06 17:33:31 +01:00
Alexandre Aubin
9fc33eec1a Update changelog for 11.1.2 2023-01-06 00:38:59 +01:00
Alexandre Aubin
95695e94d7
Merge pull request #325 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2023-01-05 19:19:12 +01:00
xabirequejo
bc9b4a2ce2 Translated using Weblate (Basque)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/eu/
2023-01-05 19:18:49 +01:00
André Koot
55a09c57ac Translated using Weblate (Dutch)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/nl/
2023-01-05 19:18:49 +01:00
ButterflyOfFire
bbd2468e35 Translated using Weblate (Arabic)
Currently translated at 91.1% (41 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/ar/
2023-01-05 19:18:49 +01:00
xabirequejo
12c5482d6c Translated using Weblate (Basque)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/eu/
2023-01-05 19:18:49 +01:00
Weblate
ea90c056b7 Added translation using Weblate (Portuguese (Brazil)) 2023-01-05 19:18:49 +01:00
Weblate
c99439740f Added translation using Weblate (Hebrew) 2023-01-05 19:18:49 +01:00
Alexandre Aubin
2195ed6f90
Merge pull request #328 from YunoHost/actions/black
Format Python code with Black
2023-01-05 19:18:38 +01:00
alexAubin
2d2eab4c30 🎨 Format Python code with Black 2023-01-05 18:15:55 +00:00
Alexandre Aubin
9bc187d404
Merge pull request #327 from YunoHost/dont-take-lock-for-read-operations
Dont take lock for read/GET operations
2023-01-05 19:15:20 +01:00
Alexandre Aubin
d0e65fdb46
Merge branch 'dev' into dont-take-lock-for-read-operations 2023-01-05 19:15:14 +01:00
Alexandre Aubin
01a4dc0942
Fix flake8 config syntax making the test crash 2023-01-05 19:14:42 +01:00
Alexandre Aubin
baa00c0812 Fix flake8 2022-12-24 19:28:26 +01:00
Alexandre Aubin
1e79e99cc8 Don't take lock for read/GET operations : also cover cases when there's a list of routes 2022-12-24 00:57:17 +01:00
Alexandre Aubin
ac0ac24996 Merge branch 'dev' into dont-take-lock-for-read-operations 2022-12-24 00:52:08 +01:00
Alexandre Aubin
7f4e8b394c Remove old test about old callback mechanism 2022-12-24 00:07:14 +01:00
Alexandre Aubin
aa06187284
Merge pull request #326 from YunoHost/actions/black
Format Python code with Black
2022-12-24 00:03:07 +01:00
Alexandre Aubin
676a19dc28 Don't take lock for read/GET operations 2022-12-24 00:02:24 +01:00
alexAubin
161c5d5c2b 🎨 Format Python code with Black 2022-12-23 23:01:46 +00:00
Alexandre Aubin
80873777c6 Code simplification/explicitation 2022-12-23 22:34:49 +01:00
Alexandre Aubin
50b19a95c6 Remove the 'global argument' mechanism ... we only use it for --version and it's just batshit overly complicated for what this achieves... 2022-12-23 20:31:58 +01:00
Alexandre Aubin
5258f22985 Update changelog for 11.1.0 2022-10-26 16:16:01 +02:00
Alexandre Aubin
d54dd286be
Merge pull request #323 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2022-09-30 15:05:13 +02:00
Sedat Albayrak
ceba5273f2 Translated using Weblate (Turkish)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/tr/
2022-09-12 06:42:11 +02:00
ButterflyOfFire
6bdd6dcecb Translated using Weblate (Arabic)
Currently translated at 88.8% (40 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/ar/
2022-08-07 21:32:10 +00:00
Alexandre Aubin
fcd84e3e34 Update changelog for 11.0.9 2022-08-07 23:31:40 +02:00
Alexandre Aubin
9dd1a8a63e Update changelog for 11.0.8 2022-08-07 11:56:27 +02:00
Alexandre Aubin
4239f466f8 Fix random_ascii behavior : it was in fact returning a string with 2*length ... 2022-08-05 10:55:04 +02:00
Alexandre Aubin
b20b1af116
Merge pull request #322 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2022-08-03 22:19:03 +02:00
Alexandre Aubin
eadf40c552
Donnnnt translate placeholders heauarg 2022-08-03 22:18:25 +02:00
Gregor
b27f2351ca Translated using Weblate (German)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/de/
2022-07-22 02:15:09 +00:00
Radek Raczkowski
0d2fbb3ad0 Translated using Weblate (Polish)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/pl/
2022-07-10 13:50:39 +00:00
Jose Riha
f0632f0927 Translated using Weblate (Slovak)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/sk/
2022-06-22 09:36:26 +00:00
Jose Riha
f9d899ca52 Translated using Weblate (Slovak)
Currently translated at 60.0% (27 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/sk/
2022-06-22 05:38:24 +00:00
Weblate
fc13528570 Added translation using Weblate (Telugu) 2022-06-06 21:18:51 +00:00
Jose Riha
5f6d5ac4de Translated using Weblate (Slovak)
Currently translated at 6.6% (3 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/sk/
2022-05-25 08:39:38 +00:00
Weblate
4a13653f3b Added translation using Weblate (Slovak) 2022-05-23 20:08:38 +00:00
tituspijean
e6ca65923c
Update changelog for 11.0.7 2022-05-18 00:03:04 +02:00
Alexandre Aubin
70e41495a4
Merge pull request #321 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2022-04-27 21:54:56 +02:00
Jimmy Angel Pérez Díaz
6f9b0f4ea4 Translated using Weblate (Spanish)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/es/
2022-04-05 15:35:02 +00:00
Kay0u
e654b67180
Merge branch 'dev' of github.com:YunoHost/moulinette into dev 2022-03-29 14:25:16 +02:00
Kay0u
641335ea7c
Update changelog for 11.0.6 2022-03-29 14:24:34 +02:00
Kayou
e4aba32128
Merge pull request #320 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2022-03-29 14:23:35 +02:00
3ole
d96d2b9c15 Translated using Weblate (German)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/de/
2022-03-27 22:17:24 +00:00
Yifei Ding
b19f1f6f80 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/zh_Hans/
2022-03-27 22:17:24 +00:00
Selyan Slimane Amiri
5c0f025e46 Translated using Weblate (Kabyle)
Currently translated at 13.3% (6 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/kab/
2022-03-24 14:38:04 +00:00
Weblate
e1e98263cd Added translation using Weblate (Kabyle) 2022-03-14 07:43:35 +00:00
Alexandre Aubin
2e117e8d63
Merge pull request #318 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2022-03-08 13:21:05 +01:00
José M
4fa7568c84 Translated using Weblate (Galician)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/gl/
2022-02-18 16:01:17 +00:00
Mico Hauataluoma
9700d8e293 Translated using Weblate (Finnish)
Currently translated at 4.4% (2 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/fi/
2022-02-08 13:46:53 +00:00
Weblate
bcf41f6ccc Added translation using Weblate (Danish) 2022-02-08 13:46:53 +00:00
Alexandre Aubin
74f2c26da6
Merge pull request #319 from YunoHost/actions/black
Format Python code with Black
2022-02-08 14:46:49 +01:00
alexAubin
b0b440e4cc 🎨 Format Python code with Black 2022-02-08 13:45:42 +00:00
Alexandre Aubin
cb9ecb468d cli: Add possibility to hide commands in --help 2022-02-08 14:45:02 +01:00
Alexandre Aubin
7e2da7a897
Merge pull request #317 from YunoHost/actions/black
Format Python code with Black
2022-01-19 23:17:54 +01:00
alexAubin
45dbcd98f4 🎨 Format Python code with Black 2022-01-19 22:14:17 +00:00
Alexandre Aubin
23d6107861 Update changelog for 11.0.2 2022-01-19 21:18:06 +01:00
Alexandre Aubin
d8cb39d809 Merge branch 'dev' into bullseye 2022-01-19 21:15:34 +01:00
Alexandre Aubin
0ff3f45d92 Update changelog for 4.4.0 2022-01-19 21:13:34 +01:00
Alexandre Aubin
5434a8994c Update changelog for 4.3.3.1 2022-01-19 21:12:31 +01:00
Alexandre Aubin
5192c66f95
Merge pull request #316 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2022-01-19 20:04:11 +01:00
Gregor
88bf138501 Translated using Weblate (German)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/de/
2022-01-17 13:41:59 +00:00
Mico Hauataluoma
496e387900 Translated using Weblate (Finnish)
Currently translated at 2.2% (1 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/fi/
2022-01-15 13:20:47 +00:00
Alexandre Aubin
e8c0ffac53
Merge pull request #315 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2022-01-14 00:49:41 +01:00
Boudewijn
8473743fea Translated using Weblate (Dutch)
Currently translated at 93.3% (42 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/nl/
2022-01-13 21:22:08 +00:00
Alexandre Aubin
5440053d5f
Merge pull request #311 from YunoHost/moar_session_management_changes
api: Move cookie session management logic to the authenticator for more flexibility
2022-01-11 12:58:28 +01:00
Alexandre Aubin
9ccc914ca1
Merge branch 'bullseye' into moar_session_management_changes 2022-01-11 12:56:24 +01:00
Alexandre Aubin
35351696ac Merge branch 'dev' into bullseye 2021-12-31 15:21:57 +01:00
Alexandre Aubin
c04aac6d98
Merge pull request #314 from YunoHost/add-lgtm
Add lgtm
2021-12-30 18:59:28 +01:00
Kay0u
be0006fdb9
fix some warnings 2021-12-30 14:39:38 +01:00
Kay0u
29d0d0cfc1
add lgtm badge 2021-12-30 14:39:30 +01:00
Alexandre Aubin
8b05fcf2fd Update changelog fro 4.3.3 2021-12-29 01:09:02 +01:00
Alexandre Aubin
0afface3f2
Merge pull request #313 from YunoHost/actions/black
Format Python code with Black
2021-12-29 00:47:28 +01:00
alexAubin
ea6eaa6b1e 🎨 Format Python code with Black 2021-12-28 23:46:17 +00:00
Alexandre Aubin
ccc005fb84
Merge pull request #312 from YunoHost/pyupgrade
Pyupgrade
2021-12-29 00:45:49 +01:00
Alexandre Aubin
37d43d1bdf
Fix weird format syntax 2021-12-29 00:40:01 +01:00
Alexandre Aubin
d10879faa2
Merge pull request #310 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2021-12-27 15:25:59 +01:00
Alexandre Aubin
83a95fd8c9 Unused import 2021-12-26 18:13:48 +01:00
Alexandre Aubin
ce8322bbf4 Fix tests 2021-12-26 18:12:54 +01:00
Alexandre Aubin
ef08a8be53 actionsmap: add global flag to disable cache and lock 2021-12-25 15:41:09 +01:00
Laurent Peuch
9855b6d7f5 [mod] stop using old style class 2021-12-24 01:18:11 +01:00
Laurent Peuch
8127e7cd1a [mod] run pyupgrade on source code 2021-12-24 01:18:03 +01:00
Alexandre Aubin
964483b23b Fix issues in previous commits regarding authentication mecanism 2021-12-23 16:57:43 +01:00
Alexandre Aubin
ee1e63c7a1 Propagate changes on tests 2021-12-22 19:27:56 +01:00
Alexandre Aubin
2fc9611b53 api: Move cookie session management logic to the authenticator for more flexibility 2021-12-22 19:06:33 +01:00
Valentin von Guttenberg
265753aef7 Translated using Weblate (German)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/de/
2021-11-29 18:44:42 +00:00
maique madeira
a53dd3d175 Translated using Weblate (Portuguese)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/pt/
2021-11-20 10:48:00 +00:00
Radek S
5b506a5f70 Translated using Weblate (Czech)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/cs/
2021-11-19 22:07:05 +00:00
Christian Wehrli
27bbd81f49 Translated using Weblate (German)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/de/
2021-11-19 13:48:06 +00:00
Alexandre Aubin
9fcc9630bd Rework actionsmap and m18n init, drop multiple actionsmap support 2021-11-17 19:24:26 +01:00
Alexandre Aubin
b2c67369a8 Merge branch 'dev' into bullseye 2021-11-15 19:14:40 +01:00
Alexandre Aubin
845399dba0 Update changelog for 4.3.2.2 2021-11-15 18:54:02 +01:00
Alexandre Aubin
96b7cb237e Update changelog for 4.3.2.1 2021-11-15 18:44:58 +01:00
Alexandre Aubin
be3b9f7a53
Merge pull request #309 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2021-11-15 18:43:29 +01:00
Alexandre Aubin
b7c05f1daa
fix i18n string format 2021-11-15 18:43:18 +01:00
Flavio Cristoforetti
cb6e0464ad Translated using Weblate (Italian)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/it/
2021-11-15 17:32:34 +00:00
dagangtie
166fd7e824 Translated using Weblate (Chinese (Simplified))
Currently translated at 97.7% (44 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/zh_Hans/
2021-11-15 17:32:34 +00:00
Alexandre Aubin
c5700f1ba3 Fix issue with accented chars in form .. decode() is not needed anymore? 2021-11-15 18:31:56 +01:00
Alexandre Aubin
c5f577c04f Fix 'Missing credentials parameter' bug : request.POST is buggy when value contains special chars ... request.params appears to be more reliable 2021-11-15 18:07:46 +01:00
Alexandre Aubin
fc4b31bdea Update changelog for 4.3.2 2021-11-05 02:38:24 +01:00
Alexandre Aubin
40ca5a8ad6 Update changelog for 4.3.2 2021-11-05 02:36:25 +01:00
Alexandre Aubin
56ce07d114 Update changelog for 4.3.1.2 2021-11-03 18:43:20 +01:00
Alexandre Aubin
1c412bf8ea
Merge pull request #308 from yunohost-bot/weblate-yunohost-moulinette
Translations update from Weblate
2021-11-03 18:29:47 +01:00
punkrockgirl
a745c93a58 Translated using Weblate (Basque)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/eu/
2021-10-31 14:11:21 +00:00
punkrockgirl
891216dec4 Translated using Weblate (Basque)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/eu/
2021-10-30 01:40:59 +00:00
Page Asgardius
7694024c58 Translated using Weblate (Spanish)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/es/
2021-10-23 18:16:35 +00:00
punkrockgirl
07b6779eac Translated using Weblate (Basque)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/eu/
2021-10-21 10:29:06 +00:00
punkrockgirl
d997e5f912 Translated using Weblate (Basque)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/eu/
2021-10-20 23:40:28 +00:00
punkrockgirl
551ad28ed2 Translated using Weblate (Basque)
Currently translated at 93.3% (42 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/eu/
2021-10-20 20:32:29 +00:00
Alexandre Aubin
c44e025210 cli prompt: fix bottom toolbar displayed even if message is empty 2021-10-20 18:31:29 +02:00
Alexandre Aubin
5d61b74fde ci: unused imports 2021-10-20 18:25:25 +02:00
Alexandre Aubin
12218bcbae setup.py: bump prompt-toolkit version requirement 2021-10-20 11:38:47 +02:00
Alexandre Aubin
d901d28add
Adapt prompt_toolkit call because now using the v3.x of the lib 2021-10-19 20:18:20 +02:00
punkrockgirl
9130574191 Translated using Weblate (Basque)
Currently translated at 93.3% (42 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/eu/
2021-10-19 08:20:07 +00:00
punkrockgirl
aaeeb0a02e Translated using Weblate (Basque)
Currently translated at 15.5% (7 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/eu/
2021-10-18 15:26:27 +00:00
Semen Turchikhin
f2ffcd09b2 Translated using Weblate (Russian)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/ru/
2021-10-18 10:39:40 +00:00
Alexandre Aubin
824a9ed42e Merge branch 'dev' into bullseye 2021-10-13 14:59:30 +02:00
Weblate
b3d950ca4b Added translation using Weblate (Slovenian) 2021-10-12 22:39:21 +00:00
Quentí
84ccb993ab Translated using Weblate (Occitan)
Currently translated at 100.0% (45 of 45 strings)

Translation: YunoHost/moulinette
Translate-URL: https://translate.yunohost.org/projects/yunohost/moulinette/oc/
2021-10-09 01:02:16 +00:00
Alexandre Aubin
63defa3926 Update changelog for 4.3.1.1 2021-10-04 01:29:30 +02:00
Alexandre Aubin
8ef882b4f5 Merge branch 'dev' into bullseye 2021-10-02 19:59:09 +02:00
Alexandre Aubin
14e37366df Add cp helper 2021-10-01 20:18:46 +02:00
Alexandre Aubin
f25d202fee
swag: Add version badge 2021-10-01 17:50:25 +02:00
Kayou
8218c2061a
moulinette is now compatible with python 3.9 \o/ 2021-09-01 17:46:39 +02:00
Kayou
3cb81a54fc
adding py39 to tox 2021-09-01 17:23:27 +02:00
Kay0u
4cacaefbfe
Run tests on branch bullseye too 2021-09-01 17:01:37 +02:00
Kay0u
bdec23b533
Run tests on python 3.9 2021-09-01 17:00:29 +02:00
Kay0u
4774e2a0a9
Merge branch 'dev' into bullseye 2021-09-01 16:58:54 +02:00
Alexandre Aubin
7c89e44404 Bullseye: idk what i'm doing but let's try to bump compat to 13 2021-05-28 01:06:18 +02:00
Alexandre Aubin
c1c517d5b2 Update changelog for 11.0 2021-05-28 01:06:18 +02:00
76 changed files with 1057 additions and 887 deletions

View file

@ -8,16 +8,23 @@ jobs:
name: Check / auto apply black
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Check files using the black formatter
uses: rickstaa/action-black@v1
id: action_black
uses: psf/black@stable
id: black
with:
black_args: "."
options: "."
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
if: steps.action_black.outputs.is_formatted == 'true'
uses: peter-evans/create-pull-request@v3
if: steps.check_files_changed.outputs.files_changed == 'true'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
title: "Format Python code with Black"

View file

@ -8,7 +8,7 @@ jobs:
name: Autoreformat locale files
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Apply reformating scripts
id: action_reformat
run: |
@ -18,7 +18,7 @@ jobs:
git diff -w --exit-code
- name: Create Pull Request
if: ${{ failure() }}
uses: peter-evans/create-pull-request@v3
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
title: "Reformat locale files"

View file

@ -4,6 +4,7 @@ on:
push:
branches:
- dev
- bullseye
pull_request:
jobs:
@ -11,11 +12,11 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7]
python-version: [3.9]
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install apt dependencies
@ -25,17 +26,17 @@ jobs:
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Test with tox
run: tox -e py37-pytest
run: tox -e py39-pytest
invalidcode:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7]
python-version: [3.9]
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install tox
@ -43,6 +44,6 @@ jobs:
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Linter
run: tox -e py37-invalidcode
run: tox -e py39-invalidcode
- name: Mypy
run: tox -e py37-mypy
run: tox -e py39-mypy

View file

@ -2,7 +2,9 @@
<div align="center">
![Version](https://img.shields.io/github/v/tag/yunohost/moulinette?label=version&sort=semver)
[![Tests status](https://github.com/YunoHost/moulinette/actions/workflows/tox.yml/badge.svg)](https://github.com/YunoHost/moulinette/actions/workflows/tox.yml)
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/YunoHost/moulinette.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/YunoHost/moulinette/context:python)
[![GitHub license](https://img.shields.io/github/license/YunoHost/moulinette)](https://github.com/YunoHost/moulinette/blob/dev/LICENSE)

160
debian/changelog vendored
View file

@ -1,3 +1,163 @@
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))

1
debian/compat vendored
View file

@ -1 +0,0 @@
9

2
debian/control vendored
View file

@ -2,7 +2,7 @@ Source: moulinette
Section: python
Priority: optional
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
Homepage: https://github.com/YunoHost/moulinette

View file

@ -65,18 +65,18 @@ source_suffix = ".rst"
master_doc = "index"
# General information about the project.
project = u"Moulinette"
copyright = u"2017, YunoHost Collective"
author = u"YunoHost Collective"
project = "Moulinette"
copyright = "2017, YunoHost Collective"
author = "YunoHost Collective"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u"2.6.1"
version = "2.6.1"
# 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
# for a list of supported languages.
@ -161,8 +161,8 @@ latex_documents = [
(
master_doc,
"Moulinette.tex",
u"Moulinette Documentation",
u"YunoHost Collective",
"Moulinette Documentation",
"YunoHost Collective",
"manual",
),
]
@ -172,7 +172,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (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 -------------------------------------------
@ -184,7 +184,7 @@ texinfo_documents = [
(
master_doc,
"Moulinette",
u"Moulinette Documentation",
"Moulinette Documentation",
author,
"Moulinette",
"One line description of project.",

View file

@ -34,7 +34,7 @@ ldapsearch -x -b 'dc=nodomain' | \\
import sys
class Element(object):
class Element:
"""Represents an LDIF entry."""
def __init__(self):
@ -43,7 +43,7 @@ class Element(object):
def __repr__(self):
"""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):
"""Adds a line of input to the object.
@ -57,10 +57,10 @@ class Element(object):
"""
def _valid(line):
return line and not line.startswith('#')
return line and not line.startswith("#")
def _interesting(line):
return line != 'objectClass: top'
return line != "objectClass: top"
if self.is_valid() and not _valid(line):
return True
@ -70,11 +70,11 @@ class Element(object):
def is_valid(self):
"""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):
"""Returns the DN for this entry."""
if self.attributes[0].startswith('dn: '):
if self.attributes[0].startswith("dn: "):
return self.attributes[0][4:]
else:
return None
@ -86,12 +86,12 @@ class Element(object):
Element objects) and returns a string which declares a DOT edge, or an
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):
parent = ','.join(dn_components[i:])
parent = ",".join(dn_components[i:])
if parent in dnmap:
return ' n%d->n%d\n' % (dnmap[parent].index, self.index)
return ''
return " n%d->n%d\n" % (dnmap[parent].index, self.index)
return ""
def dot(self, dnmap):
"""Returns a text representation of the node and perhaps its parent edge.
@ -99,6 +99,7 @@ class Element(object):
Args:
- dnmap: dictionary mapping dn names to Element objects
"""
def _format(attributes):
result = [TITLE_ENTRY_TEMPALTE % attributes[0]]
@ -107,9 +108,14 @@ class Element(object):
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."""
def __init__(self):
@ -144,7 +150,11 @@ class Converter(object):
e = Element()
if e.is_valid():
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 = """\
strict digraph "%s" {
@ -191,13 +201,13 @@ ENTRY_TEMPALTE = """\
"""
if __name__ == '__main__':
if __name__ == "__main__":
if len(sys.argv) > 2:
raise 'Expected at most one argument.'
raise "Expected at most one argument."
elif len(sys.argv) == 2:
name = sys.argv[1]
file = open(sys.argv[1], 'r')
file = open(sys.argv[1], "r")
else:
name = '<stdin>'
name = "<stdin>"
file = sys.stdin
print Converter().parse(file, name)
print(Converter().parse(file, name))

View file

@ -28,18 +28,20 @@
"cannot_open_file": "ليس بالإمكان فتح الملف {file} (السبب : {error})",
"cannot_write_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_removing": "خطأ أثناء عملية حذف {path}: {error}",
"error_changing_file_permissions": "خطأ أثناء عملية تعديل التصريحات لـ {path}: {error}",
"invalid_url": "خطأ في عنوان الرابط {url} (هل هذا الموقع موجود حقًا ؟)",
"invalid_url": "فشل الاتصال بـ {url}… ربما تكون الخدمة معطلة ، أو أنك غير متصل بشكل صحيح بالإنترنت في IPv4 / IPv6.",
"download_ssl_error": "خطأ في الاتصال الآمن عبر الـ SSL أثناء محاولة الربط بـ {url}",
"download_timeout": "{url} استغرق مدة طويلة جدا للإستجابة، فتوقّف.",
"download_unknown_error": "خطأ أثناء عملية تنزيل البيانات مِن {url} : {error}",
"download_bad_status_code": "{url} أعاد رمز الحالة {code}",
"corrupted_yaml": "قراءة مُشوّهة لنسق yaml مِن {ressource} (السبب : {error})",
"corrupted_yaml": "قراءة مُشوّهة لملف YAML مِن {ressource} (السبب : {error})",
"info": "معلومة:",
"warn_the_user_about_waiting_lock_again": "جارٍ الانتظار…",
"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})"
}

View file

@ -1,7 +1,7 @@
{
"argument_required": "Es requereix l'argument {argument}",
"argument_required": "Es requereix l'argument «{argument}»",
"authentication_required": "Es requereix autenticació",
"confirm": "Confirmar{prompt}",
"confirm": "Confirmar {prompt}",
"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",
"error": "Error:",
@ -33,7 +33,7 @@
"error_writing_file": "Error al escriure el fitxer {file}: {error}",
"error_removing": "Error al eliminar {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_timeout": "{url} ha tardat massa en respondre, s'ha deixat d'esperar.",
"download_unknown_error": "Error al baixar dades des de {url}: {error}",
@ -42,5 +42,6 @@
"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_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]: "
}

View file

@ -34,13 +34,14 @@
"error_writing_file": "写入文件{file}失败:{error}",
"error_removing": "删除路径{path}失败:{error}",
"error_changing_file_permissions": "目录{path}权限修改失败:{error}",
"invalid_url": "URL{url}无效site是否存在",
"invalid_url": "{url} 连接失败… 可能是服务中断了或者你没有正确连接到IPv4/IPv6的互联网。",
"download_ssl_error": "连接{url}时发生SSL错误",
"download_timeout": "{url}响应超时,放弃。",
"download_unknown_error": "下载{url}失败:{error}",
"download_bad_status_code": "{url}返回状态码:{code}",
"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命令我们在运行此命令之前等待它完成",
"corrupted_toml": "从{ressources}读取的TOML损坏原因{errors}"
"corrupted_toml": "从{ressource}读取的TOML已损坏原因{error}",
"edit_text_question": "{}.编辑此文本?[yN]: "
}

View file

@ -2,13 +2,13 @@
"password": "Heslo",
"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_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í",
"download_bad_status_code": "{url} vrátil stavový kód {code}",
"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_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_removing": "Chyba při přesunu {path}: {error}",
"error_writing_file": "Chyba při zápisu souboru/ů {file}: {error}",
@ -42,5 +42,6 @@
"deprecated_command": "'{prog} {command}' je zastaralý a bude odebrán v budoucích verzích",
"confirm": "Potvrdit {prompt}",
"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
View file

@ -0,0 +1 @@
{}

View file

@ -1,9 +1,9 @@
{
"argument_required": "Der Parameter {argument} ist erforderlich",
"authentication_required": "Anmeldung erforderlich",
"confirm": "Bestätige {prompt}",
"confirm": "Bestätigen Sie {prompt}",
"error": "Fehler:",
"file_not_exist": "Datei ist nicht vorhanden: '{path}'",
"file_not_exist": "Datei ist nicht vorhanden: '{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.",
"invalid_argument": "Argument ungültig '{argument}': {error}",
@ -24,7 +24,7 @@
"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",
"unknown_group": "Gruppe '{group}' ist unbekannt",
"unknown_user": "Benutzer '{user}' ist unbekannt",
"unknown_user": "Konto '{user}' ist unbekannt",
"info": "Info:",
"corrupted_json": "Beschädigtes JSON gelesen von {ressource} (reason: {error})",
"unknown_error_reading_file": "Unbekannter Fehler beim Lesen der Datei {file} (reason: {error})",
@ -32,15 +32,16 @@
"cannot_open_file": "Datei {file} konnte nicht geöffnet werden (Ursache: {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_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",
"download_bad_status_code": "{url} lieferte folgende(n) Status Code(s) {code}",
"download_unknown_error": "Fehler beim Herunterladen von Daten von {url}: {error}",
"download_timeout": "{url} brauchte zu lange zum Antworten, hab aufgegeben.",
"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_removing": "Fehler beim Entfernen {path}: {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]: "
}

View file

@ -36,7 +36,7 @@
"error_writing_file": "Error when writing file {file}: {error}",
"error_removing": "Error when removing {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_timeout": "{url} took too long to answer, gave up.",
"download_unknown_error": "Error when downloading data from {url}: {error}",

View file

@ -1,7 +1,7 @@
{
"password": "Pasvorto",
"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",
"download_bad_status_code": "{url} redonita statuskodo {code}",
"download_unknown_error": "Eraro dum elŝutado de datumoj de {url}: {error}",
@ -16,7 +16,7 @@
"corrupted_json": "Koruptita JSON legis de {ressource} (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_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",
"warning": "Averto:",
"values_mismatch": "Valoroj ne kongruas",

View file

@ -32,7 +32,7 @@
"error_writing_file": "Error al escribir el archivo {file}: {error}",
"error_removing": "Error al eliminar {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_timeout": "{url} tardó demasiado en responder, abandono.",
"download_unknown_error": "Error al descargar datos desde {url} : {error}",
@ -41,6 +41,7 @@
"info": "Información:",
"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_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_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",
"edit_text_question": "{}. Editar este texto ? [sN]: "
}

View file

@ -1,5 +1,47 @@
{
"argument_required": "'{argument}' argumentua beharrezkoa da",
"logged_out": "Saioa amaitu",
"password": "Pasahitza"
}
"argument_required": "'{argument}' argumentua ezinbestekoa da",
"logged_out": "Saioa amaituta",
"password": "Pasahitza",
"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."
}

View file

@ -14,13 +14,13 @@
"argument_required": "استدلال '{argument}' ضروری است",
"password": "کلمه عبور",
"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 در حال اجرا است ، ما منتظر هستیم تا قبل از اجرای این دستور به پایان برسد",
"download_bad_status_code": "{url} کد وضعیّت بازگشتی {code}",
"download_unknown_error": "خطا هنگام بارگیری داده ها از {url}: {error}",
"download_timeout": "پاسخ {url} خیلی طول کشید ، منصرف شو.",
"download_ssl_error": "خطای SSL هنگام اتصال به {url}",
"invalid_url": "اتصال به {url} انجام نشد ... شاید سرویس خاموش باشد یا در IPv4/IPv6 به درستی به اینترنت متصل نشده باشید.",
"invalid_url": "اتصال به {url} انجام نشد شاید سرویس خاموش باشد یا در IPv4/IPv6 به درستی به اینترنت متصل نشده باشید.",
"error_changing_file_permissions": "خطا هنگام تغییر مجوزهای {path}: {error}",
"error_removing": "خطا هنگام حذف {path}: {error}",
"error_writing_file": "خطا هنگام نوشتن فایل {file}: {error}",

View file

@ -1 +1,4 @@
{}
{
"password": "Salasana",
"logged_out": "Kirjauduttu ulos"
}

View file

@ -4,11 +4,11 @@
"confirm": "Confirmez {prompt}",
"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",
"error": "Erreur :",
"error": "Erreur:",
"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.",
"invalid_argument": "Argument '{argument}' incorrect : {error}",
"invalid_argument": "Argument '{argument}' incorrect: {error}",
"invalid_usage": "Utilisation erronée, utilisez --help pour accéder à l'aide",
"logged_in": "Connecté",
"logged_out": "Déconnecté",
@ -16,32 +16,32 @@
"operation_interrupted": "Opération interrompue",
"password": "Mot de passe",
"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",
"success": "Succès !",
"success": "Succès!",
"unable_authenticate": "Impossible de vous authentifier",
"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",
"warning": "Attention :",
"warning": "Attention:",
"websocket_request_expected": "Une requête WebSocket est attendue",
"cannot_open_file": "Impossible d'ouvrir 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})",
"corrupted_json": "Fichier JSON corrompu en lecture depuis {ressource} (raison : {error})",
"error_writing_file": "Erreur en écrivant le fichier {file} : {error}",
"error_removing": "Erreur lors de la suppression {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.",
"cannot_open_file": "Impossible d'ouvrir 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})",
"corrupted_json": "Fichier JSON corrompu en lecture depuis {ressource} (raison: {error})",
"error_writing_file": "Erreur en écrivant le fichier {file}: {error}",
"error_removing": "Erreur lors de la suppression {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 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_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_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_bad_status_code": "{url} renvoie le code d'état {code}",
"corrupted_yaml": "Fichier YAML corrompu en lecture depuis {ressource} (raison : {error})",
"info": "Info :",
"corrupted_toml": "Fichier TOML corrompu en lecture depuis {ressource} (raison : {error})",
"corrupted_yaml": "Fichier YAML corrompu en lecture depuis {ressource} (raison: {error})",
"info": "Info:",
"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_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",
"edit_text_question": "{}. Modifier ce texte ? [yN] : "
}
"edit_text_question": "{}. Modifier ce texte? [yN]: "
}

View file

@ -23,16 +23,16 @@
"root_required": "Tes que ser root para facer esta acción",
"pattern_not_match": "Non concorda co patrón",
"operation_interrupted": "Interrumpeuse a operación",
"not_logged_in": "Non estás conectada",
"logged_in": "Conectada",
"not_logged_in": "Non iniciaches sesión",
"logged_in": "Sesión iniciada",
"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",
"download_bad_status_code": "{url} devolveu o código de estado {code}",
"download_unknown_error": "Erro ao descargar os datos desde {url}: {error}",
"download_timeout": "{url} está tardando en responder, deixámolo.",
"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_removing": "Erro ao eliminar {path}: {error}",
"error_writing_file": "Erro ao escribir o ficheiro {file}: {error}",
@ -44,4 +44,4 @@
"cannot_open_file": "Non se puido abrir o ficheiro {file} (razón: {error})",
"websocket_request_expected": "Agardábase unha solicitude WebSocket",
"edit_text_question": "{}. Editar este texto ? [yN]: "
}
}

1
locales/he.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -1,19 +1,47 @@
{
"argument_required": "Argumen '{argument}' dibutuhkan",
"authentication_required": "Otentikasi 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}' saja",
"deprecated_command_alias": "'{prog} {old}' sudah usang dan akan dihapus nanti, gunakan '{prog} {new}'",
"info": "Informasi:",
"instance_already_running": "Sudah ada operasi YunoHost yang sedang berjalan. Tunggu itu selesai sebelum menjalankan yang lain.",
"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 kesalahan ketika menghapus {path}: {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 mengotentikasi",
"warn_the_user_that_lock_is_acquired": "Perintah yang tadi baru saja selesai, akan memulai perintah ini"
"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"
}

View file

@ -33,14 +33,15 @@
"error_writing_file": "Errore durante la scrittura del file {file}: {error}",
"error_removing": "Errore durante la rimozione {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_timeout": "{url} ci ha messo troppo a rispondere, abbandonato.",
"download_unknown_error": "Errore durante il download di dati da {url} : {error}",
"download_bad_status_code": "{url} ha restituito il codice di stato {code}",
"info": "Info:",
"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",
"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
View 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
View 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
View file

@ -0,0 +1 @@
{}

1
locales/lt.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -7,7 +7,7 @@
"unknown_user": "Ukjent '{user}' bruker",
"unknown_group": "Ukjent '{group}' gruppe",
"unable_authenticate": "Kunne ikke identitetsbekrefte",
"success": "Vellykket.",
"success": "Vellykket!",
"operation_interrupted": "Operasjon forstyrret",
"not_logged_in": "Du er ikke innlogget",
"logged_in": "Innlogget",

View file

@ -5,7 +5,7 @@
"error": "Fout:",
"file_not_exist": "Bestand bestaat niet: '{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_usage": "Ongeldig gebruik, doe --help om de hulptekst te lezen",
"logged_in": "Ingelogd",
@ -28,19 +28,20 @@
"cannot_open_file": "Niet mogelijk om bestand {file} te openen (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})",
"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_removing": "Fout tijdens het verwijderen van {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_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_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_about_waiting_lock_again": "Nog steeds aan het wachten...",
"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": "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_yaml": "Ongeldig YAML bestand op {ressource} (reason: {error})",
"info": "Ter info:"
"corrupted_toml": "Ongeldige TOML werd gelezen van {ressource} (reason: {error})",
"corrupted_yaml": "Ongeldig YAML bestand op {ressource} (reden: {error})",
"info": "Ter info:",
"edit_text_question": "{}. Deze tekst bewerken ? [yN]: "
}

View file

@ -42,5 +42,6 @@
"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 quacabe abans daviar aquesta daquí",
"warn_the_user_about_waiting_lock_again": "Encara en espèra…",
"warn_the_user_that_lock_is_acquired": "lautra comanda ven dacabar, ara lançament daquesta comanda"
"warn_the_user_that_lock_is_acquired": "lautra comanda ven dacabar, ara lançament daquesta comanda",
"edit_text_question": "{}. Modificar aqueste tèxte? [yN]: "
}

View file

@ -2,17 +2,17 @@
"logged_out": "Wylogowano",
"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_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_again": "Wciąż czekam",
"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_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}",
"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_removing": "Błąd podczas usuwania {path}: {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_json": "Uszkodzony JSON odczytany z {ressource} (reason: {error})",
"unknown_error_reading_file": "Nieznany błąd podczas próby odczytania pliku {file} (przyczyna: {error})",
@ -31,10 +31,10 @@
"operation_interrupted": "Operacja przerwana",
"not_logged_in": "Nie jesteś 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}",
"instance_already_running": "Trwa już operacja YunoHost. Zaczekaj na zakończenie, zanim uruchomisz kolejny.",
"info": "Informacje:",
"instance_already_running": "Trwa już operacja YunoHost. Zaczekaj na jej zakończenie, zanim uruchomisz kolejną.",
"info": "Info:",
"folder_exists": "Folder już istnieje: „{path}”",
"file_not_exist": "Plik nie istnieje: „{path}”",
"error": "Błąd:",
@ -42,5 +42,6 @@
"deprecated_command": "„{prog} {command}” jest przestarzałe i zostanie usunięte w przyszłości",
"confirm": "Potwierdź {prompt}",
"authentication_required": "Wymagane uwierzytelnienie",
"argument_required": "Argument „{argument}” jest wymagany"
"argument_required": "Argument „{argument}” jest wymagany",
"edit_text_question": "{}. Zedytować ten tekst? [tN]: "
}

View file

@ -39,8 +39,9 @@
"corrupted_json": "JSON 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_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",
"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
View file

@ -0,0 +1 @@
{}

View file

@ -1,5 +1,5 @@
{
"argument_required": "Требуется'{argument}' аргумент",
"argument_required": "Требуется аргумент «{argument}»",
"authentication_required": "Требуется аутентификация",
"confirm": "Подтвердить {prompt}",
"deprecated_command": "'{prog} {command}' устарела и будет удалена",
@ -10,7 +10,7 @@
"invalid_argument": "Неправильный аргумент '{argument}': {error}",
"logged_in": "Вы вошли",
"logged_out": "Вы вышли из системы",
"not_logged_in": "Вы не залогинились",
"not_logged_in": "Вы не вошли в систему",
"operation_interrupted": "Действие прервано",
"password": "Пароль",
"pattern_not_match": "Не соответствует образцу",
@ -28,19 +28,20 @@
"corrupted_yaml": "Повреждённой YAML получен от {ressource} (причина: {error})",
"error_writing_file": "Ошибка при записи файла {file}: {error}",
"error_removing": "Ошибка при удалении {path}: {error}",
"invalid_url": "Неправильный url {url} (этот сайт существует ?)",
"invalid_url": "Не удалось подключиться к {url}... возможно этот сервис недоступен или вы не подключены к Интернету через IPv4/IPv6.",
"download_ssl_error": "Ошибка SSL при соединении с {url}",
"download_timeout": "Превышено время ожидания ответа от {url}.",
"download_unknown_error": "Ошибка при загрузке данных с {url} : {error}",
"instance_already_running": "Операция YunoHost уже запущена. Пожалуйста, подождите, пока он закончится, прежде чем запускать другой.",
"root_required": "Чтобы выполнить это действие, вы должны иметь права root",
"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": "Сейчас запускается еще одна команда YunoHost, мы ждем ее завершения, прежде чем запустить эту",
"download_bad_status_code": "{url} вернул код состояния {code}",
"error_changing_file_permissions": "Ошибка при изменении разрешений для {path}: {error}",
"corrupted_toml": "Поврежденный TOML, прочитанный из {ressource} (причина: {error})",
"invalid_usage": "Неправильное использование, передайте --help, чтобы увидеть помощь",
"info": "Информация:"
"info": "Информация:",
"edit_text_question": "{}. Изменить этот текст? [yN]: "
}

47
locales/sk.json Normal file
View 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
View file

@ -0,0 +1 @@
{}

View file

@ -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_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}",

1
locales/te.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -19,19 +19,19 @@
"warning": "Uyarı:",
"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_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",
"download_bad_status_code": "{url} döndürülen durum kodu {code}",
"download_unknown_error": "{url} adresinden veri indirilirken hata oluştu: {error}",
"download_timeout": "{url} yanıtlaması çok uzun sürdü, pes etti.",
"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_removing": "{path} kaldırı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_yaml": "{ressource} kaynağından bozuk yaml okunuyor (nedeni: {error})",
"corrupted_json": "{ressource} adresinden okunan bozuk json (nedeni: {error})",
"corrupted_yaml": "{ressource} kaynağından bozuk YAML okunuyor (neden: {error})",
"corrupted_json": "{ressource} adresinden okunan bozuk json (neden: {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_open_file": "{file} dosyasıılamadı (nedeni: {error})",
@ -42,5 +42,6 @@
"folder_exists": "Klasör zaten var: '{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": "'{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]: "
}

View file

@ -1,9 +1,9 @@
{
"password": "Пароль",
"logged_out": "Ви вийшли з системи",
"invalid_url": "Помилка з'єднання із {url}... можливо, служба не працює, або ви неправильно під'єднані до Інтернету в IPv4/IPv6.",
"invalid_url": "Помилка з'єднання із {url} можливо, служба не працює, або ви неправильно під'єднані до Інтернету в IPv4/IPv6.",
"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, ми чекаємо її завершення, перш ніж запустити цю",
"download_bad_status_code": "{url} повернув код стану {code}",
"download_unknown_error": "Помилка під час завантаження даних з {url}: {error}",

View 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'

View file

@ -3,7 +3,6 @@
from moulinette.core import (
MoulinetteError,
Moulinette18n,
env,
)
__title__ = "moulinette"
@ -31,7 +30,7 @@ __all__ = ["init", "api", "cli", "m18n", "MoulinetteError", "Moulinette"]
m18n = Moulinette18n()
class classproperty(object):
class classproperty:
def __init__(self, f):
self.f = f
@ -40,7 +39,6 @@ class classproperty(object):
class Moulinette:
_interface = None
def prompt(*args, **kwargs):
@ -54,35 +52,8 @@ class Moulinette:
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
def api(host="localhost", port=80, routes={}):
def api(host="localhost", port=80, routes={}, actionsmap=None, locales_dir=None):
"""Web server (API) interface
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
m18n.set_locales_dir(locales_dir)
try:
Api(routes=routes).run(host, port)
Api(
routes=routes,
actionsmap=actionsmap,
).run(host, port)
except MoulinetteError as e:
import logging
@ -110,7 +86,9 @@ def api(host="localhost", port=80, routes={}):
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
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
m18n.set_locales_dir(locales_dir)
try:
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(
args, output_as=output_as, timeout=timeout
)
Cli(
top_parser=top_parser,
load_only_category=load_only_category,
actionsmap=actionsmap,
).run(args, output_as=output_as, timeout=timeout)
except MoulinetteError as e:
import logging

View file

@ -10,15 +10,15 @@ from typing import List, Optional
from time import time
from collections import OrderedDict
from importlib import import_module
from functools import cache
from moulinette import m18n, Moulinette
from moulinette.core import (
MoulinetteError,
MoulinetteLock,
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.filesystem import read_yaml
@ -30,7 +30,7 @@ logger = logging.getLogger("moulinette.actionsmap")
# Extra parameters definition
class _ExtraParameter(object):
class _ExtraParameter:
"""
Argument parser for an extra parameter.
@ -107,7 +107,6 @@ class CommentParameter(_ExtraParameter):
class AskParameter(_ExtraParameter):
"""
Ask for the argument value if possible and needed.
@ -146,7 +145,6 @@ class AskParameter(_ExtraParameter):
class PasswordParameter(AskParameter):
"""
Ask for the password argument value if possible and needed.
@ -169,7 +167,6 @@ class PasswordParameter(AskParameter):
class PatternParameter(_ExtraParameter):
"""
Check if the argument value match a pattern.
@ -222,7 +219,6 @@ class PatternParameter(_ExtraParameter):
class RequiredParameter(_ExtraParameter):
"""
Check if a required argument is defined or not.
@ -261,8 +257,7 @@ extraparameters_list = [
# Extra parameters argument Parser
class ExtraArgumentParser(object):
class ExtraArgumentParser:
"""
Argument validator and parser for the extra parameters.
@ -373,8 +368,7 @@ class ExtraArgumentParser(object):
# Main class ----------------------------------------------------------
class ActionsMap(object):
class ActionsMap:
"""Validate and process actions defined into an actions map
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).
Moreover, the action can have specific argument(s).
This class allows to manipulate one or several actions maps
associated to a namespace.
Keyword arguments:
- top_parser -- A BaseActionsMapParser-derived instance to use for
parsing the actions map
@ -394,92 +385,82 @@ class ActionsMap(object):
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), (
"Invalid parser class '%s'" % top_parser.__class__.__name__
)
DATA_DIR = env["DATA_DIR"]
CACHE_DIR = env["CACHE_DIR"]
actionsmaps = OrderedDict()
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)
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,
)
logger.debug("loading actions map")
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
logger.debug("generating cache for actions map namespace '%s'", n)
actionsmap_pkl = f"{actionsmap_yml_dir}/.{actionsmap_yml_file}.{actionsmap_yml_stat.st_size}-{actionsmap_yml_stat.st_mtime}.pkl"
# Read actions map from yaml file
actionsmap = read_yaml(actionsmap_yml)
def generate_cache():
logger.debug("generating cache for actions map")
# Delete old cache files
for old_cache in glob.glob("%s/actionsmap/%s-*.pkl" % (CACHE_DIR, n)):
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)
# Read actions map from yaml file
actionsmap = read_yaml(actionsmap_yml)
if not actionsmap["_global"].get("cache", True):
return actionsmap
if os.path.exists(actionsmap_pkl):
try:
# Attempt to load cache
with open(actionsmap_pkl, "rb") as f:
actionsmaps[n] = pickle.load(f)
# Delete old cache files
for old_cache in glob.glob(
f"{actionsmap_yml_dir}/.{actionsmap_yml_file}.*.pkl"
):
os.remove(old_cache)
self.from_cache = True
# TODO: Switch to python3 and catch proper exception
except (IOError, EOFError):
actionsmaps[n] = generate_cache()
else: # cache file doesn't exists
actionsmaps[n] = generate_cache()
# at installation, cachedir might not exists
dir_ = os.path.dirname(actionsmap_pkl)
if not os.path.isdir(dir_):
os.makedirs(dir_)
# 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 actionsmaps[n]:
actionsmaps[n] = {
k: v
for k, v in actionsmaps[n].items()
if k in [load_only_category, "_global"]
}
# Cache actions map into pickle file
with open(actionsmap_pkl, "wb") as f:
pickle.dump(actionsmap, f)
# Load translations
m18n.load_namespace(n)
return actionsmap
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
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):
if auth_method == "default":
auth_method = self.default_authentication
# 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}")
try:
mod = import_module(auth_module)
@ -494,7 +475,6 @@ class ActionsMap(object):
return mod.Authenticator()
def check_authentication_if_required(self, *args, **kwargs):
auth_method = self.parser.auth_method(*args, **kwargs)
if auth_method is None:
@ -525,19 +505,17 @@ class ActionsMap(object):
tid = arguments.pop("_tid")
arguments = self.extraparser.parse_args(tid, arguments)
# Return immediately if a value is defined
if TO_RETURN_PROP in arguments:
return arguments.get(TO_RETURN_PROP)
want_to_take_lock = self.parser.want_to_take_lock(args, **kwargs)
# Retrieve action information
if len(tid) == 4:
namespace, category, subcategory, action = tid
func_name = "%s_%s_%s" % (
func_name = "{}_{}_{}".format(
category,
subcategory.replace("-", "_"),
action.replace("-", "_"),
)
full_action_name = "%s.%s.%s.%s" % (
full_action_name = "{}.{}.{}.{}".format(
namespace,
category,
subcategory,
@ -547,22 +525,22 @@ class ActionsMap(object):
assert len(tid) == 3
namespace, category, action = tid
subcategory = None
func_name = "%s_%s" % (category, action.replace("-", "_"))
full_action_name = "%s.%s.%s" % (namespace, category, action)
func_name = "{}_{}".format(category, action.replace("-", "_"))
full_action_name = "{}.{}.{}".format(namespace, category, action)
# Lock the moulinette for the namespace
with MoulinetteLock(namespace, timeout):
with MoulinetteLock(namespace, timeout, self.enable_lock and want_to_take_lock):
start = time()
try:
mod = __import__(
"%s.%s" % (namespace, category),
"{}.{}".format(namespace, category),
globals=globals(),
level=0,
fromlist=[func_name],
)
logger.debug(
"loading python module %s took %.3fs",
"%s.%s" % (namespace, category),
"{}.{}".format(namespace, category),
time() - start,
)
func = getattr(mod, func_name)
@ -570,7 +548,7 @@ class ActionsMap(object):
import traceback
traceback.print_exc()
error_message = "unable to load function %s.%s because: %s" % (
error_message = "unable to load function {}.{} because: {}".format(
namespace,
func_name,
e,
@ -591,7 +569,6 @@ class ActionsMap(object):
logger.debug("processing action [%s]: %s", log_id, full_action_name)
# Load translation and process the action
m18n.load_namespace(namespace)
start = time()
try:
return func(**arguments)
@ -599,43 +576,14 @@ class ActionsMap(object):
stop = time()
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
def _construct_parser(self, actionsmaps, top_parser):
def _construct_parser(self, actionsmap, top_parser):
"""
Construct the parser with the actions map
Keyword arguments:
- actionsmaps -- A dict of multi-level dictionnary of
categories/actions/arguments list for each namespaces
- actionsmap -- A dictionnary of categories/actions/arguments list
- top_parser -- A BaseActionsMapParser-derived instance to use for
parsing the actions map
@ -658,52 +606,86 @@ class ActionsMap(object):
# * namespace define the top "name", for us it will always be
# "yunohost" and there well be only this one
# * actionsmap is the actual actionsmap that we care about
for namespace, actionsmap in actionsmaps.items():
# Retrieve global parameters
_global = actionsmap.pop("_global", {})
if _global:
if getattr(self, "main_namespace", None) is not None:
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
]
# Retrieve global parameters
_global = actionsmap.pop("_global", {})
if top_parser.has_global_parser():
top_parser.add_global_arguments(_global["arguments"])
self.namespace = _global["namespace"]
self.enable_lock = _global.get("lock", True)
self.default_authentication = _global["authentication"][interface_type]
if not hasattr(self, "main_namespace"):
raise MoulinetteError("Did not found the main namespace", raw_msg=True)
# category_name is stuff like "user", "domain", "hooks"...
# 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():
# category_name is stuff like "user", "domain", "hooks"...
# category_values is the values of this category (like actions)
for category_name, category_values in actionsmap.items():
# Get category parser
category_parser = top_parser.add_category_parser(
category_name, **category_values
)
actions = category_values.pop("actions", {})
subcategories = category_values.pop("subcategories", {})
# action_name is like "list" of "domain list"
# 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
category_parser = top_parser.add_category_parser(
category_name, **category_values
# Get action parser
action_parser = category_parser.add_action_parser(
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
for action_name, action_options in actions.items():
arguments = action_options.pop("arguments", {})
authentication = action_options.pop("authentication", {})
tid = (namespace, category_name, action_name)
tid = (self.namespace, category_name, subcategory_name, action_name)
# Get action parser
action_parser = category_parser.add_action_parser(
action_name, tid, **action_options
)
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
@ -721,50 +703,13 @@ class ActionsMap(object):
if interface_type in authentication:
action_parser.authentication = authentication[interface_type]
# 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
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
]
# 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
logger.debug("building parser took %.3fs", time() - start)
return top_parser

View file

@ -10,8 +10,7 @@ logger = logging.getLogger("moulinette.authenticator")
# Base Class -----------------------------------------------------------
class BaseAuthenticator(object):
class BaseAuthenticator:
"""Authenticator base representation
Each authenticators must implement an Authenticator class derived
@ -29,7 +28,6 @@ class BaseAuthenticator(object):
# Each authenticator classes must implement these methods.
def authenticate_credentials(self, credentials):
try:
# Attempt to authenticate
auth_info = self._authenticate_credentials(credentials) or {}

View file

@ -9,19 +9,6 @@ import moulinette
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():
return "TESTS_RUN" in os.environ
@ -30,8 +17,7 @@ def during_unittests_run():
# Internationalization -------------------------------------------------
class Translator(object):
class Translator:
"""Internationalization class
Provide an internationalization mechanism based on JSON files to
@ -51,11 +37,7 @@ class Translator(object):
# Attempt to load default translations
if not self._load_translations(default_locale):
logger.error(
"unable to load locale '%s' from '%s'. Does the file '%s/%s.json' exists?",
default_locale,
locale_dir,
locale_dir,
default_locale,
f"unable to load locale '{default_locale}' from '{locale_dir}'. Does the file '{locale_dir}/{default_locale}.json' exists?",
)
self.default_locale = default_locale
@ -133,7 +115,6 @@ class Translator(object):
self.default_locale != self.locale
and key in self._translations.get(self.default_locale, {})
):
try:
return self._translations[self.default_locale][key].format(
*args, **kwargs
@ -190,8 +171,7 @@ class Translator(object):
return True
class Moulinette18n(object):
class Moulinette18n:
"""Internationalization service for the moulinette
Manage internationalization and access to the proper keys translation
@ -207,44 +187,22 @@ class Moulinette18n(object):
self.default_locale = default_locale
self.locale = default_locale
self.locales_dir = env["LOCALES_DIR"]
# 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._namespaces = {}
self._current_namespace = None
self._global = Translator(global_locale_dir, default_locale)
def load_namespace(self, namespace):
"""Load the namespace to use
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_locales_dir(self, locales_dir):
self.translator = Translator(locales_dir, self.default_locale)
def set_locale(self, locale):
"""Set the locale to use"""
self.locale = locale
self.locale = locale
self._global.set_locale(locale)
for n in self._namespaces.values():
n.set_locale(locale)
self.translator.set_locale(locale)
def g(self, key: str, *args, **kwargs) -> str:
"""Retrieve proper translation for a moulinette key
@ -269,7 +227,7 @@ class Moulinette18n(object):
- 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:
"""Check if a key exists in the translation files
@ -278,14 +236,13 @@ class Moulinette18n(object):
- key -- The key to translate
"""
return self._namespaces[self._current_namespace].key_exists(key)
return self.translator.key_exists(key)
# Moulinette core classes ----------------------------------------------
class MoulinetteError(Exception):
http_code = 500
"""Moulinette base exception"""
@ -303,17 +260,14 @@ class MoulinetteError(Exception):
class MoulinetteValidationError(MoulinetteError):
http_code = 400
class MoulinetteAuthenticationError(MoulinetteError):
http_code = 401
class MoulinetteLock(object):
class MoulinetteLock:
"""Locker for a moulinette instance
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"
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.timeout = timeout
self.interval = interval
self.enable_lock = enable_lock
self._lockfile = self.base_lockfile % namespace
self._stale_checked = False
@ -359,7 +314,6 @@ class MoulinetteLock(object):
logger.debug("acquiring lock...")
while True:
lock_pids = self._lock_PIDs()
if self._is_son_of(lock_pids):
@ -427,7 +381,6 @@ class MoulinetteLock(object):
raise MoulinetteError("root_required")
def _lock_PIDs(self):
if not os.path.isfile(self._lockfile):
return []
@ -460,7 +413,7 @@ class MoulinetteLock(object):
return False
def __enter__(self):
if not self._locked:
if self.enable_lock and not self._locked:
self.acquire()
return self

View file

@ -5,25 +5,19 @@ import logging
import argparse
import copy
import datetime
from collections import deque, OrderedDict
from collections import OrderedDict
from json.encoder import JSONEncoder
from typing import Optional
from moulinette import m18n
from moulinette.core import MoulinetteError
logger = logging.getLogger("moulinette.interface")
# FIXME : are these even used for anything useful ...
TO_RETURN_PROP = "_to_return"
CALLBACKS_PROP = "_callbacks"
# Base Class -----------------------------------------------------------
class BaseActionsMapParser(object):
class BaseActionsMapParser:
"""Actions map's base Parser
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__
)
# 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 ------------------------------------------------------
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):
"""Subparsers with extended properties for argparse
It provides the following additional properties at initialization,
@ -261,11 +168,14 @@ class _ExtendedSubParsersAction(argparse._SubParsersAction):
self._deprecated_command_map = {}
def add_parser(self, name, type_=None, **kwargs):
hide_in_help = kwargs.pop("hide_in_help", False)
deprecated = kwargs.pop("deprecated", False)
deprecated_alias = kwargs.pop("deprecated_alias", [])
if deprecated:
self._deprecated_command_map[name] = None
if deprecated or hide_in_help:
if "help" in kwargs:
del kwargs["help"]
@ -317,35 +227,8 @@ class ExtendedArgumentParser(argparse.ArgumentParser):
)
# Register additional actions
self.register("action", "callback", _CallbackAction)
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(
self, arguments, extraparser, format_arg_names=None, validate_extra=True
):
@ -398,11 +281,9 @@ class ExtendedArgumentParser(argparse.ArgumentParser):
# positionals, optionals and user-defined groups
for action_group in self._action_groups:
# Dirty hack to separate 'subcommands'
# into 'actions' and 'subcategories'
if action_group.title == "subcommands":
# Make a copy of the "action group actions"...
choice_actions = action_group._group_actions[0]._choices_actions
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
text_width = self._width - self._current_indent
if len(prefix) + len(usage) > text_width:
# break usage into wrappable parts
part_regexp = r"\(.*?\)+|\[.*?\]+|\S+"
opt_usage = format(optionals, groups)
@ -562,11 +442,10 @@ class PositionalsFirstHelpFormatter(argparse.HelpFormatter):
usage = "\n".join(lines)
# prefix with 'usage:'
return "%s%s\n\n" % (prefix, usage)
return "{}{}\n\n".format(prefix, usage)
class JSONExtendedEncoder(JSONEncoder):
"""Extended JSON encoder
Extend default JSON encoder to recognize more types and classes. It will
@ -579,7 +458,6 @@ class JSONExtendedEncoder(JSONEncoder):
"""
def default(self, o):
import pytz # Lazy loading, this takes like 3+ sec on a RPi2 ?!
"""Return a serializable object"""

View file

@ -29,7 +29,6 @@ from moulinette.interfaces import (
JSONExtendedEncoder,
)
from moulinette.utils import log
from moulinette.utils.text import random_ascii
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...
UPLOAD_DIR = None
CSRF_TYPES = set(
["text/plain", "application/x-www-form-urlencoded", "multipart/form-data"]
)
CSRF_TYPES = {"text/plain", "application/x-www-form-urlencoded", "multipart/form-data"}
def is_csrf():
@ -69,14 +66,12 @@ def filter_csrf(callback):
class LogQueues(dict):
"""Map of session ids to queue."""
pass
class APIQueueHandler(logging.Handler):
"""
A handler class which store logging records into a queue, to be used
and retrieved from the API.
@ -85,9 +80,19 @@ class APIQueueHandler(logging.Handler):
def __init__(self):
logging.Handler.__init__(self)
self.queues = LogQueues()
# actionsmap is actually set during the interface's init ...
self.actionsmap = None
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:
queue = self.queues[s_id]
except KeyError:
@ -101,8 +106,7 @@ class APIQueueHandler(logging.Handler):
sleep(0)
class _HTTPArgumentParser(object):
class _HTTPArgumentParser:
"""Argument parser for HTTP requests
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)
def dequeue_callbacks(self, *args, **kwargs):
return self._parser.dequeue_callbacks(*args, **kwargs)
def _error(self, message):
raise MoulinetteValidationError(message, raw_msg=True)
class Session:
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):
class _ActionsMapPlugin:
"""Actions map Bottle Plugin
Process relevant action for the request using the actions map and
@ -291,10 +250,8 @@ class _ActionsMapPlugin(object):
api = 2
def __init__(self, actionsmap, log_queues={}):
self.actionsmap = actionsmap
self.log_queues = log_queues
Session.actionsmap_name = actionsmap.name
def setup(self, app):
"""Setup plugin on the application
@ -331,7 +288,7 @@ class _ActionsMapPlugin(object):
)
# 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)
def apply(self, callback, context):
@ -358,7 +315,7 @@ class _ActionsMapPlugin(object):
params[a] = True
# 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
req_params += list(request.files.dict.items())
for k, v in req_params:
@ -393,21 +350,15 @@ class _ActionsMapPlugin(object):
"""
credentials = request.POST.credentials
# Apparently even if the key doesn't exists, request.POST.foobar just returns empty string...
if not credentials:
if "credentials" not in request.params:
raise HTTPResponse("Missing credentials parameter", 400)
credentials = request.params["credentials"]
profile = request.POST.profile
if not profile:
profile = self.actionsmap.default_authentication
profile = request.params.get("profile", self.actionsmap.default_authentication)
authenticator = self.actionsmap.get_authenticator(profile)
try:
auth_info = authenticator.authenticate_credentials(credentials)
session_infos = Session.get_infos(raise_if_no_session_exists=False)
session_infos[profile] = auth_info
auth_infos = authenticator.authenticate_credentials(credentials)
except MoulinetteError as e:
try:
self.logout()
@ -415,18 +366,13 @@ class _ActionsMapPlugin(object):
pass
raise HTTPResponse(e.strerror, 401)
else:
Session.set_infos(session_infos)
authenticator.set_session_cookie(auth_infos)
return m18n.g("logged_in")
# This is called before each time a route is going to be processed
def authenticate(self, authenticator):
try:
session_infos = Session.get_infos()[authenticator.name]
# 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...
session_infos = authenticator.get_session_cookie()
except Exception:
msg = m18n.g("authentication_required")
raise HTTPResponse(msg, 401)
@ -434,13 +380,16 @@ class _ActionsMapPlugin(object):
return session_infos
def logout(self):
profile = request.params.get("profile", self.actionsmap.default_authentication)
authenticator = self.actionsmap.get_authenticator(profile)
try:
Session.get_infos()
except KeyError:
authenticator.get_session_cookie()
except Exception:
raise HTTPResponse(m18n.g("not_logged_in"), 401)
else:
# Delete cookie and clean the session
Session.delete_infos()
authenticator.delete_session_cookie()
return m18n.g("logged_out")
def messages(self):
@ -449,7 +398,11 @@ class _ActionsMapPlugin(object):
Retrieve the WebSocket stream and send to it each messages displayed by
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:
queue = self.log_queues[s_id]
except KeyError:
@ -508,7 +461,6 @@ class _ActionsMapPlugin(object):
else:
return format_for_response(ret)
finally:
# Clean upload directory
# FIXME do that in a better way
global UPLOAD_DIR
@ -517,17 +469,25 @@ class _ActionsMapPlugin(object):
UPLOAD_DIR = None
# 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:
s_id = Session.get_infos()["id"]
s_id = authenticator.get_session_cookie()["id"]
queue = self.log_queues[s_id]
except MoulinetteAuthenticationError:
pass
except KeyError:
pass
else:
queue.put(StopIteration)
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:
queue = self.log_queues[s_id]
except KeyError:
@ -548,7 +508,6 @@ class _ActionsMapPlugin(object):
def moulinette_error_to_http_response(error):
content = error.content()
if isinstance(content, dict):
return HTTPResponse(
@ -585,7 +544,6 @@ def format_for_response(content):
class ActionsMapParser(BaseActionsMapParser):
"""Actions map's Parser for the API
Provide actions map parsing methods for a CLI usage. The parser for
@ -665,36 +623,44 @@ class ActionsMapParser(BaseActionsMapParser):
return parser
def auth_method(self, _, route):
try:
# Retrieve the tid for the route
_, parser = self._parsers[route]
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)
raise MoulinetteValidationError(error_message, raw_msg=True)
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
Keyword arguments:
- route -- The action route as a 2-tuple (method, path)
"""
route = kwargs["route"]
try:
# Retrieve the parser for the route
_, parser = self._parsers[route]
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)
raise MoulinetteValidationError(error_message, raw_msg=True)
ret = argparse.Namespace()
# TODO: Catch errors?
ret = parser.parse_args(args, ret)
parser.dequeue_callbacks(ret)
return ret
# Private methods
@ -721,7 +687,6 @@ class ActionsMapParser(BaseActionsMapParser):
class Interface:
"""Application Programming Interface for the moulinette
Initialize a HTTP server which serves the API connected to a given
@ -737,14 +702,14 @@ class Interface:
type = "api"
def __init__(self, routes={}):
actionsmap = ActionsMap(ActionsMapParser())
def __init__(self, routes={}, actionsmap=None):
actionsmap = ActionsMap(actionsmap, ActionsMapParser())
# Attempt to retrieve log queues from an APIQueueHandler
handler = log.getHandlersByClass(APIQueueHandler, limit=1)
if handler:
log_queues = handler.queues
handler.actionsmap = actionsmap
# TODO: Return OK to 'OPTIONS' xhr requests (l173)
app = Bottle(autojson=True)

View file

@ -208,7 +208,6 @@ def get_locale():
class TTYHandler(logging.StreamHandler):
"""TTY log handler
A handler class which prints logging records for a tty. The record is
@ -251,7 +250,7 @@ class TTYHandler(logging.StreamHandler):
# add translated level name before message
level = "%s " % m18n.g(record.levelname.lower())
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:
# use user-defined formatter
record.__dict__[self.message_key] = msg
@ -274,7 +273,6 @@ class TTYHandler(logging.StreamHandler):
class ActionsMapParser(BaseActionsMapParser):
"""Actions map's Parser for the CLI
Provide actions map parsing methods for a CLI usage. The parser for
@ -289,13 +287,12 @@ class ActionsMapParser(BaseActionsMapParser):
"""
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)
if subparser_kwargs is None:
subparser_kwargs = {"title": "categories", "required": False}
self._parser = parser or ExtendedArgumentParser()
self._subparsers = self._parser.add_subparsers(**subparser_kwargs)
self.global_parser = parent.global_parser if parent else None
@ -336,7 +333,11 @@ class ActionsMapParser(BaseActionsMapParser):
parser = self._subparsers.add_parser(
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):
"""Add a parser for a subcategory
@ -355,7 +356,11 @@ class ActionsMapParser(BaseActionsMapParser):
help=subcategory_help,
**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(
self,
@ -364,6 +369,7 @@ class ActionsMapParser(BaseActionsMapParser):
action_help=None,
deprecated=False,
deprecated_alias=[],
hide_in_help=False,
**kwargs,
):
"""Add a parser for an action
@ -384,37 +390,12 @@ class ActionsMapParser(BaseActionsMapParser):
description=action_help,
deprecated=deprecated,
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):
# FIXME? idk .. this try/except is duplicated from parse_args below
# Just to be able to obtain the 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
ret = self.parse_args(args)
tid = getattr(ret, "_tid", [])
# We go down in the subparser tree until we find the leaf
# corresponding to the tid with a defined authentication
@ -427,28 +408,42 @@ class ActionsMapParser(BaseActionsMapParser):
else:
_p = _p._actions[1]
if tid == []:
return None
raise MoulinetteError(f"Authentication undefined for {tid} ?", raw_msg=True)
def parse_args(self, args, **kwargs):
try:
ret = self._parser.parse_args(args)
return self._parser.parse_args(args)
except SystemExit:
raise
except Exception as e:
error_message = "unable to parse arguments '%s' because: %s" % (
error_message = "unable to parse arguments '{}' because: {}".format(
" ".join(args),
e,
)
logger.exception(error_message)
raise MoulinetteValidationError(error_message, raw_msg=True)
else:
self.prepare_action_namespace(getattr(ret, "_tid", None), ret)
self._parser.dequeue_callbacks(ret)
return ret
def want_to_take_lock(self, args):
ret = self.parse_args(args)
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:
"""Command-line Interface for the moulinette
Initialize an interface connected to the standard input/output
@ -461,12 +456,18 @@ class Interface:
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
m18n.set_locale(get_locale())
self.actionsmap = ActionsMap(
actionsmap,
ActionsMapParser(top_parser=top_parser),
load_only_category=load_only_category,
)
@ -492,6 +493,9 @@ class Interface:
if output_as and output_as not in ["json", "plain", "none"]:
raise MoulinetteValidationError("invalid_usage")
if not args:
raise MoulinetteValidationError("invalid_usage")
try:
ret = self.actionsmap.process(args, timeout=timeout)
except (KeyboardInterrupt, EOFError):
@ -546,39 +550,39 @@ class Interface:
)
def _prompt(message):
if not is_multiline:
import prompt_toolkit
from prompt_toolkit.contrib.completers import WordCompleter
from pygments.token import Token
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.styles import Style
autocomplete_ = WordCompleter(autocomplete)
style = prompt_toolkit.styles.style_from_dict(
style = Style.from_dict(
{
Token.Message: f"#ansi{color} bold",
"": "",
"message": f"#ansi{color} bold",
}
)
def get_bottom_toolbar_tokens(cli):
if help:
return [(Token, help)]
else:
return []
if help:
def get_tokens(cli):
return [
(Token.Message, message),
(Token, ": "),
]
def bottom_toolbar():
return [("class:", help)]
else:
bottom_toolbar = None
colored_message = [
("class:message", message),
("class:", ": "),
]
return prompt_toolkit.prompt(
get_prompt_tokens=get_tokens,
get_bottom_toolbar_tokens=get_bottom_toolbar_tokens,
colored_message,
bottom_toolbar=bottom_toolbar,
style=style,
default=prefill,
true_color=True,
completer=autocomplete_,
complete_while_typing=True,
is_password=is_password,
)

View file

@ -24,7 +24,7 @@ def read_file(file_path, file_mode="r"):
"""
assert isinstance(
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,
type(file_path),
)
@ -121,7 +121,7 @@ def write_to_file(file_path, data, file_mode="w"):
"""
assert (
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,
type(data),
)
@ -130,7 +130,7 @@ def write_to_file(file_path, data, file_mode="w"):
)
assert os.path.isdir(
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,
os.path.dirname(file_path),
)
@ -140,7 +140,7 @@ def write_to_file(file_path, data, file_mode="w"):
for element in data:
assert isinstance(
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,
type(element),
)
@ -179,13 +179,13 @@ def write_to_json(file_path, data, sort_keys=False, indent=None):
# Assumptions
assert isinstance(
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,
type(file_path),
)
assert isinstance(data, dict) or isinstance(
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,
type(data),
)
@ -194,7 +194,7 @@ def write_to_json(file_path, data, sort_keys=False, indent=None):
)
assert os.path.isdir(
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,
os.path.dirname(file_path),
)
@ -365,3 +365,10 @@ def rm(path, recursive=False, force=False):
except OSError as e:
if not force:
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)

View file

@ -1,5 +1,4 @@
import os
import logging
# import all constants because other modules try to import them from this
# module because SUCCESS is defined in this module
@ -70,8 +69,11 @@ def configure_logging(logging_config=None):
def getHandlersByClass(classinfo, limit=0):
"""Retrieve registered handlers of a given class."""
from logging import _handlers
handlers = []
for ref in logging._handlers.itervaluerefs():
for ref in _handlers.itervaluerefs():
o = ref()
if o is not None and isinstance(o, classinfo):
if limit == 1:
@ -83,7 +85,6 @@ def getHandlersByClass(classinfo, limit=0):
class MoulinetteLogger(Logger):
"""Custom logger class
Extend base Logger class to provide the SUCCESS custom log level with
@ -102,14 +103,17 @@ class MoulinetteLogger(Logger):
def findCaller(self, *args):
"""Override findCaller method to consider this source file."""
f = logging.currentframe()
from logging import currentframe, _srcfile
f = currentframe()
if f is not None:
f = f.f_back
rv = "(unknown file)", 0, "(unknown function)"
while hasattr(f, "f_code"):
co = f.f_code
filename = os.path.normcase(co.co_filename)
if filename == logging._srcfile or filename == __file__:
if filename == _srcfile or filename == __file__:
f = f.f_back
continue
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
class ActionFilter(object):
class ActionFilter:
"""Extend log record for an optionnal action
Filter a given record and look for an `action_id` key. If it is not found

View file

@ -80,7 +80,6 @@ def call_async_output(args, callback, **kwargs):
p = subprocess.Popen(args, **kwargs)
while p.poll() is None:
while True:
try:
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
error = 0
for cmd in cmds:
process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=_stderr, shell=shell, **kwargs
)

View file

@ -59,12 +59,12 @@ def searchf(pattern, path, count=0, flags=re.MULTILINE):
def prependlines(text, prepend):
"""Prepend a string to each line of a text"""
lines = text.splitlines(True)
return "%s%s" % (prepend, prepend.join(lines))
return "{}{}".format(prepend, prepend.join(lines))
# Randomize ------------------------------------------------------------
def random_ascii(length=20):
def random_ascii(length=40):
"""Return a random ascii string"""
return binascii.hexlify(os.urandom(length)).decode("ascii")
return binascii.hexlify(os.urandom(length)).decode("ascii")[:length]

View file

@ -3,5 +3,4 @@ addopts = --cov=moulinette -s -v --no-cov-on-fail
norecursedirs = dist doc build .tox .eggs
testpaths = test/
env =
MOULINETTE_LOCALES_DIR = {PWD}/locales
TESTS_RUN = True

View file

@ -4,5 +4,7 @@ ignore =
E128,
E731,
E722,
W503 # Black formatter conflict
E203 # Black formatter conflict
# Black formatter conflict
W503,
# Black formatter conflict
E203

View file

@ -5,7 +5,6 @@ import sys
import subprocess
from setuptools import setup, find_packages
from moulinette import env
version = (
subprocess.check_output(
@ -15,8 +14,6 @@ version = (
.strip()
)
LOCALES_DIR = env["LOCALES_DIR"]
# Extend installation
locale_files = []
@ -33,7 +30,7 @@ install_deps = [
"toml",
"gevent-websocket",
"bottle",
"prompt-toolkit==1.0.15", # To be bumped to debian version once we're on bullseye (+ need tweaks in cli.py)
"prompt-toolkit>=3.0",
"pygments",
]
@ -62,8 +59,8 @@ setup(
url="https://yunohost.org",
license="AGPL",
packages=find_packages(exclude=["test"]),
data_files=[(LOCALES_DIR, locale_files)],
python_requires=">=3.7.*, <3.8",
data_files=[("/usr/share/moulinette/locales", locale_files)],
python_requires=">=3.7.0,<3.10",
install_requires=install_deps,
tests_require=test_deps,
extras_require=extras,

View file

@ -3,25 +3,10 @@
# Global parameters #
#############################
_global:
name: moulitest
namespace: moulitest
authentication:
api: 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 #

View file

@ -12,13 +12,11 @@ reference = json.loads(open(locale_folder + "en.json").read())
def fix_locale(locale_file):
this_locale = json.loads(open(locale_folder + locale_file).read())
fixed_stuff = False
# We iterate over all keys/string in en.json
for key, string in reference.items():
# Ignore check if there's no translation yet for this key
if key not in this_locale:
continue

View file

@ -1,24 +1,15 @@
"""Pytest fixtures for testing."""
import sys
import toml
import yaml
import json
import os
import shutil
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):
"""Configure translator to raise errors when there are missing keys."""
old_translate = moulinette.core.Translator.translate
@ -38,9 +29,9 @@ def patch_translate(moulinette):
moulinette.core.Moulinette18n.g = new_m18nn
def patch_logging(moulinette):
def logging_configuration(moulinette):
"""Configure logging to use the custom logger."""
handlers = set(["tty", "api"])
handlers = {"tty", "api"}
root_handlers = set(handlers)
level = "INFO"
@ -86,33 +77,32 @@ def patch_lock(moulinette):
@pytest.fixture(scope="session", autouse=True)
def moulinette(tmp_path_factory):
import moulinette
import moulinette.core
from moulinette.utils.log import configure_logging
# Can't call the namespace just 'test' because
# that would lead to some "import test" not importing the right stuff
namespace = "moulitest"
tmp_cache = str(tmp_path_factory.mktemp("cache"))
tmp_data = str(tmp_path_factory.mktemp("data"))
tmp_lib = str(tmp_path_factory.mktemp("lib"))
moulinette.env["CACHE_DIR"] = tmp_cache
moulinette.env["DATA_DIR"] = tmp_data
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))
tmp_dir = str(tmp_path_factory.mktemp(namespace))
shutil.copy("./test/actionsmap/moulitest.yml", f"{tmp_dir}/moulitest.yml")
shutil.copytree("./test/src", f"{tmp_dir}/lib/{namespace}/")
shutil.copytree("./test/locales", f"{tmp_dir}/locales")
sys.path.insert(0, f"{tmp_dir}/lib")
patch_init(moulinette)
patch_translate(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
@pytest.fixture
def moulinette_webapi(moulinette):
from webtest import TestApp
from webtest.app import CookiePolicy
@ -125,7 +115,7 @@ def moulinette_webapi(moulinette):
from moulinette.interfaces.api import Interface as Api
return TestApp(Api(routes={})._app)
return TestApp(Api(routes={}, actionsmap=moulinette._actionsmap_path)._app)
@pytest.fixture
@ -142,7 +132,7 @@ def moulinette_cli(moulinette, mocker):
mocker.patch("os.isatty", return_value=True)
from moulinette.interfaces.cli import Interface as Cli
cli = Cli(top_parser=parser)
cli = Cli(top_parser=parser, actionsmap=moulinette._actionsmap_path)
mocker.stopall()
return cli

View file

@ -2,7 +2,6 @@ import re
def reformat(lang, transformations):
locale = open(f"locales/{lang}.json").read()
for pattern, replace in transformations.items():
locale = re.compile(pattern).sub(replace, locale)
@ -25,8 +24,8 @@ godamn_spaces_of_hell = [
"\u2008",
"\u2009",
"\u200A",
"\u202f",
"\u202F",
# "\u202f",
# "\u202F",
"\u3000",
]

View file

@ -10,7 +10,6 @@ locale_files.remove("en.json")
reference = json.loads(open(locale_folder + "en.json").read())
for locale_file in locale_files:
print(locale_file)
this_locale = json.loads(
open(locale_folder + locale_file).read(), object_pairs_hook=OrderedDict

View file

@ -1,16 +1,18 @@
# -*- coding: utf-8 -*-
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
logger = logging.getLogger("moulinette.authenticator.dummy")
logger = logging.getLogger("moulinette.authenticator.yoloswag")
# Dummy authenticator implementation
session_secret = random_ascii()
class Authenticator(BaseAuthenticator):
"""Dummy authenticator used for tests"""
name = "dummy"
@ -19,8 +21,50 @@ class Authenticator(BaseAuthenticator):
pass
def _authenticate_credentials(self, credentials=None):
if not credentials == self.name:
raise MoulinetteError("invalid_password", raw_msg=True)
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")

View file

@ -1,16 +1,18 @@
# -*- coding: utf-8 -*-
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
logger = logging.getLogger("moulinette.authenticator.yoloswag")
# Dummy authenticator implementation
session_secret = random_ascii()
class Authenticator(BaseAuthenticator):
"""Dummy authenticator used for tests"""
name = "yoloswag"
@ -19,8 +21,50 @@ class Authenticator(BaseAuthenticator):
pass
def _authenticate_credentials(self, credentials=None):
if not credentials == self.name:
raise MoulinetteError("invalid_password", raw_msg=True)
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")

View file

@ -44,7 +44,3 @@ def testauth_with_extra_str_only(only_a_str):
def testauth_with_type_int(only_an_int):
return only_an_int
def yoloswag_version(*args, **kwargs):
return "666"

View file

@ -161,10 +161,9 @@ def test_required_paremeter_missing_value(iface, caplog):
def test_actions_map_unknown_authenticator(monkeypatch, tmp_path):
from moulinette.interfaces.api import ActionsMapParser
amap = ActionsMap(ActionsMapParser())
amap = ActionsMap("test/actionsmap/moulitest.yml", ActionsMapParser())
with pytest.raises(MoulinetteError) as exception:
amap.get_authenticator("unknown")
@ -192,10 +191,12 @@ def test_extra_argument_parser_add_argument_bad_arg(iface):
with pytest.raises(MoulinetteError) as exception:
extra_argument_parse.add_argument("_global", "foo", {"ask": 1})
expected_msg = "unable to validate extra parameter '%s' for argument '%s': %s" % (
"ask",
"foo",
"parameter value must be a string, got 1",
expected_msg = (
"unable to validate extra parameter '{}' for argument '{}': {}".format(
"ask",
"foo",
"parameter value must be a string, got 1",
)
)
assert expected_msg in str(exception)
@ -233,9 +234,9 @@ def test_actions_map_api():
from moulinette.interfaces.api import 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 ("GET", "/test-auth/default") 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):
from moulinette.interfaces.api import ActionsMapParser
amap = ActionsMap(ActionsMapParser())
amap = ActionsMap("test/actionsmap/moulitest.yml", ActionsMapParser())
from moulinette.core import MoulinetteLock
@ -266,7 +267,7 @@ def test_actions_map_import_error(mocker):
with pytest.raises(MoulinetteError) as exception:
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",
"testauth_none",
"Yoloswag",
@ -287,9 +288,9 @@ def test_actions_map_cli():
)
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 "testauth" in amap.parser._subparsers.choices
assert "none" in amap.parser._subparsers.choices["testauth"]._actions[1].choices

View file

@ -66,7 +66,7 @@ class TestAuthAPI:
def test_login(self, moulinette_webapi):
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):
assert (
@ -74,7 +74,7 @@ class TestAuthAPI:
== "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):
# C.f.
@ -85,9 +85,7 @@ class TestAuthAPI:
"CSRF protection"
in self.login(moulinette_webapi, csrf=True, status=403).text
)
assert not any(
c.name == "session.moulitest" for c in moulinette_webapi.cookiejar
)
assert not any(c.name == "moulitest" for c in moulinette_webapi.cookiejar)
def test_login_then_legit_request_without_cookies(self, moulinette_webapi):
self.login(moulinette_webapi)
@ -99,7 +97,7 @@ class TestAuthAPI:
def test_login_then_legit_request(self, moulinette_webapi):
self.login(moulinette_webapi)
assert "session.moulitest" in moulinette_webapi.cookies
assert "moulitest" in moulinette_webapi.cookies
assert (
moulinette_webapi.get("/test-auth/default", status=200).text
@ -124,7 +122,7 @@ class TestAuthAPI:
def test_login_other_profile(self, moulinette_webapi):
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):
self.login(moulinette_webapi)
@ -257,25 +255,6 @@ class TestAuthCLI:
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("prompt_toolkit.prompt", 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):
mocker.patch("os.isatty", return_value=True)
mocker.patch("prompt_toolkit.prompt", return_value="dummy")

View file

@ -330,7 +330,6 @@ def test_mkdir(tmp_path):
def test_mkdir_with_permission(tmp_path, mocker):
# This test only make sense when not being root
if os.getuid() == 0:
return

View file

@ -13,12 +13,10 @@ reference = json.loads(open(locale_folder + "en.json").read())
def find_inconsistencies(locale_file):
this_locale = json.loads(open(locale_folder + locale_file).read())
# We iterate over all keys/string in en.json
for key, string in reference.items():
# Ignore check if there's no translation yet for this key
if key not in this_locale:
continue
@ -26,10 +24,10 @@ def find_inconsistencies(locale_file):
# Then we check that every "{stuff}" (for python's .format())
# should also be in the translated string, otherwise the .format
# will trigger an exception!
subkeys_in_ref = set(k[0] for k in re.findall(r"{(\w+)(:\w)?}", string))
subkeys_in_this_locale = set(
subkeys_in_ref = {k[0] for k in re.findall(r"{(\w+)(:\w)?}", string)}
subkeys_in_this_locale = {
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):
yield """\n

View file

@ -11,7 +11,6 @@ import json
def find_expected_string_keys():
# Try to find :
# m18n.g( "foo"
# MoulinetteError("foo"
@ -69,7 +68,6 @@ def test_undefined_i18n_keys():
def test_unused_i18n_keys():
unused_keys = keys_defined.difference(expected_string_keys)
unused_keys = sorted(unused_keys)

View file

@ -66,7 +66,6 @@ def test_run_shell_kwargs():
def test_call_async_output(test_file):
mock_callback_stdout = 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):
mock_callback_stdout = mock.Mock()
mock_callback_stdinfo = mock.Mock()
mock_callback_stderr = mock.Mock()

View file

@ -5,7 +5,7 @@ from moulinette.interfaces import JSONExtendedEncoder
def test_json_extended_encoder(caplog):
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"

View file

@ -20,3 +20,4 @@ def test_prependlines():
def test_random_ascii():
assert isinstance(random_ascii(length=2), str)
assert len(random_ascii(length=10)) == 10

View file

@ -13,12 +13,10 @@ reference = json.loads(open(locale_folder + "en.json").read())
def find_inconsistencies(locale_file):
this_locale = json.loads(open(locale_folder + locale_file).read())
# We iterate over all keys/string in en.json
for key, string in reference.items():
# Ignore check if there's no translation yet for this key
if key not in this_locale:
continue
@ -26,10 +24,10 @@ def find_inconsistencies(locale_file):
# Then we check that every "{stuff}" (for python's .format())
# should also be in the translated string, otherwise the .format
# will trigger an exception!
subkeys_in_ref = set(k[0] for k in re.findall(r"{(\w+)(:\w)?}", string))
subkeys_in_this_locale = set(
subkeys_in_ref = {k[0] for k in re.findall(r"{(\w+)(:\w)?}", string)}
subkeys_in_this_locale = {
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):
yield """\n

18
tox.ini
View file

@ -1,6 +1,6 @@
[tox]
envlist =
py37-{pytest,lint,invalidcode,mypy}
py{37,39}-{pytest,lint,invalidcode,mypy}
format
format-check
docs
@ -11,15 +11,15 @@ usedevelop = True
passenv = *
extras = tests
deps =
py37-pytest: .[tests]
py37-lint: flake8
py37-invalidcode: flake8
py37-mypy: mypy >= 0.761
py{37,39}-pytest: .[tests]
py{37,39}-lint: flake8
py{37,39}-invalidcode: flake8
py{37,39}-mypy: mypy >= 0.761
commands =
py37-pytest: pytest {posargs} -c pytest.ini
py37-lint: flake8 moulinette test
py37-invalidcode: flake8 moulinette test --select F
py37-mypy: mypy --ignore-missing-imports --install-types --non-interactive moulinette/
py{37,39}-pytest: pytest {posargs} -c pytest.ini
py{37,39}-lint: flake8 moulinette test
py{37,39}-invalidcode: flake8 moulinette test --select F
py{37,39}-mypy: mypy --ignore-missing-imports --install-types --non-interactive moulinette/
[gh-actions]
python =