diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..c02c114
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,24 @@
+name: Build and deploy to PyPi
+
+on:
+ release:
+ types: [created]
+
+jobs:
+ deploy:
+ runs-on: ubuntu-22.04
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v4
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install setuptools wheel twine
+ - name: Build and publish
+ env:
+ TWINE_USERNAME: __token__
+ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
+ run: |
+ python setup.py sdist bdist_wheel
+ twine upload dist/*
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index bcb962c..e99e217 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,9 +1,8 @@
{
- "python.pythonPath": "C:\\Users\\dnorh\\AppData\\Local\\Programs\\Python\\Python39\\python.exe",
- "restructuredtext.confPath": "${workspaceFolder}\\docs",
"python.linting.enabled": true,
"python.linting.flake8Enabled": true,
"python.linting.flake8Args": [
"--ignore=E501"
- ]
+ ],
+ "esbonio.sphinx.confDir": "${workspaceFolder}/docs"
}
\ No newline at end of file
diff --git a/Pipfile.lock b/Pipfile.lock
index 5baab51..08b42a6 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -16,58 +16,59 @@
"default": {
"beautifulsoup4": {
"hashes": [
- "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf",
- "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"
+ "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30",
+ "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"
],
"index": "pypi",
- "version": "==4.10.0"
+ "version": "==4.11.1"
},
"certifi": {
"hashes": [
- "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
- "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
+ "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14",
+ "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"
],
- "version": "==2021.10.8"
+ "markers": "python_version >= '3.6'",
+ "version": "==2022.9.24"
},
"charset-normalizer": {
"hashes": [
- "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45",
- "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c"
+ "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
+ "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
],
- "markers": "python_version >= '3'",
- "version": "==2.0.11"
+ "markers": "python_version >= '3.6'",
+ "version": "==2.1.1"
},
"idna": {
"hashes": [
- "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
- "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
+ "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
+ "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
],
- "markers": "python_version >= '3'",
- "version": "==3.3"
+ "markers": "python_version >= '3.5'",
+ "version": "==3.4"
},
"requests": {
"hashes": [
- "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
- "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
+ "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983",
+ "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"
],
"index": "pypi",
- "version": "==2.27.1"
+ "version": "==2.28.1"
},
"soupsieve": {
"hashes": [
- "sha256:1a3cca2617c6b38c0343ed661b1fa5de5637f257d4fe22bd9f1338010a1efefb",
- "sha256:b8d49b1cd4f037c7082a9683dfa1801aa2597fb11c3a1155b7a5b94829b4f1f9"
+ "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759",
+ "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"
],
"markers": "python_version >= '3.6'",
- "version": "==2.3.1"
+ "version": "==2.3.2.post1"
},
"urllib3": {
"hashes": [
- "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed",
- "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"
+ "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e",
+ "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"
],
- "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.8"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'",
+ "version": "==1.26.12"
}
},
"develop": {
@@ -80,34 +81,35 @@
},
"autopep8": {
"hashes": [
- "sha256:44f0932855039d2c15c4510d6df665e4730f2b8582704fa48f9c55bd3e17d979",
- "sha256:ed77137193bbac52d029a52c59bec1b0629b5a186c495f1eb21b126ac466083f"
+ "sha256:6f09e90a2be784317e84dc1add17ebfc7abe3924239957a37e5040e27d812087",
+ "sha256:ca9b1a83e53a7fad65d731dc7a2a2d50aa48f43850407c59f6a1a306c4201142"
],
"index": "pypi",
- "version": "==1.6.0"
+ "version": "==1.7.0"
},
"babel": {
"hashes": [
- "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9",
- "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"
+ "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51",
+ "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"
],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==2.9.1"
+ "markers": "python_version >= '3.6'",
+ "version": "==2.10.3"
},
"certifi": {
"hashes": [
- "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
- "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
+ "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14",
+ "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"
],
- "version": "==2021.10.8"
+ "markers": "python_version >= '3.6'",
+ "version": "==2022.9.24"
},
"charset-normalizer": {
"hashes": [
- "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45",
- "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c"
+ "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
+ "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
],
- "markers": "python_version >= '3'",
- "version": "==2.0.11"
+ "markers": "python_version >= '3.6'",
+ "version": "==2.1.1"
},
"docutils": {
"hashes": [
@@ -119,117 +121,89 @@
},
"flake8": {
"hashes": [
- "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d",
- "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"
+ "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db",
+ "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"
],
"index": "pypi",
- "version": "==4.0.1"
+ "version": "==5.0.4"
},
"idna": {
"hashes": [
- "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
- "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
+ "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
+ "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
],
- "markers": "python_version >= '3'",
- "version": "==3.3"
+ "markers": "python_version >= '3.5'",
+ "version": "==3.4"
},
"imagesize": {
"hashes": [
- "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c",
- "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"
+ "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b",
+ "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==1.3.0"
+ "version": "==1.4.1"
},
"jinja2": {
"hashes": [
- "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8",
- "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"
+ "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
+ "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
],
- "markers": "python_version >= '3.6'",
- "version": "==3.0.3"
+ "markers": "python_version >= '3.7'",
+ "version": "==3.1.2"
},
"markupsafe": {
"hashes": [
- "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298",
- "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64",
- "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b",
- "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194",
- "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567",
- "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff",
- "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724",
- "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74",
- "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646",
- "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35",
- "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6",
- "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a",
- "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6",
- "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad",
- "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26",
- "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38",
- "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",
- "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145",
- "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",
- "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51",
- "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==2.0.1"
+ "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003",
+ "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88",
+ "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5",
+ "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7",
+ "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a",
+ "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603",
+ "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1",
+ "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135",
+ "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247",
+ "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6",
+ "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601",
+ "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77",
+ "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02",
+ "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e",
+ "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63",
+ "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f",
+ "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980",
+ "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b",
+ "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812",
+ "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff",
+ "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96",
+ "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1",
+ "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925",
+ "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a",
+ "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6",
+ "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e",
+ "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f",
+ "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4",
+ "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f",
+ "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3",
+ "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c",
+ "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a",
+ "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417",
+ "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a",
+ "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a",
+ "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37",
+ "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452",
+ "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933",
+ "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a",
+ "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.1.1"
},
"mccabe": {
"hashes": [
- "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
- "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
+ "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
+ "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
],
- "version": "==0.6.1"
+ "markers": "python_version >= '3.6'",
+ "version": "==0.7.0"
},
"packaging": {
"hashes": [
@@ -241,50 +215,50 @@
},
"pycodestyle": {
"hashes": [
- "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20",
- "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"
+ "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785",
+ "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"
],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
- "version": "==2.8.0"
+ "markers": "python_version >= '3.6'",
+ "version": "==2.9.1"
},
"pyflakes": {
"hashes": [
- "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c",
- "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"
+ "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2",
+ "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"
],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==2.4.0"
+ "markers": "python_version >= '3.6'",
+ "version": "==2.5.0"
},
"pygments": {
"hashes": [
- "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65",
- "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"
+ "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1",
+ "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"
],
- "markers": "python_version >= '3.5'",
- "version": "==2.11.2"
+ "markers": "python_version >= '3.6'",
+ "version": "==2.13.0"
},
"pyparsing": {
"hashes": [
- "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
- "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
+ "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
+ "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
],
- "markers": "python_version >= '3.6'",
- "version": "==3.0.7"
+ "markers": "python_full_version >= '3.6.8'",
+ "version": "==3.0.9"
},
"pytz": {
"hashes": [
- "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c",
- "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"
+ "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197",
+ "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5"
],
- "version": "==2021.3"
+ "version": "==2022.2.1"
},
"requests": {
"hashes": [
- "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
- "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
+ "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983",
+ "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"
],
"index": "pypi",
- "version": "==2.27.1"
+ "version": "==2.28.1"
},
"snowballstemmer": {
"hashes": [
@@ -295,11 +269,11 @@
},
"sphinx": {
"hashes": [
- "sha256:5da895959511473857b6d0200f56865ed62c31e8f82dd338063b84ec022701fe",
- "sha256:6caad9786055cb1fa22b4a365c1775816b876f91966481765d7d50e9f0dd35cc"
+ "sha256:7225c104dc06169eb73b061582c4bc84a9594042acae6c1582564de274b7df2f",
+ "sha256:9150a8ed2e98d70e778624373f183c5498bf429dd605cf7b63e80e2a166c35a5"
],
"index": "pypi",
- "version": "==4.4.0"
+ "version": "==5.2.2"
},
"sphinx-rtd-theme": {
"hashes": [
@@ -367,11 +341,11 @@
},
"urllib3": {
"hashes": [
- "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed",
- "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"
+ "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e",
+ "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"
],
- "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.8"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'",
+ "version": "==1.26.12"
}
}
}
diff --git a/README.md b/README.md
index 0d29296..de05fad 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,9 @@
# lectio.py
-![](https://img.shields.io/github/license/dnorhoj/lectio.py)
-
+[![License](https://img.shields.io/github/license/dnorhoj/lectio.py)](LICENSE)
+[![Documentation Status](https://readthedocs.org/projects/lectiopy/badge/?version=latest)](https://lectiopy.readthedocs.io/en/latest/?badge=latest)
+[![PyPi version](https://img.shields.io/pypi/v/lectio.py.svg)](https://pypi.org/project/lectio.py/)
+[![PyPi supported python versions](https://img.shields.io/pypi/pyversions/lectio.py.svg)](https://python.org/)
*Please note: This library is nowhere close to done.*
@@ -14,13 +16,37 @@ You can read the documentation
## How do i use this?
-To use this repository feel free to check the documentation [here](https://lectiopy.rtfd.io/).
+You can install this library via `pip`:
+
+ pip install lectio.py
+
+For a quickstart guide as well as some examples, you can read the documentation [here](https://lectiopy.rtfd.io/).
+
+## Progress
+
+
+ Implementation progress
+
+* [x] Schedule
+* [x] User info
+* [ ] Absence
+* [ ] Mail
+* [ ] Assignments
+* [ ] Homework
+* [ ] Surveys (Probably not going to be implemented)
+* [ ] Grades
+* [ ] Search for students / teachers
+
+
## Todo
-* [ ] Finish authentication check
* [ ] Make a better README
-* [ ] Start doing the hard part (basically everything)
+* [ ] Quickstart, Examples, etc.
+
+## Known bugs
+
+* Not made to work with teacher accounts (as i have no way of testing anything)
## Contributing
@@ -30,4 +56,4 @@ If you notice something that isn't working as intended you can start an issue.
## License
-This repository uses the `GNU Lesser General Public License v3.0` you can read more about it in [LICENSE](LICENSE).
\ No newline at end of file
+This repository uses the `GNU Lesser General Public License v3.0` you can read more about it in [LICENSE](LICENSE).
diff --git a/docs/conf.py b/docs/conf.py
index 662a94a..7be8703 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -18,11 +18,11 @@
# -- Project information -----------------------------------------------------
project = 'lectio.py'
-copyright = '2020, dnorhoj'
+copyright = '2022, dnorhoj'
author = 'dnorhoj'
# The full version, including alpha/beta/rc tags
-release = '0.0.1'
+release = '0.2.0'
# -- General configuration ---------------------------------------------------
diff --git a/docs/index.rst b/docs/index.rst
index 7743660..6e60ec0 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,9 +1,30 @@
-Welcome to lectio.py's documentation!
+Welcome to lectio.py
=======================================
+Lectio.py is a Python library for reading and interacting with lectio.dk,
+a Danish school management system.
+
+.. note::
+ This library is not affiliated with lectio.dk or macom in any way.
+ Nor is it endorsed by them. It scrapes the website as there is no
+ official API.
+
+**Features:**
+
+- Pythonic API
+- Easy to use
+
+Information
+-----------
+
+**Useful links:** :doc:`/installation` | :doc:`/quickstart`
+**Reference:** :doc:`/reference` | :doc:`/exceptions`
+
+Table of contents
+-----------------
+
.. toctree::
:maxdepth: 2
- :caption: Contents:
installation
quickstart
diff --git a/docs/installation.rst b/docs/installation.rst
index 75a91e8..ca24c90 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -1,4 +1,20 @@
Installation
============
-TODO
\ No newline at end of file
+To install lectio.py and its dependencies, you can simply install it from PyPi::
+
+ pip install lectio.py
+
+or clone the repository and install it manually::
+
+ git clone https://github.com/dnorhoj/Lectio.py.git
+ cd lectio.py
+ python setup.py install
+
+You can check if the installation was successful by running::
+
+ python
+
+ >>> import lectio
+ >>> lectio.__version__
+
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 474a449..8925be2 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -1,4 +1,31 @@
Quickstart
==========
-TODO
\ No newline at end of file
+Let's start with a simple example, where we will get your schedule for today.
+
+Schedule
+--------
+
+Create a new file, and add the following code:
+
+.. code-block:: python
+
+ from lectio import Lectio
+ from datetime import datetime, timedelta
+
+ lec = Lectio('', '')
+
+ # Get my user object
+ me = lec.me()
+
+ # Get the schedule for today
+ schedule = me.get_schedule(datetime.now(), datetime.now() + timedelta(days=1))
+
+We now have a list of all :class:`lectio.helpers.Module` in the ``schedule`` variable. Let's print it:
+
+.. code-block:: python
+
+ for module in schedule:
+ print(module.title, module.teacher, module.room)
+
+**More to come...!**
diff --git a/docs/reference.rst b/docs/reference.rst
index 0683dfb..22c82e4 100644
--- a/docs/reference.rst
+++ b/docs/reference.rst
@@ -1,6 +1,53 @@
+.. currentmodule:: lectio
+
API Reference
=============
-.. automodule:: lectio.lectio
+Lectio
+------
+
+.. autoclass:: Lectio
+ :members:
+ :undoc-members:
+
+
+School
+------
+
+.. autoclass:: lectio.models.school.School
+ :members:
+ :undoc-members:
+
+User
+----
+
+User
+^^^^
+
+.. autoclass:: lectio.models.user.User
+ :members:
+ :undoc-members:
+
+Me
+^^^
+
+.. autoclass:: lectio.models.user.Me
+ :members:
+ :undoc-members:
+
+User types
+^^^^^^^^^^
+
+.. autoclass:: lectio.models.user.UserType
+ :members:
+ :undoc-members:
+
+Misc
+----
+
+Module
+^^^^^^
+
+.. autoclass:: lectio.helpers.schedule.Module
:members:
:undoc-members:
\ No newline at end of file
diff --git a/lectio/__init__.py b/lectio/__init__.py
index 8709e99..934296d 100644
--- a/lectio/__init__.py
+++ b/lectio/__init__.py
@@ -1,4 +1,7 @@
# flake8: noqa
-from .lectio import Lectio, Module
-from . import lectio
-from . import exceptions
+from .lectio import Lectio
+from .helpers.schedule import Module
+from .exceptions import *
+from .models.user import User, UserType
+from .models.school import School
+from .models import *
\ No newline at end of file
diff --git a/lectio/exceptions.py b/lectio/exceptions.py
index 1181afa..07b8990 100644
--- a/lectio/exceptions.py
+++ b/lectio/exceptions.py
@@ -1,3 +1,6 @@
+"""Here are all the custom Lectio.py exceptions as well as their explanation"""
+
+
class LectioError(Exception):
"""Base lectio.py exception"""
@@ -8,3 +11,11 @@ class UnauthenticatedError(LectioError):
class IncorrectCredentialsError(LectioError):
"""Incorrect credentials error, mostly thrown in auto-login on session expired"""
+
+
+class InstitutionDoesNotExistError(LectioError):
+ """The institution with the id you provided does not exist."""
+
+
+class UserDoesNotExistError(LectioError):
+ """The user does not exist."""
diff --git a/lectio/helpers/__init__.py b/lectio/helpers/__init__.py
new file mode 100644
index 0000000..aa6e1c6
--- /dev/null
+++ b/lectio/helpers/__init__.py
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .schedule import *
diff --git a/lectio/helpers/schedule.py b/lectio/helpers/schedule.py
new file mode 100644
index 0000000..6e14174
--- /dev/null
+++ b/lectio/helpers/schedule.py
@@ -0,0 +1,179 @@
+import re
+from datetime import datetime, timedelta
+from typing import TYPE_CHECKING, List
+from bs4 import BeautifulSoup
+
+if TYPE_CHECKING:
+ from ..lectio import Lectio
+
+
+class Module:
+ """Lectio module object
+
+ Represents a lectio module
+
+ Args:
+ title (str|None): Optional description of module (not present in all modules)
+ subject (str|None): "Hold" from lectio, bascially which subject.
+ Example: `1.a Da`
+ teacher (str|None): Initials of teacher.
+ Example: `abcd`
+ room (str|None): Room name of module.
+ Example: `0.015`
+ extra_info (str|None): Extra info from module, includes homework and other info.
+ start_time (:class:`datetime.datetime`): Start time of module
+ end_time (:class:`datetime.datetime`): End time of module
+ status (int): 0=normal, 1=changed, 2=cancelled
+ url (str|None): Url for more info for the module
+ """
+
+ def __init__(self, **kwargs) -> None:
+ self.title = kwargs.get("title")
+ self.subject = kwargs.get("subject")
+ self.teacher = kwargs.get("teacher")
+ self.room = kwargs.get("room")
+ self.extra_info = kwargs.get("extra_info")
+ self.start_time = kwargs.get("start_time")
+ self.end_time = kwargs.get("end_time")
+ self.status = kwargs.get("status")
+ self.url = kwargs.get("url")
+
+ def __repr__(self) -> str:
+ return f"Module({self.subject}, {self.start_time}, {self.end_time})"
+
+ def display(self):
+ print(f"Title: {self.title}")
+ print(f"Subject(s): {self.subject}")
+ print(f"Teacher(s): {self.teacher}")
+ print(f"Room(s): {self.room}")
+ print(f"Starts at: {self.start_time}")
+ print(f"Ends at: {self.end_time}")
+ print(f"Status: {self.status}")
+ print(f"URL: {self.url}")
+ print(f"Extra info:\n\n{self.extra_info}")
+
+
+def get_schedule(lectio: 'Lectio', params: List[str], start_date: datetime, end_date: datetime, strip_time: bool = True) -> List[Module]:
+ """Get lectio schedule for current or specific week.
+
+ Get all modules in specified time range.
+
+ Parameters:
+ lectio (:class:`lectio.Lectio`): Base lectio object
+ params (list): List of get parameters to add to request
+ start_date (:class:`datetime.datetime`): Start date
+ end_date (:class:`datetime.datetime`): End date
+ strip_time (bool): Whether to remove hours, minutes and seconds from date info, also adds 1 day to end time.
+ Basically just allows you to put in a random time of two days, and still get all modules from all the days including start and end date.
+
+ Returns:
+ List[:class:`lectio.Module`]: List containing all modules in specified time range.
+ """
+
+ replacetime = {}
+ if strip_time:
+ end_date = end_date + timedelta(days=1)
+ replacetime = {
+ "hour": 0,
+ "minute": 0,
+ "second": 0,
+ }
+
+ start_date = start_date.replace(
+ **replacetime, microsecond=0).isoformat()
+ end_date = end_date.replace(**replacetime, microsecond=0).isoformat()
+
+ params = "&".join([
+ "type=ShowListAll",
+ f"starttime={start_date}",
+ f"endtime={end_date}",
+ "dagsbemaerk=0",
+ *params
+ ])
+
+ schedule_request = lectio._request(f"SkemaAvanceret.aspx?{params}")
+
+ soup = BeautifulSoup(schedule_request.text, 'html.parser')
+
+ module_table = soup.find(
+ "table", class_="list texttop lf-grid")
+
+ if not module_table:
+ return []
+
+ # Not a good way of checking if there are no modules, but it works
+ if module_table.find("div", {"class": "noRecord"}):
+ return []
+
+ modules = module_table.findChildren('tr', class_=None)
+
+ schedule = []
+ for module in modules:
+ a = module.findChild('a')
+ module = parse_additionalinfo(
+ a.attrs.get('data-additionalinfo'))
+
+ # Add href to module
+ href = a.attrs.get('href')
+ if href:
+ module.url = f"https://www.lectio.dk{href}"
+
+ schedule.append(module)
+
+ return schedule
+
+
+def parse_additionalinfo(info: str) -> Module:
+ module = Module()
+
+ info_list = info.split('\n')
+
+ # Parse module status
+ if info_list[0] == 'Ændret!':
+ module.status = 1
+ info_list.pop(0)
+ elif info_list[0] == 'Aflyst!':
+ module.status = 2
+ info_list.pop(0)
+ else:
+ module.status = 0
+
+ # Parse title
+ if not re.match(r'^[0-9]{1,2}\/[0-9]{1,2}-[0-9]{4} [0-9]{2}:[0-9]{2}', info_list[0]):
+ module.title = info_list[0]
+ info_list.pop(0)
+
+ # Parse time
+ times = info_list[0].split(" til ")
+ info_list.pop(0)
+ module.start_time = datetime.strptime(times[0], "%d/%m-%Y %H:%M")
+ if len(times[1]) == 5:
+ module.end_time = datetime.strptime(
+ times[0][:-5] + times[1], "%d/%m-%Y %H:%M")
+ else:
+ module.end_time = datetime.strptime(times[1], "%d/%m-%Y %H:%M")
+
+ # Parse subject(s)
+ subject = re.search(r"Hold: (.*)", info)
+ if subject:
+ info_list.pop(0)
+ module.subject = subject[1]
+
+ # Parse teacher(s)
+ teacher = re.search(r"Lærere?: (.*)", info)
+ if teacher:
+ info_list.pop(0)
+ module.teacher = teacher[1]
+
+ # Parse room(s)
+ room = re.search(r"Lokaler?: (.*)", info)
+ if room:
+ info_list.pop(0)
+ module.room = room[1]
+
+ # Put any additional info into extra_info
+ if info_list:
+ info_list.pop(0)
+ module.extra_info = "\n".join(info_list)
+
+ return module
diff --git a/lectio/lectio.py b/lectio/lectio.py
index cbd68c5..14f4da0 100644
--- a/lectio/lectio.py
+++ b/lectio/lectio.py
@@ -1,51 +1,11 @@
-from datetime import datetime, timedelta
import re
import requests
from bs4 import BeautifulSoup
-from . import exceptions
-
-
-class Module:
- """Lectio module object
- Represents a lectio module
-
- Args:
- title (str|None): Optional description of module (not present in all modules)
- subject (str|None): "Hold" from lectio, bascially which subject.
- Example: `1.a Da`
- teacher (str|None): Initials of teacher.
- Example: `abcd`
- room (str|None): Room name of module.
- Example: `0.015`
- extra_info (str|None): Extra info from module, includes homework and other info.
- start_time (:class:`datetime.datetime`): Start time of module
- end_time (:class:`datetime.datetime`): End time of module
- status (int): 0=normal, 1=changed, 2=cancelled
- url (str|None): Url for more info for the module
- """
+from . import exceptions
- def __init__(self, **kwargs) -> None:
- self.title = kwargs.get("title")
- self.subject = kwargs.get("subject")
- self.teacher = kwargs.get("teacher")
- self.room = kwargs.get("room")
- self.extra_info = kwargs.get("extra_info")
- self.start_time = kwargs.get("start_time")
- self.end_time = kwargs.get("end_time")
- self.status = kwargs.get("status")
- self.url = kwargs.get("url")
-
- def display(self):
- print(f"Title: {self.title}")
- print(f"Subject(s): {self.subject}")
- print(f"Teacher(s): {self.teacher}")
- print(f"Room(s): {self.room}")
- print(f"Starts at: {self.start_time}")
- print(f"Ends at: {self.end_time}")
- print(f"Status: {self.status}")
- print(f"URL: {self.url}")
- print(f"Extra info:\n\n{self.extra_info}")
+from .models.user import Me, User, UserType
+from .models.school import School
class Lectio:
@@ -61,16 +21,17 @@ class Lectio:
https://www.lectio.dk/lectio/123/login.aspx
- Here, the `123` would be my institution id.
+ Here, the ``123`` would be my institution id.
"""
+ __school: School = None
+ __me: Me = None
+
def __init__(self, inst_id: int) -> None:
- self.__INST_ID = inst_id
- self.__BASE_URL = f"https://www.lectio.dk/lectio/{str(inst_id)}"
self.__CREDS = []
-
self.__session = requests.Session()
- # TODO: Check if inst_id is valid
+
+ self.inst_id = inst_id
def authenticate(self, username: str, password: str, save_creds: bool = True) -> bool:
"""Authenticates you on Lectio.
@@ -87,19 +48,23 @@ def authenticate(self, username: str, password: str, save_creds: bool = True) ->
save_creds (bool): Whether the credentials should be saved in the object (useful for auto relogin on logout)
Raises:
- lectio.IncorrectCredentialsError: When incorrect credentials passed
+ :class:`exceptions.IncorrectCredentialsError`: When incorrect credentials passed
+ :class:`exceptions.InstitutionDoesNotExistError`: When the institution id passed on creation of object is invalid
Example::
- from lectio import Lectio, errors
+ from lectio import Lectio, exceptions
lect = Lectio(123)
try:
- lect.authenticate("username", "password"):
+ lect.authenticate("username", "password")
print("Authenticated")
- except errors.IncorrectCredentialsError:
+ except exceptions.IncorrectCredentialsError:
print("Not authenticated")
+ exit(1)
+
+ ...
"""
self.__CREDS = []
@@ -109,180 +74,88 @@ def authenticate(self, username: str, password: str, save_creds: bool = True) ->
# Call the actual authentication method
self._authenticate(username, password)
- # Check if authentication passed
- self._request(self.__BASE_URL + "/forside.aspx")
-
- def _authenticate(self, username: str = None, password: str = None) -> bool:
+ def _authenticate(self, username: str = None, password: str = None):
if username is None or password is None:
if len(self.__CREDS) != 2:
raise exceptions.UnauthenticatedError(
- "Auto auth failed, did you authenticate?")
+ "No authentication details provided and no saved credentials found!")
username, password = self.__CREDS
self.log_out() # Clear session
- login_page = self.__session.get(self.__BASE_URL + "/login.aspx")
+ URL = f"https://www.lectio.dk/lectio/{self.inst_id}/login.aspx"
- if login_page.status_code != 200:
- return False
+ login_page = self.__session.get(URL)
+
+ if 'fejlhandled.aspx?title=Skolen+eksisterer+ikke' in login_page.url:
+ raise exceptions.InstitutionDoesNotExistError(
+ f"The institution with the id '{self._INST_ID}' does not exist!")
parser = BeautifulSoup(login_page.text, "html.parser")
- self.__session.post(
- self.__BASE_URL + "/login.aspx",
- data={
- "time": 0,
- "__EVENTTARGET": "m$Content$submitbtn2",
- "__EVENTARGUMENT": "",
- "__SCROLLPOSITION": "",
- "__VIEWSTATEX": parser.find(attrs={"name": "__VIEWSTATEX"})["value"],
- "__VIEWSTATEY_KEY": "",
- "__VIEWSTATE": "",
- "__EVENTVALIDATION": parser.find(attrs={"name": "__EVENTVALIDATION"})["value"],
- "m$Content$username": username,
- "m$Content$password": password
- }
- )
-
- def get_user_id(self) -> int:
- """Gets your user id
+ r = self.__session.post(URL, data={
+ "time": 0,
+ "__EVENTTARGET": "m$Content$submitbtn2",
+ "__EVENTARGUMENT": "",
+ "__SCROLLPOSITION": "",
+ "__VIEWSTATEX": parser.find(attrs={"name": "__VIEWSTATEX"})["value"],
+ "__VIEWSTATEY_KEY": "",
+ "__VIEWSTATE": "",
+ "__EVENTVALIDATION": parser.find(attrs={"name": "__EVENTVALIDATION"})["value"],
+ "m$Content$username": username,
+ "m$Content$password": password
+ })
+
+ if r.url == URL:
+ # Authentication failed
+ raise exceptions.IncorrectCredentialsError(
+ "Incorrect credentials provided!")
+
+ def school(self) -> School:
+ """Returns a :class:`lectio.models.school.School` object for the given institution id.
Returns:
- int: User id
+ :class:`lectio.models.school.School`: The school object for the authenticated user.
"""
- r = self._request(self.__BASE_URL + "/forside.aspx")
+ if self.__school is None:
+ self.__school = School(self)
- soup = BeautifulSoup(r.text, 'html.parser')
+ return self.__school
- content = soup.find(
- 'meta', {'name': 'msapplication-starturl'}).attrs.get('content')
+ def me(self) -> Me:
+ """Gets the authenticated user
- user_id = int(re.match(r'.*id=([0-9]+)$', content)[1])
+ Returns:
+ :class:`lectio.models.user.Me`: Own user object
+ """
- return user_id
+ if self.__me is None:
+ r = self._request("forside.aspx")
- def get_schedule_for_student(self, elevid: int, start_date: datetime, end_date: datetime, strip_time: bool = True) -> any:
- """Get lectio schedule for current or specific week.
+ soup = BeautifulSoup(r.text, 'html.parser')
- Get all modules in specified time range.
+ content = soup.find(
+ 'meta', {'name': 'msapplication-starturl'}).attrs.get('content')
- Parameters:
- elevid (int): Student id
- start_date (:class:`datetime.datetime`): Start date
- end_date (:class:`datetime.datetime`): End date
- strip_time (bool): Whether to remove hours, minutes and seconds from date info, also adds 1 day to end time.
- Basically just allows you to put in a random time of two days, and still get all modules from all the days including start and end date.
+ user_id = re.match(r'.*id=([0-9]+)$', content)[1]
- Returns:
- list: List containing all modules in specified time range.
- """
+ # TODO; Add support for teachers
+ self.__me = Me(self, user_id, UserType.STUDENT)
- replacetime = {}
- if strip_time:
- end_date = end_date + timedelta(days=1)
- replacetime = {
- "hour": 0,
- "minute": 0,
- "second": 0,
- }
-
- start_date = start_date.replace(
- **replacetime, microsecond=0).isoformat()
- end_date = end_date.replace(**replacetime, microsecond=0).isoformat()
-
- params = (
- "type=ShowListAll&"
- f"starttime={start_date}&"
- f"endtime={end_date}&"
- "dagsbemaerk=0&"
- f"studentsel={elevid}"
- )
-
- schedule_request = self._request(
- f"{self.__BASE_URL}/SkemaAvanceret.aspx?{params}")
-
- soup = BeautifulSoup(schedule_request.text, 'html.parser')
-
- modules = soup.find(
- "table", class_="list texttop lf-grid").findChildren('tr', class_=None)
-
- schedule = []
- for module in modules:
- a = module.findChild('a')
- module = self._parse_additionalinfo(
- a.attrs.get('data-additionalinfo'))
-
- href = a.attrs.get('href')
- if href is not None:
- module.url = f"https://www.lectio.dk{href}"
- schedule.append(module)
-
- return schedule
-
- def _parse_additionalinfo(self, info: str) -> Module:
- module = Module()
-
- info_list = info.split('\n')
-
- # Parse module status
- if info_list[0] == 'Ændret!':
- module.status = 1
- info_list.pop(0)
- elif info_list[0] == 'Aflyst!':
- module.status = 2
- info_list.pop(0)
- else:
- module.status = 0
-
- # Parse title
- if not re.match(r'^[0-9]{1,2}\/[0-9]{1,2}-[0-9]{4} [0-9]{2}:[0-9]{2}', info_list[0]):
- module.title = info_list[0]
- info_list.pop(0)
-
- # Parse time
- times = info_list[0].split(" til ")
- info_list.pop(0)
- module.start_time = datetime.strptime(times[0], "%d/%m-%Y %H:%M")
- if len(times[1]) == 5:
- module.end_time = datetime.strptime(
- times[0][:-5] + times[1], "%d/%m-%Y %H:%M")
- else:
- module.end_time = datetime.strptime(times[1], "%d/%m-%Y %H:%M")
-
- # Parse subject(s)
- subject = re.search(r"Hold: (.*)", info)
- if subject:
- info_list.pop(0)
- module.subject = subject[1]
-
- # Parse teacher(s)
- teacher = re.search(r"Lærere?: (.*)", info)
- if teacher:
- info_list.pop(0)
- module.teacher = teacher[1]
-
- # Parse room(s)
- room = re.search(r"Lokaler?: (.*)", info)
- if room:
- info_list.pop(0)
- module.room = room[1]
-
- # Put any additional info into extra_info
- if info_list:
- info_list.pop(0)
- module.extra_info = "\n".join(info_list)
-
- return module
+ return self.__me
def _request(self, url: str, method: str = "GET", **kwargs) -> requests.Response:
- r = self.__session.request(method, url, **kwargs)
-
- if f"{self.__INST_ID}/login.aspx?prevurl=" in r.url:
- self._authenticate()
- r = self.__session.get(url)
- if f"{self.__INST_ID}/login.aspx?prevurl=" in r.url:
+ r = self.__session.request(
+ method, f"https://www.lectio.dk/lectio/{str(self.inst_id)}/{url}", **kwargs)
+
+ if f"{self.inst_id}/login.aspx?prevurl=" in r.url:
+ if not self._authenticate():
+ raise exceptions.UnauthenticatedError("Unauthenticated")
+ r = self.__session.get(
+ f"https://www.lectio.dk/lectio/{str(self.inst_id)}/{url}")
+ if f"{self.inst_id}/login.aspx?prevurl=" in r.url:
raise exceptions.IncorrectCredentialsError(
"Could not restore session, probably incorrect credentials")
diff --git a/lectio/models/__init__.py b/lectio/models/__init__.py
new file mode 100644
index 0000000..2c2b9de
--- /dev/null
+++ b/lectio/models/__init__.py
@@ -0,0 +1,3 @@
+# flake8: noqa
+from .school import School
+from .user import User, Me, UserType
diff --git a/lectio/models/school.py b/lectio/models/school.py
new file mode 100644
index 0000000..5f7b327
--- /dev/null
+++ b/lectio/models/school.py
@@ -0,0 +1,226 @@
+from typing import TYPE_CHECKING, List
+from bs4 import BeautifulSoup
+import re
+from urllib.parse import quote
+
+from .user import User, UserType
+from ..import exceptions
+
+if TYPE_CHECKING:
+ from .. import Lectio
+
+
+class School:
+ """A school object.
+
+ Represents a school.
+
+ Note:
+ This class should not be instantiated directly,
+ but rather through the :meth:`lectio.Lectio.get_school` method.
+
+ Args:
+ lectio (:class:`lectio.Lectio`): Lectio object
+ """
+
+ def __init__(self, lectio: 'Lectio') -> None:
+ self._lectio = lectio
+ self.__populate()
+
+ def __populate(self) -> None:
+ r = self._lectio._request("forside.aspx")
+
+ soup = BeautifulSoup(r.text, 'html.parser')
+
+ self.name = soup.find(
+ "div", {"id": "s_m_masterleftDiv"}).text.strip().split("\n")[0].replace("\r", "")
+
+ def get_user_by_id(self, user_id: str, user_type: UserType = UserType.STUDENT, check: bool = True) -> User:
+ """Gets a user by their id
+
+ Args:
+ user_id (str): The id of the user
+ user_type (:class:`lectio.models.user.UserType`): The type of the user (student or teacher)
+ check (bool): Whether to check if the user exists (slower)
+
+ Returns:
+ :class:`lectio.models.user.User`: User object
+
+ Raises:
+ :class:`lectio.exceptions.UserDoesNotExistError`: When the user does not exist
+ """
+
+ if check:
+ # Check if user exists
+ r = self._lectio._request(
+ f"SkemaNy.aspx?type={user_type}&{user_type}id={user_id}")
+
+ soup = BeautifulSoup(r.text, 'html.parser')
+
+ if soup.title.string.strip().startswith("Fejl - Lectio"):
+ raise exceptions.UserDoesNotExistError(
+ f"The {UserType.get_str(user_type, True)} with the id '{user_id}' does not exist!")
+
+ return User(self._lectio, user_id, user_type)
+
+ def get_teachers(self) -> List[User]:
+ """Get all teachers
+
+ Returns:
+ list(:class:`lectio.models.user.User`): List of teachers
+ """
+
+ r = self._lectio._request("FindSkema.aspx?type=laerer&sortering=id")
+
+ soup = BeautifulSoup(r.text, 'html.parser')
+
+ teachers = []
+
+ # Container containing all teachers
+ lst = soup.find("ul", {"class": "ls-columnlist mod-onechild"})
+
+ if lst is None:
+ return []
+
+ # Iterate and create user objects
+ for i in lst.find_all("li"):
+ user_id = int(i.a["href"].split("=")[-1])
+
+ user_name = i.a.contents[1].strip()
+
+ initial_span = i.a.find('span')
+
+ user_initials = None
+ if initial_span is not None:
+ user_initials = initial_span.text.strip()
+
+ teachers.append(User(self._lectio,
+ user_id,
+ UserType.TEACHER,
+ lazy=True,
+ name=user_name,
+ initials=user_initials))
+
+ return teachers
+
+ def search_for_teachers(self, query: str) -> List[User]:
+ """Search for teachers
+
+ Note:
+ This method is not very reliable, and will sometimes return no results.
+ Also, the query has to be from the beginning of the name.
+
+ Example: Searching for "John" will return "John Doe", but searching for "Doe" will not.
+
+ Args:
+ query (str): Name to search for
+
+ Returns:
+ list(:class:`lectio.models.user.User`): List of teachers
+ """
+
+ res = []
+
+ for teacher in self.get_teachers():
+ if teacher.name.lower().startswith(query.lower()):
+ res.append(teacher)
+
+ return res
+
+ def get_students_by_letter(self, letter: str) -> List[User]:
+ """Get students by first letter of name
+
+ Args:
+ letter (str): Letter to search for
+
+ Returns:
+ list(:class:`lectio.models.user.User`): List of students
+ """
+
+ r = self._lectio._request(
+ "FindSkema.aspx?type=elev&forbogstav=" + quote(letter.upper()))
+
+ soup = BeautifulSoup(r.text, 'html.parser')
+
+ students = []
+
+ # Container containing all students
+ lst = soup.find("ul", {"class": "ls-columnlist mod-onechild"})
+
+ # Shouldn't happen in cases other than ``len(letter) > 1`` or if letter is not a valid character
+ if lst is None:
+ return []
+
+ # Iterate and create user objects
+ for i in lst.find_all("li"):
+ user_id = int(i.a["href"].split("=")[-1])
+
+ user_info = i.a.text.strip()
+
+ # Search for name and class
+ search = re.search(
+ r"(?P.*) \((?P.*?) \d+?\)", user_info)
+
+ if search is None:
+ continue
+
+ students.append(User(self._lectio,
+ user_id,
+ lazy=True,
+ name=search.group("name"),
+ class_name=search.group("class_name")))
+
+ return students
+
+ def search_for_students(self, query: str) -> List[User]:
+ """Search for user
+
+ Note:
+ This method is not very reliable, and will sometimes return no results.
+ Also, the query has to be from the beginning of the name.
+
+ Example: Searching for "John" will return "John Doe", but searching for "Doe" will not.
+
+ Args:
+ query (str): Name to search for
+
+ Returns:
+ list(:class:`lectio.User`): List of users
+ """
+
+ res = []
+
+ for student in self.get_students_by_letter(query[0]):
+ if student.name.lower().startswith(query.lower()):
+ res.append(student)
+
+ return res
+
+ def get_all_students(self) -> List[User]:
+ """Get all students
+
+ Returns:
+ list(:class:`User`): List of students
+ """
+
+ res = []
+
+ for letter in "abcdefghijklmnopqrstuvwxyzæøå":
+ res.extend(self.get_students_by_letter(letter))
+
+ return res
+
+ def search_for_users(self, query: str) -> List[User]:
+ """Search for user
+
+ Args:
+ query (str): Name to search for
+
+ Returns:
+ list(:class:`lectio.models.user.User`): List of users
+ """
+
+ return [*self.search_for_students(query), *self.search_for_teachers(query)]
+
+ def __repr__(self) -> str:
+ return f"School({self.name})"
diff --git a/lectio/models/user.py b/lectio/models/user.py
new file mode 100644
index 0000000..bd3f78b
--- /dev/null
+++ b/lectio/models/user.py
@@ -0,0 +1,196 @@
+from enum import Enum
+from bs4 import BeautifulSoup
+from typing import TYPE_CHECKING, List
+
+from ..helpers.schedule import get_schedule
+
+if TYPE_CHECKING:
+ from datetime import datetime
+ from ..helpers.schedule import Module
+ from ..lectio import Lectio
+
+
+class UserType(Enum):
+ """User types enum
+
+ Example:
+ >>> from lectio import Lectio
+ >>> from lectio.models.user import UserType
+ >>> lec = Lectio(123)
+ >>> lec.authenticate("username", "password")
+ >>> me = lec.me()
+ >>> print(me.type)
+ 0
+ >>> print(me.type == UserType.STUDENT)
+ True
+ """
+
+ STUDENT = 0
+ TEACHER = 1
+
+ def get_str(self) -> str:
+ """Get string representation of user type for lectio interface in english
+
+ Returns:
+ str: String representation of user type
+ """
+
+ if self.value == self.STUDENT.value:
+ return "student"
+ elif self.value == self.TEACHER.value:
+ return "teacher"
+
+ def __str__(self) -> str:
+ if self.value == self.STUDENT.value:
+ return "elev"
+ elif self.value == self.TEACHER.value:
+ return "laerer"
+
+
+class User:
+ """Lectio user object
+
+ Represents a lectio user
+
+ Note:
+ This class should not be instantiated directly,
+ but rather through the :meth:`lectio.Lectio.get_user`
+ or :meth:`lectio.models.school.School.search_for_users` methods or similar.
+
+ Args:
+ lectio (:class:`lectio.Lectio`): Lectio object
+ user_id (int): User id
+ user_type (:class:`lectio.models.user.UserType`): User type (UserType.STUDENT or UserType.TEACHER)
+ lazy (bool): Whether to not populate user object on instantiation (default: False)
+
+ Attributes:
+ id (int): User id
+ type (:class:`lectio.models.user.UserType`): User type (UserType.STUDENT or UserType.TEACHER)
+ """
+
+ __name = None
+ __initials = None
+ __class_name = None
+ __image = None
+
+ def __init__(self, lectio: 'Lectio', user_id: int, user_type: UserType = UserType.STUDENT, *, lazy=False, **user_data) -> None:
+ self._lectio = lectio
+ self.id = user_id
+
+ self.type = user_type
+
+ if not lazy:
+ self.__populate()
+ else:
+ self.__name = user_data.get("name")
+ self.__initials = user_data.get("initials")
+ self.__class_name = user_data.get("class_name")
+ self.__image = user_data.get("image")
+
+ def __populate(self) -> None:
+ """Populate user object
+
+ Populates the user object with data from lectio, such as name, class name, etc.
+ """
+
+ # TODO; Check if user is student or teacher
+
+ # Get user's schedule for today
+ r = self._lectio._request(
+ f"SkemaNy.aspx?type={self.type}&{self.type}id={self.id}")
+
+ soup = BeautifulSoup(r.text, "html.parser")
+
+ title = soup.find("div", {"id": "s_m_HeaderContent_MainTitle"}).text
+
+ title = " ".join(title.split()[1:])
+
+ if self.type == UserType.STUDENT:
+ self.__name = title.split(", ")[0]
+ self.__class_name = title.split(", ")[1].split(" - ")[0]
+ elif self.type == UserType.TEACHER:
+ self.__initials, self.__name, *_ = title.split(" - ")
+
+ src = soup.find(
+ "img", {"id": "s_m_HeaderContent_picctrlthumbimage"}).get("src")
+
+ self.__image = f"https://www.lectio.dk{src}&fullsize=1"
+
+ def get_schedule(self, start_date: 'datetime', end_date: 'datetime', strip_time: bool = True) -> List['Module']:
+ """Get schedule for user
+
+ Note:
+ As lectio is weird, you can only get a schedule for a range
+ that is less than one month.
+ If you specify a range greater than one month, you will get an empty return list.
+
+ Args:
+ start_date (:class:`datetime.datetime`): Start date
+ end_date (:class:`datetime.datetime`): End date
+ strip_time (bool): Whether to remove hours, minutes and seconds from date info, also adds 1 day to end time.
+ Basically just allows you to put in a random time of two days, and still get all modules from all the days including start and end date.
+ """
+
+ return get_schedule(
+ self._lectio,
+ [f"{self.type.get_str()}sel={self.id}"],
+ start_date,
+ end_date,
+ strip_time
+ )
+
+ def __repr__(self) -> str:
+ return f"User({self.type.get_str().capitalize()}, {self.id})"
+
+ @property
+ def name(self) -> str:
+ """str: User's name"""
+
+ if not self.__name:
+ self.__populate()
+
+ return self.__name
+
+ @property
+ def image(self) -> str:
+ """str: User's image url"""
+
+ if not self.__image:
+ self.__populate()
+
+ return self.__image
+
+ @property
+ def initials(self) -> str:
+ """str|None: User's initials (only for teachers)"""
+
+ if self.type == UserType.STUDENT:
+ return None
+
+ if not self.__initials:
+ self.__populate()
+
+ return self.__initials
+
+ @property
+ def class_name(self) -> str:
+ """str|None: User's class name (only for students)"""
+
+ if self.type == UserType.TEACHER:
+ return None
+
+ if not self.__class_name:
+ self.__populate()
+
+ return self.__class_name
+
+ def __eq__(self, __o: object) -> bool:
+ if not isinstance(__o, User):
+ return False
+
+ return self.id == __o.id and self.type == __o.type
+
+
+class Me(User):
+ # TODO: Add methods for getting grades, absences, etc.
+ pass
diff --git a/setup.py b/setup.py
index 032f5b7..9ed3735 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@
setuptools.setup(
name="lectio.py",
- version="0.1.1",
+ version="0.2.0",
author="dnorhoj",
author_email="daniel.norhoj@gmail.com",
description="Interact with lectio through python",