aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.python-version1
-rw-r--r--LICENSE.txt2
-rw-r--r--docs/Pipfile10
-rw-r--r--docs/Pipfile.lock166
-rw-r--r--docs/api/fiction.rst2
-rw-r--r--docs/config/pyfingerd.service12
-rwxr-xr-xpyfingerd/__init__.py2
-rwxr-xr-xpyfingerd/__main__.py2
-rw-r--r--pyfingerd/binds.py233
-rwxr-xr-xpyfingerd/cli.py4
-rwxr-xr-xpyfingerd/core.py250
-rwxr-xr-xpyfingerd/errors.py2
-rwxr-xr-xpyfingerd/fiction.py7
-rwxr-xr-xpyfingerd/native.py2
-rwxr-xr-xpyfingerd/posix.py5
-rw-r--r--pyfingerd/utils.py2
-rwxr-xr-xpyfingerd/version.py4
-rw-r--r--setup.cfg4
-rw-r--r--tests/test_binds.py44
-rw-r--r--tests/test_server.py78
21 files changed, 492 insertions, 341 deletions
diff --git a/.gitignore b/.gitignore
index 345e29f..bb92ab9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
__pycache__
*.pyc
*.spec
+/.python-version
/.env
/actions*.*
diff --git a/.python-version b/.python-version
deleted file mode 100644
index 11aaa06..0000000
--- a/.python-version
+++ /dev/null
@@ -1 +0,0 @@
-3.9.5
diff --git a/LICENSE.txt b/LICENSE.txt
index f730817..65feab7 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,7 +1,7 @@
The MIT License (MIT)
=====================
-Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr>
+Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the “Software”),
diff --git a/docs/Pipfile b/docs/Pipfile
index efdb5a1..d8bf3cb 100644
--- a/docs/Pipfile
+++ b/docs/Pipfile
@@ -6,10 +6,18 @@ verify_ssl = true
[dev-packages]
[packages]
-sphinx = "*"
sphinx-rtd-theme = "*"
sphinx-autobuild = "*"
sphinx-autodoc-typehints = "*"
+sphinx-autodoc-annotation = "*"
+click = "*"
+coloredlogs = "*"
+croniter = "*"
+python-dateutil = "*"
+pytz = "*"
+pyutmpx = "*"
+toml = "*"
+Sphinx = "*"
[requires]
python_version = "3.9"
diff --git a/docs/Pipfile.lock b/docs/Pipfile.lock
index 6a33722..7658342 100644
--- a/docs/Pipfile.lock
+++ b/docs/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "9e53fd6e9c8d06e445ecf8cdf0d568ef01a8e987bb88ea841878ddf582def9d1"
+ "sha256": "b3940e1cfb50d6595efe5dfd8d5e5f975f9c18b5a8dcd277c0cfcb8b54c4471e"
},
"pipfile-spec": 6,
"requires": {
@@ -33,18 +33,26 @@
},
"certifi": {
"hashes": [
- "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
- "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
+ "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
+ "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
],
- "version": "==2021.5.30"
+ "version": "==2021.10.8"
},
"charset-normalizer": {
"hashes": [
- "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b",
- "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
+ "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721",
+ "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"
],
"markers": "python_version >= '3'",
- "version": "==2.0.4"
+ "version": "==2.0.9"
+ },
+ "click": {
+ "hashes": [
+ "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3",
+ "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"
+ ],
+ "index": "pypi",
+ "version": "==8.0.3"
},
"colorama": {
"hashes": [
@@ -54,37 +62,61 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.4.4"
},
+ "coloredlogs": {
+ "hashes": [
+ "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934",
+ "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"
+ ],
+ "index": "pypi",
+ "version": "==15.0.1"
+ },
+ "croniter": {
+ "hashes": [
+ "sha256:4023e4d18ced979332369964351e8f4f608c1f7c763e146b1d740002c4245247",
+ "sha256:d30dd147d1daec39d015a15b8cceb3069b9780291b9c141e869c32574a8eeacb"
+ ],
+ "index": "pypi",
+ "version": "==1.1.0"
+ },
"docutils": {
"hashes": [
- "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af",
- "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"
+ "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125",
+ "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==0.17.1"
+ },
+ "humanfriendly": {
+ "hashes": [
+ "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477",
+ "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
- "version": "==0.16"
+ "version": "==10.0"
},
"idna": {
"hashes": [
- "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
- "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
+ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
+ "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_version >= '3'",
- "version": "==3.2"
+ "version": "==3.3"
},
"imagesize": {
"hashes": [
- "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1",
- "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"
+ "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c",
+ "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==1.2.0"
+ "version": "==1.3.0"
},
"jinja2": {
"hashes": [
- "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4",
- "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"
+ "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8",
+ "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"
],
"markers": "python_version >= '3.6'",
- "version": "==3.0.1"
+ "version": "==3.0.3"
},
"livereload": {
"hashes": [
@@ -97,6 +129,7 @@
"sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298",
"sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64",
"sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b",
+ "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194",
"sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567",
"sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff",
"sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724",
@@ -104,6 +137,7 @@
"sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646",
"sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35",
"sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6",
+ "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a",
"sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6",
"sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad",
"sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26",
@@ -111,27 +145,36 @@
"sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac",
"sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7",
"sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6",
+ "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047",
"sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75",
"sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f",
+ "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b",
"sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135",
"sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8",
"sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a",
"sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a",
+ "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1",
"sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9",
"sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864",
"sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914",
+ "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee",
+ "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f",
"sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18",
"sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8",
"sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2",
"sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d",
"sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b",
"sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b",
+ "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86",
+ "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6",
"sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f",
"sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb",
"sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833",
"sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28",
+ "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e",
"sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415",
"sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902",
+ "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f",
"sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d",
"sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9",
"sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d",
@@ -139,10 +182,14 @@
"sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066",
"sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c",
"sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1",
+ "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a",
+ "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207",
"sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f",
"sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53",
+ "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd",
"sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134",
"sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85",
+ "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9",
"sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5",
"sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94",
"sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509",
@@ -154,34 +201,50 @@
},
"packaging": {
"hashes": [
- "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
- "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
+ "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
+ "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
],
"markers": "python_version >= '3.6'",
- "version": "==21.0"
+ "version": "==21.3"
},
"pygments": {
"hashes": [
- "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380",
- "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"
+ "sha256:59b895e326f0fb0d733fd28c6839bd18ad0687ba20efc26d4277fd1d30b971f4",
+ "sha256:9135c1af61eec0f650cd1ea1ed8ce298e54d56bcd8cc2ef46edd7702c171337c"
],
"markers": "python_version >= '3.5'",
- "version": "==2.10.0"
+ "version": "==2.11.1"
},
"pyparsing": {
"hashes": [
- "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
- "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
+ "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4",
+ "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==3.0.6"
+ },
+ "python-dateutil": {
+ "hashes": [
+ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
+ "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
],
- "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==2.4.7"
+ "index": "pypi",
+ "version": "==2.8.2"
},
"pytz": {
"hashes": [
- "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
- "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"
+ "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c",
+ "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"
],
- "version": "==2021.1"
+ "index": "pypi",
+ "version": "==2021.3"
+ },
+ "pyutmpx": {
+ "hashes": [
+ "sha256:95f9631bfaf464b38a5dc5452d951d604c566ab64b3a437b5d65cefecdc90bcc"
+ ],
+ "index": "pypi",
+ "version": "==0.4.1"
},
"requests": {
"hashes": [
@@ -201,18 +264,18 @@
},
"snowballstemmer": {
"hashes": [
- "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2",
- "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"
+ "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1",
+ "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"
],
- "version": "==2.1.0"
+ "version": "==2.2.0"
},
"sphinx": {
"hashes": [
- "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13",
- "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544"
+ "sha256:0a8836751a68306b3fe97ecbe44db786f8479c3bf4b80e3a7f5c838657b4698c",
+ "sha256:6a11ea5dd0bdb197f9c2abc2e0ce73e01340464feaece525e64036546d24c851"
],
"index": "pypi",
- "version": "==4.1.2"
+ "version": "==4.3.2"
},
"sphinx-autobuild": {
"hashes": [
@@ -222,6 +285,13 @@
"index": "pypi",
"version": "==2021.3.14"
},
+ "sphinx-autodoc-annotation": {
+ "hashes": [
+ "sha256:4a3d03081efe1e5f2bc9b9d00746550f45b9f543b0c79519c523168ca7f7d89a"
+ ],
+ "index": "pypi",
+ "version": "==1.0.post1"
+ },
"sphinx-autodoc-typehints": {
"hashes": [
"sha256:193617d9dbe0847281b1399d369e74e34cd959c82e02c7efde077fca908a9f52",
@@ -232,11 +302,11 @@
},
"sphinx-rtd-theme": {
"hashes": [
- "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a",
- "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f"
+ "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8",
+ "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"
],
"index": "pypi",
- "version": "==0.5.2"
+ "version": "==1.0.0"
},
"sphinxcontrib-applehelp": {
"hashes": [
@@ -286,6 +356,14 @@
"markers": "python_version >= '3.5'",
"version": "==1.1.5"
},
+ "toml": {
+ "hashes": [
+ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+ "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
+ ],
+ "index": "pypi",
+ "version": "==0.10.2"
+ },
"tornado": {
"hashes": [
"sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb",
@@ -335,11 +413,11 @@
},
"urllib3": {
"hashes": [
- "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
- "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
+ "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
+ "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
- "version": "==1.26.6"
+ "version": "==1.26.7"
}
},
"develop": {}
diff --git a/docs/api/fiction.rst b/docs/api/fiction.rst
index cefd359..6d6386a 100644
--- a/docs/api/fiction.rst
+++ b/docs/api/fiction.rst
@@ -46,7 +46,7 @@ Playing fictions
----------------
.. autoclass:: FingerFictionInterface
- :members: reset, apply, update
+ :members: reset, apply
Playing scenarios
-----------------
diff --git a/docs/config/pyfingerd.service b/docs/config/pyfingerd.service
deleted file mode 100644
index 035a49c..0000000
--- a/docs/config/pyfingerd.service
+++ /dev/null
@@ -1,12 +0,0 @@
-[Unit]
-Description=pyfingerd
-After=network.target
-
-[Service]
-Type=simple
-ExecStart=/usr/bin/pyfingerd
-ExecStop=/bin/kill -s TERM $MAINPID
-TimeoutSec=15
-
-[Install]
-WantedBy=multi-user.target
diff --git a/pyfingerd/__init__.py b/pyfingerd/__init__.py
index aad1d8a..b6dcfec 100755
--- a/pyfingerd/__init__.py
+++ b/pyfingerd/__init__.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# *****************************************************************************
-# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr>
+# Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr>
# This file is part of the pyfingerd project, which is MIT-licensed.
# *****************************************************************************
""" Pure Python finger protocol implementation.
diff --git a/pyfingerd/__main__.py b/pyfingerd/__main__.py
index d8e58e2..a9f8150 100755
--- a/pyfingerd/__main__.py
+++ b/pyfingerd/__main__.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# *****************************************************************************
-# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr>
+# Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr>
# This file is part of the pyfingerd project, which is MIT-licensed.
# *****************************************************************************
""" Main script of the module. """
diff --git a/pyfingerd/binds.py b/pyfingerd/binds.py
new file mode 100644
index 0000000..4d22c93
--- /dev/null
+++ b/pyfingerd/binds.py
@@ -0,0 +1,233 @@
+#!/usr/bin/env python3
+# *****************************************************************************
+# Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr>
+# This file is part of the pyfingerd project, which is MIT-licensed.
+# *****************************************************************************
+""" Binds decoder for the finger server. """
+
+import socket as _socket
+
+from typing import Sequence as _Sequence
+
+from .errors import InvalidBindError as _InvalidBindError
+
+__all__ = [
+ 'FingerBind', 'FingerBindsDecoder',
+ 'FingerTCPv4Bind', 'FingerTCPv6Bind',
+]
+
+
+class FingerBind:
+ """ Bind address for pyfingerd. """
+
+ @property
+ def runserver_params(self):
+ """ Return the data as ``_runserver`` arguments. """
+
+ raise NotImplementedError
+
+
+class FingerTCPv4Bind(FingerBind):
+ """ IPv4 TCP Address. """
+
+ def __init__(self, address, port):
+ try:
+ self._addr = _socket.inet_pton(_socket.AF_INET, address)
+ except Exception:
+ self._addr = address
+
+ self._port = port
+
+ @property
+ def runserver_params(self):
+ """ Return the data as `_runserver` parameters. """
+
+ return (
+ _socket.AF_INET,
+ _socket.inet_ntop(_socket.AF_INET, self._addr),
+ self._port,
+ )
+
+
+class FingerTCPv6Bind(FingerBind):
+ """ IPv6 TCP Address. """
+
+ def __init__(self, address, port):
+ try:
+ self._addr = _socket.inet_pton(_socket.AF_INET6, address)
+ except Exception:
+ self._addr = address
+
+ self._port = port
+
+ @property
+ def runserver_params(self):
+ """ Return the data as `_runserver` parameters. """
+
+ return (
+ _socket.AF_INET6,
+ _socket.inet_ntop(_socket.AF_INET6, self._addr),
+ self._port,
+ )
+
+
+class FingerBindsDecoder:
+ """ Binds decoder for pyfingerd. """
+
+ def __init__(self, proto: str = 'finger'):
+ proto = proto.casefold()
+ if proto not in ('finger',):
+ raise ValueError(f'unsupported protocol {proto!r}')
+
+ self._proto = proto
+
+ def decode(self, raw: str) -> _Sequence[FingerBind]:
+ """ Get binds for the server, using a given string. """
+
+ binds = set()
+
+ for addr in map(lambda x: x.strip(), raw.split(',')):
+ if not addr:
+ continue
+
+ # Try to find a scheme.
+
+ scheme, *rest = addr.split(':/')
+ if not rest:
+ # No scheme found, let's just guess the scheme based on
+ # the situation.
+
+ raw = scheme
+ scheme = {'finger': 'tcp'}[self._proto]
+ else:
+ # just don't add the ':' of ':/' again
+ raw = '/' + ':/'.join(rest)
+
+ if (
+ (self._proto == 'finger' and scheme != 'tcp')
+ or scheme not in ('tcp',)
+ ):
+ raise _InvalidBindError(
+ addr,
+ f'Unsupported scheme {scheme!r} for '
+ f'protocol {self._proto!r}',
+ )
+
+ # Decode the address data.
+
+ if scheme == 'tcp':
+ binds.update(self._decode_tcp_host(raw))
+
+ return tuple(binds)
+
+ def __repr__(self):
+ return f'{self._class__.__name__}()'
+
+ def _decode_tcp_host(self, x):
+ """ Decode suitable hosts for a TCP bind. """
+
+ addrs = ()
+ addr = x
+
+ # TODO: manage the '*' case.
+ # TODO: decode hosts without the default host.
+
+ # Get the host part first, we'll decode it later.
+
+ if x[0] == '[':
+ # The host part is an IPv6, look for the closing ']' and
+ # decode it later.
+
+ to = x.find(']')
+ if to < 0:
+ raise _InvalidBindError(
+ addr, "Expected closing ']'")
+
+ host = x[1:to]
+ x = x[to + 1:]
+
+ is_ipv6 = True
+ else:
+ # The host part is either an IPv4 or a host name, look for
+ # the ':' and decode it later.
+
+ host, *x = x.split(':')
+ x = ':' + ':'.join(x)
+
+ is_ipv6 = False
+
+ # Decode the port part.
+
+ if x in ('', ':'):
+ port = 79
+ elif x[0] == ':':
+ try:
+ port = int(x[1:])
+ except ValueError:
+ try:
+ if x[1:] != '':
+ raise AssertionError('Expected a port number')
+ port = _socket.getservbyname(x[1:])
+ except Exception:
+ raise _InvalidBindError(
+ addr,
+ 'Expected a valid port number or name '
+ f'(got {x[1:]!r})',
+ ) from None
+ else:
+ raise _InvalidBindError(
+ addr, 'Garbage found after the host',
+ )
+
+ # Decode the host part and get the addresses.
+
+ addrs = ()
+ if is_ipv6:
+ # Decode the IPv6 address (validate it using `_socket.inet_pton`).
+
+ ip6 = host
+ _socket.inet_pton(_socket.AF_INET6, host)
+ addrs += (FingerTCPv6Bind(ip6, port),)
+ else:
+ # Decode the host (try IPv4, otherwise, resolve domain).
+
+ try:
+ ip = host.split('.')
+ if len(ip) < 2 or len(ip) > 4:
+ raise AssertionError('2 <= len(ip) <= 4')
+
+ ip = list(map(int, ip))
+ if not all(lambda x: 0 <= x < 256, ip):
+ raise AssertionError('non-8-bit component')
+
+ if len(ip) == 2:
+ ip = [ip[0], 0, 0, ip[1]]
+ elif len(ip) == 3:
+ ip = [ip[0], 0, ip[1], ip[2]]
+
+ addrs += (FingerTCPv4Bind(ip, port),)
+ except Exception:
+ entries = _socket.getaddrinfo(
+ host, port,
+ proto=_socket.IPPROTO_TCP,
+ type=_socket.SOCK_STREAM)
+
+ for ent in entries:
+ if (
+ ent[0] not in (_socket.AF_INET, _socket.AF_INET6)
+ or ent[1] not in (_socket.SOCK_STREAM,)
+ ):
+ continue
+
+ if ent[0] == _socket.AF_INET:
+ ip = ent[4][0]
+ _socket.inet_pton(_socket.AF_INET, ent[4][0])
+ addrs += (FingerTCPv4Bind(ip, port),)
+ else:
+ ip6 = ent[4][0]
+ _socket.inet_pton(_socket.AF_INET6, ent[4][0])
+ addrs += (FingerTCPv6Bind(ip6, port),)
+
+ return addrs
+
+# End of file.
diff --git a/pyfingerd/cli.py b/pyfingerd/cli.py
index e184c0c..409b376 100755
--- a/pyfingerd/cli.py
+++ b/pyfingerd/cli.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# *****************************************************************************
-# Copyright (C) 2021 Thomas Touhey <thomas@touhey.fr>
+# Copyright (C) 2021-2022 Thomas Touhey <thomas@touhey.fr>
# This file is part of the pyfingerd project, which is MIT-licensed.
# *****************************************************************************
""" pyfingerd CLI interface. """
@@ -69,7 +69,7 @@ __all__ = ['cli']
@_click.option(
'-S', '--start', 'scenario_start',
type=_click.DateTime(),
- default=_datetime.now(),
+ default=_datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
envvar=('FINGER_START',),
help=(
'Date and time at which the scenario starts or has started '
diff --git a/pyfingerd/core.py b/pyfingerd/core.py
index b9e8b57..8a3d65b 100755
--- a/pyfingerd/core.py
+++ b/pyfingerd/core.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# *****************************************************************************
-# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr>
+# Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr>
# This file is part of the pyfingerd project, which is MIT-licensed.
# *****************************************************************************
""" Main classes for the finger server, interfaces and formatters.
@@ -13,21 +13,18 @@ import asyncio as _asyncio
import copy as _copy
import multiprocessing as _multip
import signal as _signal
-import socket as _socket
import string as _string
-from collections.abc import Sequence as _Sequence
from datetime import datetime as _dt, timedelta as _td, tzinfo as _tzinfo
-from enum import Enum as _Enum
from errno import errorcode as _errorcode
-from typing import Optional as _Optional
+from typing import Optional as _Optional, Sequence as _Sequence
from croniter import croniter as _croniter
from pytz import utc as _utc
+from .binds import FingerBindsDecoder as _FingerBindsDecoder
from .errors import (
HostnameError as _HostnameError,
- InvalidBindError as _InvalidBindError,
MalformedQueryError as _MalformedQueryError,
NoBindsError as _NoBindsError,
)
@@ -757,243 +754,6 @@ class FingerInterface:
# ---
-# Bind-related configuration.
-# ---
-
-
-class _BindAddress:
- """ Bind address for pyfingerd. """
-
- class Type(_Enum):
- """ Bind address type. """
-
- """ TCP on IPv4 bind. """
- TCP_IPv4 = 1
-
- """ TCP on IPv6 bind. """
- TCP_IPv6 = 2
-
- def __init__(self, family):
- self._family = self.Type(family)
-
- def __repr__(self):
- return f'{self._class__.__name__}(family={self._family!r})'
-
- @property
- def family(self):
- """ Family as one of the `BindAddress.Type` enumeration values. """
-
- return self._family
-
-
-class _TCP4Address(_BindAddress):
- """ IPv4 TCP Address. """
-
- def __init__(self, address, port):
- super().__init__(_BindAddress.Type.TCP_IPv4)
-
- try:
- self._addr = _socket.inet_pton(_socket.AF_INET, address)
- except Exception:
- self._addr = address
-
- self._port = port
-
- @property
- def runserver_params(self):
- """ Return the data as `_runserver` parameters. """
-
- return (
- _socket.AF_INET,
- _socket.inet_ntop(_socket.AF_INET, self._addr),
- self._port)
-
-
-class _TCP6Address(_BindAddress):
- """ IPv6 TCP Address. """
-
- def __init__(self, address, port):
- super().__init__(_BindAddress.Type.TCP_IPv6)
-
- try:
- self._addr = _socket.inet_pton(_socket.AF_INET6, address)
- except Exception:
- self._addr = address
-
- self._port = port
-
- @property
- def runserver_params(self):
- """ Return the data as `_runserver` parameters. """
-
- return (
- _socket.AF_INET6,
- _socket.inet_ntop(_socket.AF_INET6, self._addr),
- self._port,
- )
-
-
-class _BindsDecoder:
- """ Binds decoder for pyfingerd.
-
- Takes a raw string and the protocol name, either 'finger' (the base
- protocol managed by the class) or 'pyfingerd-control' (the protocol
- used for controlling the live pyfingerd interface).
- """
-
- def __init__(self, raw, proto='finger'):
- proto = proto.casefold()
- if proto not in ('finger',):
- raise ValueError(f'unsupported protocol {proto}')
-
- self._binds = set()
-
- for x in map(lambda x: x.strip(), raw.split(',')):
- addr = x
-
- # Try to find a scheme.
-
- scheme, *rest = x.split(':/')
- if not rest:
- # No scheme found, let's just guess the scheme based on
- # the situation.
-
- x = scheme
- scheme = {'finger': 'tcp'}[proto]
- else:
- # just don't add the ':' of ':/' again
- x = '/' + ':/'.join(rest)
-
- if (
- (proto == 'finger' and scheme != 'tcp')
- or scheme not in ('tcp',)
- ):
- raise _InvalidBindError(
- addr,
- f'Unsupported scheme {scheme!r} for protocol {proto!r}',
- )
-
- # Decode the address data.
-
- if scheme == 'tcp':
- self._binds.update(self._decode_tcp_host(x))
-
- self._binds = tuple(self._binds)
-
- def __iter__(self):
- return iter(self._binds)
-
- def __repr__(self):
- return f'{self._class__.__name__}(binds={self._binds})'
-
- def _decode_tcp_host(self, x):
- """ Decode suitable hosts for a TCP bind. """
-
- addrs = ()
- addr = x
-
- # TODO: manage the '*' case.
-
- # Get the host part first, we'll decode it later.
-
- if x[0] == '[':
- # The host part is an IPv6, look for the closing ']' and
- # decode it later.
-
- to = x.find(']')
- if to < 0:
- raise _InvalidBindError(
- addr, "Expected closing ']'")
-
- host = x[1:to]
- x = x[to + 1:]
-
- is_ipv6 = True
- else:
- # The host part is either an IPv4 or a host name, look for
- # the ':' and decode it later.
-
- host, *x = x.split(':')
- x = ':' + ':'.join(x)
-
- is_ipv6 = False
-
- # Decode the port part.
-
- if x == '':
- port = 79
- elif x[0] == ':':
- try:
- port = int(x[1:])
- except ValueError:
- try:
- if x[1:] != '':
- raise AssertionError('Expected a port number')
- port = _socket.getservbyname(x[1:])
- except Exception:
- raise _InvalidBindError(
- addr,
- 'Expected a valid port number or name '
- f'(got {x[1:]!r})',
- ) from None
- else:
- raise _InvalidBindError(
- addr, 'Garbage found after the host',
- )
-
- # Decode the host part and get the addresses.
-
- addrs = ()
- if is_ipv6:
- # Decode the IPv6 address (validate it using `_socket.inet_pton`).
-
- ip6 = host
- _socket.inet_pton(_socket.AF_INET6, host)
- addrs += (_TCP6Address(ip6, port),)
- else:
- # Decode the host (try IPv4, otherwise, resolve domain).
-
- try:
- ip = host.split('.')
- if len(ip) < 2 or len(ip) > 4:
- raise AssertionError('2 <= len(ip) <= 4')
-
- ip = list(map(int, ip))
- if not all(lambda x: 0 <= x < 256, ip):
- raise AssertionError('non-8-bit component')
-
- if len(ip) == 2:
- ip = [ip[0], 0, 0, ip[1]]
- elif len(ip) == 3:
- ip = [ip[0], 0, ip[1], ip[2]]
-
- addrs += (_TCP4Address(ip, port),)
- except Exception:
- entries = _socket.getaddrinfo(
- host, port,
- proto=_socket.IPPROTO_TCP,
- type=_socket.SOCK_STREAM)
-
- for ent in entries:
- if (
- ent[0] not in (_socket.AF_INET, _socket.AF_INET6)
- or ent[1] not in (_socket.SOCK_STREAM,)
- ):
- continue
-
- if ent[0] == _socket.AF_INET:
- ip = ent[4][0]
- _socket.inet_pton(_socket.AF_INET, ent[4][0])
- addrs += (_TCP4Address(ip, port),)
- else:
- ip6 = ent[4][0]
- _socket.inet_pton(_socket.AF_INET6, ent[4][0])
- addrs += (_TCP6Address(ip6, port),)
-
- return addrs
-
-
-# ---
# Finger/TCP server implementation.
# ---
@@ -1117,7 +877,7 @@ class FingerServer:
# Check the binds.
- self._binds = [b for b in _BindsDecoder(binds)]
+ self._binds = [b for b in _FingerBindsDecoder().decode(binds)]
if not self._binds:
raise _NoBindsError()
@@ -1290,7 +1050,7 @@ class FingerServer:
if seconds >= 0:
break
- await _asyncio.sleep(seconds) # TODO
+ await _asyncio.sleep(seconds)
async def start_servers():
""" Start the servers. """
diff --git a/pyfingerd/errors.py b/pyfingerd/errors.py
index f5ae0f6..6810df5 100755
--- a/pyfingerd/errors.py
+++ b/pyfingerd/errors.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# *****************************************************************************
-# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr>
+# Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr>
# This file is part of the pyfingerd project, which is MIT-licensed.
# *****************************************************************************
""" This file defines the exceptions used throughout the module. """
diff --git a/pyfingerd/fiction.py b/pyfingerd/fiction.py
index 7130845..1dc61b8 100755
--- a/pyfingerd/fiction.py
+++ b/pyfingerd/fiction.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# *****************************************************************************
-# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr>
+# Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr>
# This file is part of the pyfingerd project, which is MIT-licensed.
# *****************************************************************************
""" Definitions for the finger server fiction interface.
@@ -14,10 +14,11 @@ import math as _math
import os.path as _path
from collections import defaultdict as _defaultdict
-from collections.abc import Sequence as _Sequence
from datetime import datetime as _dt, timedelta as _td
from enum import Enum as _Enum
-from typing import Optional as _Optional, Union as _Union
+from typing import (
+ Optional as _Optional, Sequence as _Sequence, Union as _Union,
+)
from .core import (
FingerInterface as _FingerInterface,
diff --git a/pyfingerd/native.py b/pyfingerd/native.py
index b1be94c..8fbdd34 100755
--- a/pyfingerd/native.py
+++ b/pyfingerd/native.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# *****************************************************************************
-# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr>
+# Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr>
# This file is part of the pyfingerd project, which is MIT-licensed.
# *****************************************************************************
""" Defining the native interface. """
diff --git a/pyfingerd/posix.py b/pyfingerd/posix.py
index 3ca1849..265fa06 100755
--- a/pyfingerd/posix.py
+++ b/pyfingerd/posix.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# *****************************************************************************
-# Copyright (C) 2017-2021 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr>
# This file is part of the pyfingerd Python 3.x module, which is MIT-licensed.
# *****************************************************************************
""" Make use of the utmp/x file to read the user data.
@@ -10,13 +10,12 @@
import pwd as _pwd
-from collections.abc import Sequence as _Sequence
from copy import copy as _copy
from datetime import datetime as _dt
from multiprocessing import Lock as _Lock
from os import stat as _stat
from os.path import exists as _exists, join as _joinpaths
-from typing import Optional as _Optional
+from typing import Optional as _Optional, Sequence as _Sequence
from pytz import utc as _utc
import pyutmpx as _pyutmpx
diff --git a/pyfingerd/utils.py b/pyfingerd/utils.py
index c96412a..99d8546 100644
--- a/pyfingerd/utils.py
+++ b/pyfingerd/utils.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# *****************************************************************************
-# Copyright (C) 2021 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# Copyright (C) 2021-2022 Thomas Touhey <thomas@touhey.fr>
# This file is part of the pyfingerd Python 3.x module, which is MIT-licensed.
# *****************************************************************************
""" Utilities for the pyfingerd module. """
diff --git a/pyfingerd/version.py b/pyfingerd/version.py
index 46cf96c..23685b0 100755
--- a/pyfingerd/version.py
+++ b/pyfingerd/version.py
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
# *****************************************************************************
-# Copyright (C) 2021 Thomas Touhey <thomas@touhey.fr>
+# Copyright (C) 2021-2022 Thomas Touhey <thomas@touhey.fr>
# This file is part of the pyfingerd project, which is MIT-licensed.
# *****************************************************************************
""" pyfingerd version definition. """
-version = '0.4.1'
+version = '0.4.2'
# End of file.
diff --git a/setup.cfg b/setup.cfg
index 4ca9831..fd2ce16 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -20,7 +20,7 @@ classifiers =
[options]
zip_safe = False
include_package_data = True
-packages = find:
+packages = pyfingerd
scripts =
scripts/pyfingerd
@@ -31,7 +31,7 @@ scripts =
universal = True
[flake8]
-ignore = D105,D107,D202,D208,D210,D401,W503
+ignore = D105,D107,D202,D208,D210,D401,F405,W503
per-file-ignores =
tests/*:S101,D102
rst-roles =
diff --git a/tests/test_binds.py b/tests/test_binds.py
new file mode 100644
index 0000000..e1fac43
--- /dev/null
+++ b/tests/test_binds.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+# *****************************************************************************
+# Copyright (C) 2021 Thomas Touhey <thomas@touhey.fr>
+# This file is part of the pyfingerd project, which is MIT-licensed.
+# *****************************************************************************
+""" Tests for the pyfingerd server. """
+
+import socket
+
+from pyfingerd.binds import FingerBindsDecoder, FingerTCPv4Bind
+import pytest
+
+
+class TestFingerDecoder:
+ """ Test binds. """
+
+ @pytest.fixture
+ def decoder(self):
+ return FingerBindsDecoder(proto='finger')
+
+ def test_no_binds(self, decoder):
+ assert decoder.decode('') == ()
+
+ @pytest.mark.parametrize('raw,cls,params', (
+ ('127.0.0.1:79', FingerTCPv4Bind, (
+ socket.AF_INET,
+ '127.0.0.1',
+ 79,
+ )),
+ ('127.0.2.3', FingerTCPv4Bind, (
+ socket.AF_INET,
+ '127.0.2.3',
+ 79,
+ )),
+ ))
+ def test_binds(self, decoder, raw, cls, params):
+ binds = decoder.decode(raw)
+ assert len(binds) == 1
+
+ bind = binds[0]
+ assert isinstance(bind, cls)
+ assert bind.runserver_params == params
+
+# End of file.
diff --git a/tests/test_server.py b/tests/test_server.py
index 180c643..de944f8 100644
--- a/tests/test_server.py
+++ b/tests/test_server.py
@@ -10,11 +10,8 @@ import socket
from datetime import timedelta
from time import sleep
-from pyfingerd.core import FingerServer
-from pyfingerd.fiction import (
- FingerScenario, FingerScenarioInterface,
- FingerUserCreationAction, FingerUserLoginAction, FingerUserLogoutAction,
-)
+from pyfingerd.core import * # NOQA
+from pyfingerd.fiction import * # NOQA
import pytest
@@ -22,7 +19,7 @@ import pytest
class TestFingerConnection:
""" Test basic finger connections. """
- @pytest.fixture
+ @pytest.fixture(autouse=True)
def fingerserver(self):
""" Start a finger server.
@@ -54,10 +51,44 @@ class TestFingerConnection:
),
timedelta(seconds=1))
+ class TestFormatter(FingerFormatter):
+ """ Test formatter, uncomplicated to test. """
+
+ def _format_users(self, users):
+ result = f'{len(users)}\n'
+ for user in users:
+ result += (
+ f'{user.login}|{user.name}|{user.home}|'
+ f'{user.shell}|{user.office or ""}|'
+ f'{len(user.sessions)}\n'
+ )
+ for session in user.sessions:
+ result += (
+ f'{session.line or ""}|{session.host or ""}\n'
+ )
+
+ return result
+
+ def format_query_error(self, hostname, raw_query):
+ return f'{hostname}\n{raw_query}\nerror\n'
+
+ def format_short(self, hostname, raw_query, users):
+ return (
+ f'{hostname}\n{raw_query}\nshort\n'
+ + self._format_users(users)
+ )
+
+ def format_long(self, hostname, raw_query, users):
+ return (
+ f'{hostname}\n{raw_query}\nlong\n'
+ + self._format_users(users)
+ )
+
server = FingerServer(
'localhost:3099',
hostname='example.org',
interface=FingerScenarioInterface(scenario),
+ formatter=TestFormatter(),
)
server.start()
@@ -67,31 +98,40 @@ class TestFingerConnection:
server.stop()
def _send_command(self, command):
- conn = socket.create_connection(('localhost', 3099))
- conn.send(command)
- return conn.recv(1024)
+ conn = socket.create_connection(('localhost', 3099), 1)
+ conn.send(command.encode('ascii') + b'\r\n')
+
+ result = tuple(
+ conn.recv(1024).decode('ascii')
+ .rstrip('\r\n').split('\r\n')
+ )
+
+ assert result[:2] == (
+ 'EXAMPLE.ORG',
+ command,
+ )
+ return result[2:]
# ---
# Tests.
# ---
- def test_no_user_list(self, fingerserver):
+ def test_no_user_list(self):
""" Test if an unknown user returns an empty result. """
- result = self._send_command(b'user\r\n')
- assert result == b'No user list available.\r\n'
+ assert self._send_command('user') == ('long', '0')
- def test_existing_user_list(self, fingerserver):
+ def test_existing_user_list(self):
""" Test the user list before and after the cron is executed. """
- result = self._send_command(b'\r\n')
-
- assert result != b''
- assert result != b'No user list available.\r\n'
+ assert self._send_command('') == (
+ 'short', '1',
+ 'john|John Doe|/home/john|/bin/bash|84.6|1',
+ 'tty1|',
+ )
sleep(2)
- result = self._send_command(b'\r\n')
- assert result == b'No user list available.\r\n'
+ assert self._send_command('') == ('short', '0')
# End of file.