diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..62868e0 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,26 @@ +[run] +branch = true +source = genrss +omit = + .venv + __meta__.py + +[report] +exclude_lines = + pragma: no cover + def __repr__ + if self\.debug: + if settings.DEBUG + raise NotImplementedError + if 0: + if False: + if __name__ == .__main__.: + @abstractmethod + +fail_under = 80 +precision = 2 +show_missing = true +skip_covered = true + +[html] +directory = tests/htmlcov diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9ecf51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,155 @@ + +# Created by https://www.gitignore.io/api/vim,python +# Edit at https://www.gitignore.io/?templates=vim,python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +# End of https://www.gitignore.io/api/vim,python + +# IDE +.idea/ +.c9/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b1ccadb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: python +python: + - '3.6' + - '3.7' +install: + - pip install -r requirements-test.txt + - pip install -e . +script: + - coverage run -m pytest +after_success: + - coveralls \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..5829a13 --- /dev/null +++ b/Pipfile @@ -0,0 +1,16 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +pytest = "*" +coverage = "*" +coveralls = "*" + +[packages] +lxml = "*" +pytz = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..3c9b0ea --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,377 @@ +{ + "_meta": { + "hash": { + "sha256": "f0911fe39e4694db48dedc159ec16cbf28529b922b8b1ec5fdbdfd67d70cdcf3" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "lxml": { + "hashes": [ + "sha256:06c7616601430aa140a69f97e3116308fffe0848f543b639a5ec2e8920ae72fd", + "sha256:177202792f9842374a8077735c69c41a4282183f7851443d2beb8ee310720819", + "sha256:19317ad721ceb9e39847d11131903931e2794e447d4751ebb0d9236f1b349ff2", + "sha256:36d206e62f3e5dbaafd4ec692b67157e271f5da7fd925fda8515da675eace50d", + "sha256:387115b066c797c85f9861a9613abf50046a15aac16759bc92d04f94acfad082", + "sha256:3ce1c49d4b4a7bc75fb12acb3a6247bb7a91fe420542e6d671ba9187d12a12c2", + "sha256:4d2a5a7d6b0dbb8c37dab66a8ce09a8761409c044017721c21718659fa3365a1", + "sha256:58d0a1b33364d1253a88d18df6c0b2676a1746d27c969dc9e32d143a3701dda5", + "sha256:62a651c618b846b88fdcae0533ec23f185bb322d6c1845733f3123e8980c1d1b", + "sha256:69ff21064e7debc9b1b1e2eee8c2d686d042d4257186d70b338206a80c5bc5ea", + "sha256:7060453eba9ba59d821625c6af6a266bd68277dce6577f754d1eb9116c094266", + "sha256:7d26b36a9c4bce53b9cfe42e67849ae3c5c23558bc08363e53ffd6d94f4ff4d2", + "sha256:83b427ad2bfa0b9705e02a83d8d607d2c2f01889eb138168e462a3a052c42368", + "sha256:923d03c84534078386cf50193057aae98fa94cace8ea7580b74754493fda73ad", + "sha256:b773715609649a1a180025213f67ffdeb5a4878c784293ada300ee95a1f3257b", + "sha256:baff149c174e9108d4a2fee192c496711be85534eab63adb122f93e70aa35431", + "sha256:bca9d118b1014b4c2d19319b10a3ebed508ff649396ce1855e1c96528d9b2fa9", + "sha256:ce580c28845581535dc6000fc7c35fdadf8bea7ccb57d6321b044508e9ba0685", + "sha256:d34923a569e70224d88e6682490e24c842907ba2c948c5fd26185413cbe0cd96", + "sha256:dd9f0e531a049d8b35ec5e6c68a37f1ba6ec3a591415e6804cbdf652793d15d7", + "sha256:ecb805cbfe9102f3fd3d2ef16dfe5ae9e2d7a7dfbba92f4ff1e16ac9784dbfb0", + "sha256:ede9aad2197a0202caff35d417b671f5f91a3631477441076082a17c94edd846", + "sha256:ef2d1fc370400e0aa755aab0b20cf4f1d0e934e7fd5244f3dd4869078e4942b9", + "sha256:f2fec194a49bfaef42a548ee657362af5c7a640da757f6f452a35da7dd9f923c" + ], + "index": "pypi", + "version": "==4.3.4" + }, + "pytz": { + "hashes": [ + "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", + "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" + ], + "index": "pypi", + "version": "==2019.1" + } + }, + "develop": { + "asn1crypto": { + "hashes": [ + "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", + "sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49" + ], + "version": "==0.24.0" + }, + "atomicwrites": { + "hashes": [ + "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", + "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + ], + "version": "==1.3.0" + }, + "attrs": { + "hashes": [ + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + ], + "version": "==19.1.0" + }, + "certifi": { + "hashes": [ + "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", + "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" + ], + "version": "==2019.6.16" + }, + "cffi": { + "hashes": [ + "sha256:041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774", + "sha256:046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d", + "sha256:066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90", + "sha256:066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b", + "sha256:2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63", + "sha256:300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45", + "sha256:34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25", + "sha256:46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3", + "sha256:4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b", + "sha256:4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647", + "sha256:4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016", + "sha256:50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4", + "sha256:55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb", + "sha256:5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753", + "sha256:59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7", + "sha256:73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9", + "sha256:a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f", + "sha256:a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8", + "sha256:a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f", + "sha256:a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc", + "sha256:ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42", + "sha256:b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3", + "sha256:d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909", + "sha256:d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45", + "sha256:dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d", + "sha256:e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512", + "sha256:e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff", + "sha256:ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201" + ], + "version": "==1.12.3" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "configparser": { + "hashes": [ + "sha256:8be81d89d6e7b4c0d4e44bcc525845f6da25821de80cb5e06e7e0238a2899e32", + "sha256:da60d0014fd8c55eb48c1c5354352e363e2d30bbf7057e5e171a468390184c75" + ], + "markers": "python_version < '3'", + "version": "==3.7.4" + }, + "contextlib2": { + "hashes": [ + "sha256:509f9419ee91cdd00ba34443217d5ca51f5a364a404e1dce9e8979cea969ca48", + "sha256:f5260a6e679d2ff42ec91ec5252f4eeffdcf21053db9113bd0a8e4d953769c00" + ], + "markers": "python_version < '3'", + "version": "==0.5.5" + }, + "coverage": { + "hashes": [ + "sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", + "sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", + "sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", + "sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", + "sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", + "sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", + "sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", + "sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", + "sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", + "sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", + "sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", + "sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", + "sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", + "sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", + "sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", + "sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", + "sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", + "sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", + "sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", + "sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", + "sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", + "sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", + "sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", + "sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", + "sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", + "sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", + "sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", + "sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", + "sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", + "sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", + "sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a" + ], + "index": "pypi", + "version": "==4.5.3" + }, + "coveralls": { + "hashes": [ + "sha256:d3d49234bffd41e91b241a69f0ebb9f64d7f0515711a76134d53d4647e7eb509", + "sha256:dafabcff87425fa2ab3122dee21229afbb4d6692cfdacc6bb895f7dfa8b2c849" + ], + "index": "pypi", + "version": "==1.8.1" + }, + "cryptography": { + "hashes": [ + "sha256:24b61e5fcb506424d3ec4e18bca995833839bf13c59fc43e530e488f28d46b8c", + "sha256:25dd1581a183e9e7a806fe0543f485103232f940fcfc301db65e630512cce643", + "sha256:3452bba7c21c69f2df772762be0066c7ed5dc65df494a1d53a58b683a83e1216", + "sha256:41a0be220dd1ed9e998f5891948306eb8c812b512dc398e5a01846d855050799", + "sha256:5751d8a11b956fbfa314f6553d186b94aa70fdb03d8a4d4f1c82dcacf0cbe28a", + "sha256:5f61c7d749048fa6e3322258b4263463bfccefecb0dd731b6561cb617a1d9bb9", + "sha256:72e24c521fa2106f19623a3851e9f89ddfdeb9ac63871c7643790f872a305dfc", + "sha256:7b97ae6ef5cba2e3bb14256625423413d5ce8d1abb91d4f29b6d1a081da765f8", + "sha256:961e886d8a3590fd2c723cf07be14e2a91cf53c25f02435c04d39e90780e3b53", + "sha256:96d8473848e984184b6728e2c9d391482008646276c3ff084a1bd89e15ff53a1", + "sha256:ae536da50c7ad1e002c3eee101871d93abdc90d9c5f651818450a0d3af718609", + "sha256:b0db0cecf396033abb4a93c95d1602f268b3a68bb0a9cc06a7cff587bb9a7292", + "sha256:cfee9164954c186b191b91d4193989ca994703b2fff406f71cf454a2d3c7327e", + "sha256:e6347742ac8f35ded4a46ff835c60e68c22a536a8ae5c4422966d06946b6d4c6", + "sha256:f27d93f0139a3c056172ebb5d4f9056e770fdf0206c2f422ff2ebbad142e09ed", + "sha256:f57b76e46a58b63d1c6375017f4564a28f19a5ca912691fd2e4261b3414b618d" + ], + "version": "==2.7" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "enum34": { + "hashes": [ + "sha256:2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850", + "sha256:644837f692e5f550741432dd3f223bbb9852018674981b1664e5dc339387588a", + "sha256:6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79", + "sha256:8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1" + ], + "markers": "python_version < '3'", + "version": "==1.1.6" + }, + "funcsigs": { + "hashes": [ + "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca", + "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50" + ], + "markers": "python_version < '3.0'", + "version": "==1.0.2" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "importlib-metadata": { + "hashes": [ + "sha256:6dfd58dfe281e8d240937776065dd3624ad5469c835248219bd16cf2e12dbeb7", + "sha256:cb6ee23b46173539939964df59d3d72c3e0c1b5d54b84f1d8a7e912fe43612db" + ], + "version": "==0.18" + }, + "ipaddress": { + "hashes": [ + "sha256:64b28eec5e78e7510698f6d4da08800a5c575caa4a286c93d651c5d3ff7b6794", + "sha256:b146c751ea45cad6188dd6cf2d9b757f6f4f8d6ffb96a023e6f2e26eea02a72c" + ], + "version": "==1.0.22" + }, + "more-itertools": { + "hashes": [ + "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4", + "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc", + "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9" + ], + "markers": "python_version <= '2.7'", + "version": "==5.0.0" + }, + "packaging": { + "hashes": [ + "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", + "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" + ], + "version": "==19.0" + }, + "pathlib2": { + "hashes": [ + "sha256:2156525d6576d21c4dcaddfa427fae887ef89a7a9de5cbfe0728b3aafa78427e", + "sha256:446014523bb9be5c28128c4d2a10ad6bb60769e78bd85658fe44a450674e0ef8" + ], + "markers": "python_version < '3.6'", + "version": "==2.3.4" + }, + "pluggy": { + "hashes": [ + "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", + "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c" + ], + "version": "==0.12.0" + }, + "py": { + "hashes": [ + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + ], + "version": "==1.8.0" + }, + "pycparser": { + "hashes": [ + "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" + ], + "version": "==2.19" + }, + "pyopenssl": { + "hashes": [ + "sha256:aeca66338f6de19d1aa46ed634c3b9ae519a64b458f8468aec688e7e3c20f200", + "sha256:c727930ad54b10fc157015014b666f2d8b41f70c0d03e83ab67624fd3dd5d1e6" + ], + "version": "==19.0.0" + }, + "pyparsing": { + "hashes": [ + "sha256:530d8bf8cc93a34019d08142593cf4d78a05c890da8cf87ffa3120af53772238", + "sha256:f78e99616b6f1a4745c0580e170251ef1bbafc0d0513e270c4bd281bf29d2800" + ], + "version": "==2.4.1" + }, + "pytest": { + "hashes": [ + "sha256:6aa9bc2f6f6504d7949e9df2a756739ca06e58ffda19b5e53c725f7b03fb4aae", + "sha256:b77ae6f2d1a760760902a7676887b665c086f71e3461c64ed2a312afcedc00d6" + ], + "index": "pypi", + "version": "==4.6.4" + }, + "requests": { + "hashes": [ + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + ], + "version": "==2.22.0" + }, + "scandir": { + "hashes": [ + "sha256:2586c94e907d99617887daed6c1d102b5ca28f1085f90446554abf1faf73123e", + "sha256:2ae41f43797ca0c11591c0c35f2f5875fa99f8797cb1a1fd440497ec0ae4b022", + "sha256:2b8e3888b11abb2217a32af0766bc06b65cc4a928d8727828ee68af5a967fa6f", + "sha256:2c712840c2e2ee8dfaf36034080108d30060d759c7b73a01a52251cc8989f11f", + "sha256:4d4631f6062e658e9007ab3149a9b914f3548cb38bfb021c64f39a025ce578ae", + "sha256:67f15b6f83e6507fdc6fca22fedf6ef8b334b399ca27c6b568cbfaa82a364173", + "sha256:7d2d7a06a252764061a020407b997dd036f7bd6a175a5ba2b345f0a357f0b3f4", + "sha256:8c5922863e44ffc00c5c693190648daa6d15e7c1207ed02d6f46a8dcc2869d32", + "sha256:92c85ac42f41ffdc35b6da57ed991575bdbe69db895507af88b9f499b701c188", + "sha256:b24086f2375c4a094a6b51e78b4cf7ca16c721dcee2eddd7aa6494b42d6d519d", + "sha256:cb925555f43060a1745d0a321cca94bcea927c50114b623d73179189a4e100ac" + ], + "markers": "python_version < '3.5'", + "version": "==1.10.0" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "urllib3": { + "extras": [ + "secure" + ], + "hashes": [ + "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", + "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" + ], + "markers": "python_version < '3'", + "version": "==1.24.3" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + ], + "version": "==0.1.7" + }, + "zipp": { + "hashes": [ + "sha256:4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", + "sha256:8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec" + ], + "version": "==0.5.2" + } + } +} diff --git a/README.md b/README.md index e2cb61c..1ada341 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # genrss + +[![Build Status](https://travis-ci.org/icetemple/genrss.svg?branch=master)](https://travis-ci.org/icetemple/genrss) +[![Coverage Status](https://coveralls.io/repos/github/icetemple/genrss/badge.svg?branch=master)](https://coveralls.io/github/icetemple/genrss?branch=master) + RSS generator for python diff --git a/genrss/__init__.py b/genrss/__init__.py new file mode 100644 index 0000000..fdbb526 --- /dev/null +++ b/genrss/__init__.py @@ -0,0 +1,139 @@ +import mimetypes +from lxml.etree import Element, CDATA, tostring +from typing import Optional, List, NoReturn +from datetime import datetime +from collections import namedtuple + +import pytz + +Enclosure = namedtuple('Enclosure', ('url', 'size', 'type')) +Enclosure.__new__.__defaults__ = (None, None, None) + +RSS_DEFAULT_GENERATOR = f'Generated by genrss for python' + + +def create_element(name: str, text=None, children=None, **kwargs) -> Element: + el = Element(name, **kwargs) + if text: + if isinstance(text, datetime): + text = text.replace(tzinfo=pytz.timezone('GMT')). \ + strftime("%a, %d %b %Y %H:%M:%S %Z") + el.text = text + elif isinstance(children, (list, tuple)): + for child in children: + el.append(child) + return el + + +class GenRSS: + def __init__(self, title: str, site_url: str, feed_url: str, **kwargs): + self.title: str = title + self.site_url: str = site_url + self.feed_url: str = feed_url + self.description: str = kwargs.pop('description', self.title) + self.image_url: Optional[str] = kwargs.pop('image_url', None) + self.author: Optional[str] = kwargs.pop('author', None) + self.pub_date: Optional[datetime] = kwargs.pop('pub_date', None) + self.copyright: Optional[str] = kwargs.pop('copyright', None) + self.language: Optional[str] = kwargs.pop('language', None) + self.editor: Optional[str] = kwargs.pop('editor', None) + self.webmaster: Optional[str] = kwargs.pop('webmaster', None) + self.docs_url: Optional[str] = kwargs.pop('docs_url', None) + self.categories: List[str] = kwargs.pop('categories', []) + + self.items: List[Element] = [] + self.generator = kwargs.pop('generator', RSS_DEFAULT_GENERATOR) + self.root_version = '2.0' + self.root_nsmap = { + 'atom': 'http://www.w3.org/2005/Atom' + } + + def item(self, title: str, **kwargs) -> NoReturn: + description: str = kwargs.pop('description', '') + url: Optional[str] = kwargs.pop('url', None) + guid: Optional[str] = kwargs.pop('guid', None) + author: Optional[str] = kwargs.pop('author', None) + categories: List[str] = kwargs.pop('categories', []) + enclosure: Optional[Enclosure] = kwargs.pop('enclosure', None) + pub_date: Optional[datetime] = kwargs.pop('pub_date', None) + + item = create_element('item', children=[ + create_element('title', CDATA(title)), + create_element('description', CDATA(description)), + ]) + + if url: + item.append(create_element('link', url)) + + item.append(create_element( + 'guid', + attrib={'isPermaLink': str(bool(not guid and url)).lower()}, + text=(guid or url or CDATA(title)) + )) + + if author or self.author: + if 'dc' not in self.root_nsmap: + self.root_nsmap['dc'] = 'http://purl.org/dc/elements/1.1/' + + item.append(create_element( + '{http://purl.org/dc/elements/1.1/}creator', + CDATA(author or self.author) + )) + + for category in categories: + item.append(create_element('category', CDATA(category))) + + if enclosure: + item.append(create_element( + 'enclosure', + url=enclosure.url, + length=str(enclosure.size or 0), + type=enclosure.type or mimetypes.guess_type(enclosure.url)[0] + )) + + if pub_date: + item.append(create_element('pubDate', pub_date)) + + self.items.append(item) + + def xml(self, pretty: bool = False) -> str: + root = Element('rss', nsmap=self.root_nsmap, version=self.root_version) + channel = create_element('channel', children=[ + create_element('title', CDATA(self.title)), + create_element('description', CDATA(self.description)), + create_element('link', self.site_url), + create_element('{http://www.w3.org/2005/Atom}link', + href=self.feed_url, rel='self', + type='application/rss+xml'), + create_element('generator', self.generator), + create_element('lastBuildDate', datetime.utcnow()) + ]) + + if self.image_url: + channel.append(create_element('image', children=[ + create_element('url', self.image_url), + create_element('title', CDATA(self.title)), + create_element('link', self.site_url) + ])) + for category in self.categories: + channel.append(create_element('category', CDATA(category))) + if self.pub_date: + channel.append(create_element('pubDate', self.pub_date)) + if self.copyright: + channel.append(create_element('copyright', CDATA(self.copyright))) + if self.language: + channel.append(create_element('language', CDATA(self.language))) + if self.editor: + channel.append(create_element('managingEditor', CDATA(self.editor))) + if self.webmaster: + channel.append(create_element('webMaster', CDATA(self.webmaster))) + if self.docs_url: + channel.append(create_element('docs', self.docs_url)) + + for item in self.items: + channel.append(item) + + root.append(channel) + + return '\n' \ + + tostring(root, pretty_print=pretty).decode('utf-8') diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..dc30c7d --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,3 @@ +coverage==4.5.3 +coveralls==1.8.1 +pytest==4.6.4 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..be27639 --- /dev/null +++ b/setup.py @@ -0,0 +1,25 @@ +from setuptools import setup + + +with open('README.md', 'r') as f: + readme = f.read() + + +if __name__ == '__main__': + setup( + name='genrss', + version='1.0.0', + author='Dmitriy Pleshevskiy', + author_email='dmitriy@ideascup.me', + description='RSS feed generator for python', + long_description=readme, + long_description_content_type='text/markdown', + package_data={'': ['LICENSE', 'README.md']}, + include_package_data=True, + license='MIT', + packages=['genrss'], + install_requires=[ + 'lxml==4.3.4', + 'pytz==2019.1' + ] + ) diff --git a/tests/support.py b/tests/support.py new file mode 100644 index 0000000..3d2c530 --- /dev/null +++ b/tests/support.py @@ -0,0 +1,10 @@ +from genrss import GenRSS + + +def create_rss(**kwargs): + return GenRSS(title='SmartFridge', site_url='https://smartfridge.me/', + feed_url='https://smartfridge.me/rss.xml', **kwargs) + + +def create_item(feed, **kwargs): + feed.item(title='Recipe', **kwargs) diff --git a/tests/test_rss.py b/tests/test_rss.py new file mode 100644 index 0000000..c2faff4 --- /dev/null +++ b/tests/test_rss.py @@ -0,0 +1,111 @@ +from datetime import datetime +from textwrap import dedent + +import pytz +import pytest +from genrss import RSS_DEFAULT_GENERATOR +from tests.support import create_rss + + +def test_init_rss(): + feed = create_rss() + xml = feed.xml() + assert xml + assert '<![CDATA[SmartFridge]]>' in xml + assert '' in xml + assert 'https://smartfridge.me/' in xml + assert '' in xml + assert '{}'.format(RSS_DEFAULT_GENERATOR) in xml + + +@pytest.mark.parametrize('description, expose', [ + pytest.param('a' * 10, 'a' * 10, id='short(10)'), + pytest.param('a' * 285, 'a' * 285, id='long(285)'), + pytest.param( + dedent('''\ + This is text with + new lines.'''), + dedent('''\ + This is text with + new lines.'''), + id='+nl' + ) +]) +def test_feed_description(description, expose): + feed = create_rss(description=description) + xml = feed.xml() + assert xml + assert ''.format(expose) in xml + + +@pytest.mark.parametrize('copyright, expose', [ + pytest.param('copyright © genrss', 'copyright © genrss', id='copy'), +]) +def test_feed_copyright(copyright, expose): + feed = create_rss(copyright=copyright) + xml = feed.xml() + assert xml + assert ''.format(expose) in xml + + +def test_feed_pub_date(): + pub_date = datetime.utcnow() + feed = create_rss(pub_date=pub_date) + xml = feed.xml() + expose = pub_date.replace(tzinfo=pytz.timezone('GMT')). \ + strftime("%a, %d %b %Y %H:%M:%S %Z") + assert xml + assert '{}'.format(expose) in xml + + +def test_feed_language(): + lang = 'en' + feed = create_rss(language=lang) + xml = feed.xml() + assert xml + assert ''.format(lang) in xml + + +def test_feed_editor(): + editor = 'Dmitriy Pleshevskiy' + feed = create_rss(editor=editor) + xml = feed.xml() + assert xml + assert ('' + '').format(editor) in xml + + +def test_feed_image_url(): + image_url = 'https://s3.smartfridge.me/image.jpg' + feed = create_rss(image_url=image_url) + xml = feed.xml() + assert xml + assert (f'{image_url}' + '<![CDATA[SmartFridge]]>' + 'https://smartfridge.me/') in xml + + +def test_feed_webmaster(): + webmaster = 'Dmitriy Pleshevskiy' + feed = create_rss(webmaster=webmaster) + xml = feed.xml() + assert xml + assert ''.format(webmaster) in xml + + +def test_feed_docs_url(): + docs_url = 'https://smartfridge.me/docs' + feed = create_rss(docs_url=docs_url) + xml = feed.xml() + assert xml + assert '{}'.format(docs_url) in xml + + +def test_feed_categories(): + categories = ['Category 1', 'Category 2'] + feed = create_rss(categories=categories) + xml = feed.xml() + assert xml + assert '' \ + '' in xml diff --git a/tests/test_rss_item.py b/tests/test_rss_item.py new file mode 100644 index 0000000..7610312 --- /dev/null +++ b/tests/test_rss_item.py @@ -0,0 +1,96 @@ +from uuid import uuid4 +from datetime import datetime + +import pytz +import pytest +from genrss import Enclosure +from tests.support import create_rss, create_item + + +@pytest.fixture() +def feed(): + return create_rss() + + +def test_item(feed): + create_item(feed) + xml = feed.xml() + assert xml + assert '<![CDATA[Recipe]]>' \ + '' \ + '' \ + '' in xml + + +def test_item_description(feed): + description = 'description' + create_item(feed, description=description) + xml = feed.xml() + assert xml + assert '<![CDATA[Recipe]]>' \ + '' \ + '' \ + ''.format(description) in xml + + +def test_item_guid(feed): + guid = uuid4().hex + create_item(feed, guid=guid) + xml = feed.xml() + assert xml + assert '<![CDATA[Recipe]]>' \ + '' \ + '{}' \ + ''.format(guid) in xml + + +def test_item_url(feed): + url = 'https://smartfridge.me/' + create_item(feed, url=url) + xml = feed.xml() + assert xml + assert '<![CDATA[Recipe]]>' \ + '' \ + '{url}' \ + '{url}' \ + ''.format(url=url) in xml + + +def test_item_author(feed): + author = 'Dmitriy Pleshevskiy' + create_item(feed, author=author) + create_item(feed, author=author) + xml = feed.xml() + assert xml + assert '' \ + ''.format(author) in xml + + +def test_item_categories(feed): + categories = ['Category 1', 'Category 2'] + create_item(feed, categories=categories) + xml = feed.xml() + assert xml + assert '' \ + '' \ + '' in xml + + +def test_item_pub_date(feed): + pub_date = datetime.utcnow() + expose = pub_date.replace(tzinfo=pytz.timezone('GMT')). \ + strftime("%a, %d %b %Y %H:%M:%S %Z") + create_item(feed, pub_date=pub_date) + xml = feed.xml() + assert xml + assert '{}' \ + ''.format(expose) in xml + + +def test_item_enclosure(feed): + enclosure=Enclosure('https://smartfridge.me/image.jpg') + create_item(feed, enclosure=enclosure) + xml = feed.xml() + assert xml + assert '' \ + ''.format(0, 'image/jpeg', enclosure.url) in xml \ No newline at end of file