├── .coveragerc ├── .dockerignore ├── .gitignore ├── .readthedocs.yml ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── HACKING.md ├── INSTALL ├── LICENSE.txt ├── Makefile ├── README.rst ├── ROADMAP.md ├── appveyor.yml ├── config.mk ├── contrib └── papis.desktop ├── doc ├── Makefile ├── source │ ├── api.rst │ ├── commands.rst │ ├── commands │ │ ├── add.rst │ │ ├── addto.rst │ │ ├── bibtex.rst │ │ ├── browse.rst │ │ ├── commands.rst │ │ ├── config.rst │ │ ├── default.rst │ │ ├── edit.rst │ │ ├── explore.rst │ │ ├── export.rst │ │ ├── git.rst │ │ ├── list.rst │ │ ├── mv.rst │ │ ├── open.rst │ │ ├── rename.rst │ │ ├── rm.rst │ │ ├── run.rst │ │ └── update.rst │ ├── conf.py │ ├── configuration.rst │ ├── database_structure.rst │ ├── default-settings.rst │ ├── faq.rst │ ├── git.rst │ ├── gui.rst │ ├── importing.rst │ ├── index.rst │ ├── info_file.rst │ ├── install.rst │ ├── library_structure.rst │ ├── plugins.rst │ ├── quick_start.rst │ ├── scihub.rst │ ├── scripting.rst │ └── shell_completion.rst └── sphinx.mk ├── examples └── scripts │ ├── LTWA.json │ ├── papis-abbrev │ ├── papis-check │ ├── papis-ga │ ├── papis-mail │ └── papis-tags ├── papis ├── __init__.py ├── api.py ├── arxiv.py ├── base.py ├── bibtex.py ├── cli.py ├── commands │ ├── __init__.py │ ├── add.py │ ├── addto.py │ ├── bibtex.py │ ├── browse.py │ ├── config.py │ ├── default.py │ ├── edit.py │ ├── explore.py │ ├── export.py │ ├── external.py │ ├── git.py │ ├── list.py │ ├── mv.py │ ├── open.py │ ├── rename.py │ ├── rm.py │ ├── run.py │ └── update.py ├── config.py ├── crossref.py ├── database │ ├── __init__.py │ ├── base.py │ ├── cache.py │ └── whoosh.py ├── dissemin.py ├── docmatcher.py ├── document.py ├── downloaders │ ├── __init__.py │ ├── acs.py │ ├── annualreviews.py │ ├── aps.py │ ├── base.py │ ├── fallback.py │ ├── frontiersin.py │ ├── get.py │ ├── hal.py │ ├── ieee.py │ ├── iopscience.py │ ├── sciencedirect.py │ ├── scitationaip.py │ ├── springer.py │ ├── tandfonline.py │ ├── thesesfr.py │ └── worldscientific.py ├── exceptions.py ├── git.py ├── importer.py ├── isbn.py ├── isbnplus.py ├── json.py ├── library.py ├── pick.py ├── plugin.py ├── pubmed.py ├── strings.py ├── tui │ ├── __init__.py │ ├── app.py │ └── widgets │ │ ├── __init__.py │ │ ├── command_line_prompt.py │ │ ├── diff.py │ │ └── list.py ├── utils.py └── yaml.py ├── scripts └── shell_completion │ ├── Makefile │ ├── build │ └── bash │ │ └── papis │ ├── click │ ├── papis.sh │ └── zsh │ │ └── _papis │ └── tools │ ├── bash.sh │ └── lib.sh ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── cli.py ├── commands │ ├── data │ │ ├── ken.yaml │ │ └── lib1.bib │ ├── test_add.py │ ├── test_addto.py │ ├── test_browse.py │ ├── test_check.py │ ├── test_config.py │ ├── test_default.py │ ├── test_edit.py │ ├── test_explore.py │ ├── test_export.py │ ├── test_git.py │ ├── test_list.py │ ├── test_mv.py │ ├── test_open.py │ ├── test_rename.py │ ├── test_rm.py │ ├── test_run.py │ └── test_update.py ├── data │ └── licl.yaml ├── database │ ├── __init__.py │ ├── test_base.py │ ├── test_papis.py │ └── test_whoosh.py ├── downloaders │ ├── __init__.py │ ├── resources │ │ ├── acs_1.html │ │ ├── acs_1_out.json │ │ ├── acs_2.html │ │ ├── acs_2_out.json │ │ ├── annualreviews_1.html │ │ ├── annualreviews_1_out.json │ │ ├── fallback_1_out.json │ │ ├── fallback_2.html │ │ ├── fallback_2_out.json │ │ ├── hal_1.html │ │ ├── hal_1_out.json │ │ ├── iopscience_1.html │ │ ├── iopscience_1_out.json │ │ ├── iopscience_2.html │ │ ├── iopscience_2_out.json │ │ ├── prl_1.html │ │ ├── prl_1_out.json │ │ ├── sciencedirect_1.html │ │ ├── sciencedirect_1_authors.json │ │ ├── sciencedirect_1_authors_out.json │ │ ├── sciencedirect_1_out.json │ │ ├── sciencedirect_2.html │ │ ├── sciencedirect_2_out.json │ │ ├── springer_1.html │ │ ├── springer_1_out.json │ │ ├── springer_2.html │ │ ├── springer_2_out.json │ │ ├── tandfonline_1.html │ │ ├── tandfonline_1_out.json │ │ ├── tandfonline_2.html │ │ ├── tandfonline_2_out.json │ │ └── wiley_1.html │ ├── test_acs.py │ ├── test_annualreviews.py │ ├── test_aps.py │ ├── test_arxiv.py │ ├── test_fallback.py │ ├── test_hal.py │ ├── test_iopscience.py │ ├── test_sciencedirect.py │ ├── test_springer.py │ ├── test_tandfonline.py │ └── test_utils.py ├── resources │ ├── bibtex │ │ ├── 1.bib │ │ ├── 1_out.json │ │ ├── 2.bib │ │ ├── 2_out.json │ │ ├── 3.bib │ │ └── 3_out.json │ ├── commands │ │ └── update │ │ │ ├── positron.json │ │ │ ├── russell.yaml │ │ │ └── wannier.bib │ ├── config_1.ini │ ├── crossref │ │ ├── test1.json │ │ ├── test1_out.json │ │ ├── test_2.json │ │ ├── test_2_out.json │ │ ├── test_conference.json │ │ └── test_conference_out.json │ ├── document │ │ └── info.yaml │ ├── example_document.txt │ └── minimal.ini ├── test_arxiv.py ├── test_bibtex.py ├── test_config.py ├── test_crossref.py ├── test_deps.py ├── test_docmatcher.py ├── test_document.py ├── test_importer.py ├── test_isbn.py ├── test_utils.py └── tui │ ├── test_app.py │ └── widgets │ ├── test_command_line_prompt.py │ ├── test_general_widgets.py │ └── test_options_list.py └── tools ├── changelog.sh ├── create-docker-image.py └── update-pypi.sh /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | papis/deps/* 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | papis/deps/* 2 | .vim-run 3 | 4 | doc/build/ 5 | #Vim swap files 6 | *.swp 7 | *.swo 8 | 9 | # Byte-compiled / optimized / DLL files 10 | tags 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | env/ 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *,cover 55 | .hypothesis/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # IPython Notebook 79 | .ipynb_checkpoints 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # dotenv 88 | .env 89 | 90 | # virtualenv 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | changes 100 | *~ 101 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | papis/deps/* 2 | .vim-run 3 | 4 | doc/build/ 5 | #Vim swap files 6 | *.swp 7 | *.swo 8 | 9 | # Byte-compiled / optimized / DLL files 10 | tags 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | env/ 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *,cover 55 | .hypothesis/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # IPython Notebook 79 | .ipynb_checkpoints 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # dotenv 88 | .env 89 | 90 | # virtualenv 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | changes 100 | *~ 101 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | python: 2 | version: 3.5 3 | pip_install: true 4 | extra_requirements: 5 | - develop 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | os: 4 | - linux 5 | python: 6 | - '3.4' 7 | - '3.5' 8 | - '3.6' 9 | - '3.7' 10 | #- '3.3' 11 | script: 12 | - python -m pytest papis/ tests/ --cov=papis 13 | install: 14 | - pip install setuptools 15 | - pip install python-coveralls 16 | - pip install -e .[develop] 17 | - pip install -e .[optional] 18 | after_success: 19 | - coveralls 20 | - python setup.py sdist --formats zip,gztar 21 | 22 | #deploy: 23 | #provider: releases 24 | #skip_cleanup: true 25 | #api_key: 26 | #secure: YSWVW2PhxUwoA919l8nC3Z9sEdfV0PmrvzNiTGMBa/qhpmv3CeB2RMPzSreqG7/D6Rz5ybnXvNz0y4BuBkRPsT217X84OA5e43SqKquVeatSRFGdEjJcPrsYNMBnNG3G0Mjl7OtpM6YhzhI7fq9/RE8cASsHsEJFAvKz0ZIXv5aMW78VGewcbYqEUxaM3XFIo/eTljDLWLX0fbLjR6VC3T0fEgLMsPQoPlPrhUYoE/Yin5CjSbmW3q6/oThuLaJGFrYNTDl9GUaBEEWoUY4b+cy88qALHs1uAHqQqgFhGDnWNyZnE4/ZL7DeG7Czm8gr7/8GwBbLt68xIl8W0DeT7hLkmM7o/Bq9Y/Ow7oCtMiVkrdsr2WBqeLa2u6m7YI+QoNZyz4ct40gc1MWIGq6blUd8vw0pjNYIpTJyeCgXUM30s08gAK2s8YeJTqVnV56ITLCKQWcEY/zRfc/dOPVgX9kZ1/x7+97sVPccx3MnxRzEBCDARrq+o0bg90voErRKtQSgzOAC3KRUpyw5IGNiDYZSPshvQH/WQl+jB0//pIB9JwXq5rhnm+R3dTct/v0JE8sv4bzxpnXACAsYdtDRJfwoSLMK8mVXmf4pMuR/Ve/o87X4QWeBeFpgFuqPwyWaGSZQU4nEPL57FLPEwuj2qNe8KmMGfvZN1eO5Lr7j/VE= 27 | #file: 28 | #- dist/* 29 | #- CHANGELOG.md 30 | #on: 31 | #repo: papis/papis 32 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | 1396 Alejandro Gallo 2 | 38 Nicolò Balzarotti 3 | 16 michaelplews 4 | 10 Michael R. Plews 5 | 9 alejandrogallo 6 | 4 Alexander Von Moll 7 | 2 Katrin Leinweber <9948149+katrinleinweber@users.noreply.github.com> 8 | 2 Michael Plews 9 | 1 Andrew 10 | 1 Dennis Bruggner 11 | 1 Felix Hummel 12 | 1 Theo Tsatsoulis 13 | 1 Theodoros Tsatsoulis 14 | 1 Theodoros Tsatsoulis 15 | 1 mftrhu 16 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at a.gallo@fkf.mpg.de. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Papis 2 | 3 | ## Tips for bug reports 4 | 5 | * You can obtain much better error messages with `papis -v --debug`, please 6 | post those in bug reports too. 7 | 8 | ## Tips on patching or sending pull requests 9 | 10 | * See `HACKING.md` 11 | 12 | Thank you! 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.7 2 | FROM python:$PYTHON_VERSION 3 | 4 | RUN apt-get update 5 | RUN apt-get install -y vim-nox 6 | 7 | WORKDIR /papis 8 | VOLUME /papis 9 | 10 | COPY . /papis 11 | 12 | RUN python setup.py develop 13 | RUN pip install .[develop] 14 | RUN pip install .[optional] 15 | 16 | CMD ["pytest", "tests", "papis", "--cov", "papis"] 17 | -------------------------------------------------------------------------------- /HACKING.md: -------------------------------------------------------------------------------- 1 | Guidelines for Code Modification 2 | ================================ 3 | 4 | Coding Style 5 | ------------ 6 | 7 | * Use syntax compatible with Python `3.1+`. 8 | * Use docstrings with `sphinx` in mind 9 | * 4 spaces are the preferred indentation method. 10 | * No trailing white spaces are allowed. 11 | * Restrict each line of code to 80 characters. 12 | * Follow the PEP8 style guide: https://www.python.org/dev/peps/pep-0008/ 13 | * Always run `make test` before submitting a new PR. You need to run 14 | `pip3 install -e .[develop]` in the papis directory before running the 15 | tests. 16 | 17 | 18 | Patches 19 | ------- 20 | 21 | Send patches, created with `git format-patch`, to the email address 22 | 23 | gallo@fkf.mpg.de 24 | 25 | or open a pull request on GitHub. 26 | 27 | 28 | Version Numbering 29 | ----------------- 30 | 31 | Three numbers, `A.B.C`, where 32 | * `A` changes on a rewrite 33 | * `B` changes when major configuration incompatibilities occur 34 | * `C` changes with each release (bug fixes..) 35 | 36 | 37 | 38 | Common Changes 39 | ============== 40 | 41 | Adding options 42 | -------------- 43 | 44 | * Add a default value in `config.py`, along with a comment that describes the 45 | option. 46 | 47 | The setting is now accessible with `papis.config.get('myoption')` 48 | or through the cli interface `papis config myoption`. 49 | 50 | 51 | Adding scripts 52 | -------------- 53 | 54 | * You can add scripts for everyone to share to the folder 55 | `examples/scripts/` in the repository. These scripts will not be shipped 56 | with papis, but they are there for other users to use and modify. 57 | 58 | 59 | Adding downloaders 60 | ------------------ 61 | 62 | TODO: explain how to add new downloaders 63 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | PAPIS 2 | 3 | Please read the installation section of the manual 4 | 5 | http://papis.readthedocs.io/en/latest/install.html 6 | 7 | for instructions. 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Papis, a document manager 2 | Copyright © 2017 Alejandro Gallo 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | 17 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | 3 | matrix: 4 | 5 | # For Python versions available on Appveyor, see 6 | # http://www.appveyor.com/docs/installed-software#python 7 | # The list here is complete (excluding Python 2.6, which 8 | # isn't covered by this document) at the time of writing. 9 | 10 | - PYTHON: "C:\\Python33" 11 | - PYTHON: "C:\\Python34" 12 | - PYTHON: "C:\\Python35" 13 | - PYTHON: "C:\\Python36" 14 | - PYTHON: "C:\\Python33-x64" 15 | DISTUTILS_USE_SDK: "1" 16 | - PYTHON: "C:\\Python34-x64" 17 | DISTUTILS_USE_SDK: "1" 18 | - PYTHON: "C:\\Python35-x64" 19 | - PYTHON: "C:\\Python36-x64" 20 | 21 | install: 22 | 23 | - "%PYTHON%\\python.exe -m pip install wheel" 24 | - "%PYTHON%\\python.exe -m pip install setuptools" 25 | - "%PYTHON%\\python.exe -m pip install -e .[develop]" 26 | - "%PYTHON%\\python.exe -m pip install -e .[optional]" 27 | - "%PYTHON%\\python.exe -m pip install ." 28 | 29 | build: off 30 | 31 | test_script: 32 | - "%PYTHON%\\python.exe -m pytest papis" 33 | 34 | #on_success: 35 | # You can use this step to upload your artifacts to a public website. 36 | # See Appveyor's documentation for more details. Or you can simply 37 | # access your wheels from the Appveyor "artifacts" tab for your build. 38 | -------------------------------------------------------------------------------- /config.mk: -------------------------------------------------------------------------------- 1 | .PHONY: bash-autocomplete update-authors 2 | 3 | PYTHON = python3 4 | PIP = pip3 5 | TEST_COMMAND = $(PYTHON) -m pytest papis tests --cov=papis 6 | 7 | bash-autocomplete: 8 | make -C scripts/shell_completion/ 9 | 10 | update-authors: 11 | git shortlog -s -e -n | \ 12 | sed -e "s/\t/ /" | \ 13 | sed -e "s/^\s*//" > \ 14 | AUTHORS 15 | 16 | tags: 17 | ctags -V -R --language-force=python papis env 18 | -------------------------------------------------------------------------------- /contrib/papis.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=papis-open 4 | Comment=Launches the papis document opener after picking a library 5 | Icon=utilities-terminal 6 | Terminal=true 7 | Exec=papis --pick-lib open 8 | Categories=ConsoleOnly;Office 9 | MimeType=inode/directory; 10 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | include sphinx.mk 2 | 3 | -------------------------------------------------------------------------------- /doc/source/api.rst: -------------------------------------------------------------------------------- 1 | 2 | API 3 | === 4 | 5 | 6 | .. automodule:: papis.api 7 | :members: 8 | -------------------------------------------------------------------------------- /doc/source/commands.rst: -------------------------------------------------------------------------------- 1 | Commands 2 | ======== 3 | 4 | .. include:: commands/add.rst 5 | .. include:: commands/addto.rst 6 | .. include:: commands/browse.rst 7 | .. include:: commands/bibtex.rst 8 | .. include:: commands/commands.rst 9 | .. include:: commands/config.rst 10 | .. include:: commands/default.rst 11 | .. include:: commands/edit.rst 12 | .. include:: commands/explore.rst 13 | .. include:: commands/export.rst 14 | .. include:: commands/git.rst 15 | .. include:: commands/list.rst 16 | .. include:: commands/mv.rst 17 | .. include:: commands/open.rst 18 | .. include:: commands/rename.rst 19 | .. include:: commands/rm.rst 20 | .. include:: commands/run.rst 21 | .. include:: commands/update.rst 22 | -------------------------------------------------------------------------------- /doc/source/commands/add.rst: -------------------------------------------------------------------------------- 1 | Add 2 | --- 3 | .. automodule:: papis.commands.add 4 | 5 | -------------------------------------------------------------------------------- /doc/source/commands/addto.rst: -------------------------------------------------------------------------------- 1 | Addto 2 | ----- 3 | .. automodule:: papis.commands.addto 4 | 5 | -------------------------------------------------------------------------------- /doc/source/commands/bibtex.rst: -------------------------------------------------------------------------------- 1 | Bibtex 2 | ------ 3 | .. automodule:: papis.commands.bibtex 4 | 5 | -------------------------------------------------------------------------------- /doc/source/commands/browse.rst: -------------------------------------------------------------------------------- 1 | Browse 2 | ------ 3 | .. automodule:: papis.commands.browse 4 | 5 | -------------------------------------------------------------------------------- /doc/source/commands/commands.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /doc/source/commands/config.rst: -------------------------------------------------------------------------------- 1 | Config 2 | ------ 3 | .. automodule:: papis.commands.config 4 | 5 | -------------------------------------------------------------------------------- /doc/source/commands/default.rst: -------------------------------------------------------------------------------- 1 | Main 2 | ---- 3 | .. automodule:: papis.commands.default 4 | 5 | -------------------------------------------------------------------------------- /doc/source/commands/edit.rst: -------------------------------------------------------------------------------- 1 | Edit 2 | ---- 3 | .. automodule:: papis.commands.edit 4 | 5 | -------------------------------------------------------------------------------- /doc/source/commands/explore.rst: -------------------------------------------------------------------------------- 1 | Explore 2 | ------- 3 | .. automodule:: papis.commands.explore 4 | 5 | -------------------------------------------------------------------------------- /doc/source/commands/export.rst: -------------------------------------------------------------------------------- 1 | Export 2 | ------ 3 | .. automodule:: papis.commands.export 4 | 5 | -------------------------------------------------------------------------------- /doc/source/commands/git.rst: -------------------------------------------------------------------------------- 1 | Git 2 | --- 3 | .. automodule:: papis.commands.git 4 | 5 | -------------------------------------------------------------------------------- /doc/source/commands/list.rst: -------------------------------------------------------------------------------- 1 | List 2 | ---- 3 | .. automodule:: papis.commands.list 4 | 5 | -------------------------------------------------------------------------------- /doc/source/commands/mv.rst: -------------------------------------------------------------------------------- 1 | Mv 2 | -- 3 | .. automodule:: papis.commands.mv 4 | 5 | -------------------------------------------------------------------------------- /doc/source/commands/open.rst: -------------------------------------------------------------------------------- 1 | Open 2 | ---- 3 | .. automodule:: papis.commands.open 4 | 5 | -------------------------------------------------------------------------------- /doc/source/commands/rename.rst: -------------------------------------------------------------------------------- 1 | Rename 2 | ------ 3 | .. automodule:: papis.commands.rename 4 | 5 | -------------------------------------------------------------------------------- /doc/source/commands/rm.rst: -------------------------------------------------------------------------------- 1 | Rm 2 | -- 3 | .. automodule:: papis.commands.rm 4 | 5 | -------------------------------------------------------------------------------- /doc/source/commands/run.rst: -------------------------------------------------------------------------------- 1 | Run 2 | --- 3 | .. automodule:: papis.commands.run 4 | 5 | -------------------------------------------------------------------------------- /doc/source/commands/update.rst: -------------------------------------------------------------------------------- 1 | Update 2 | ------ 3 | .. automodule:: papis.commands.update 4 | 5 | -------------------------------------------------------------------------------- /doc/source/faq.rst: -------------------------------------------------------------------------------- 1 | FAQ 2 | === 3 | 4 | Here are some problems that users have come across often: 5 | 6 | - When I remove a folder manually in a library or I synchronize 7 | the library manually, I do not see the new papers in the library. 8 | **Answer**: You probably need to update the cache because papis did not 9 | know anything about your changes in the library since you did it by yourself. 10 | 11 | .. code:: 12 | 13 | papis --clear-cache 14 | 15 | will do. 16 | 17 | 18 | For more information you can also check the 19 | `github faq `_ link. 20 | -------------------------------------------------------------------------------- /doc/source/git.rst: -------------------------------------------------------------------------------- 1 | Git 2 | === 3 | 4 | Papis is conceived to work well with the tool `git`, this would also work with 5 | `mercurial `_ 6 | or `subversion `_. 7 | 8 | Here you will find a description of a possible workflow using git with papis. 9 | This is not the only workflow, but it is the most obvious. 10 | 11 | Let's say you have a library named ``books`` in the directory 12 | ``~/Documents/MyNiceBooks``. You could turn the ``books`` library into 13 | a `git` repository, just doing for example 14 | 15 | :: 16 | 17 | papis -l books run git init 18 | 19 | or just going to the library directory and running the command there: 20 | 21 | :: 22 | 23 | cd ~/Documents/MyNiceBooks 24 | git init 25 | 26 | Now you can add everything you have in the library with ``git add .`` 27 | if you are in the library's directory or 28 | 29 | :: 30 | 31 | papis -l books git add . 32 | 33 | if you want to do it using the `papis`' ``git`` command. 34 | 35 | Interplay with other commands 36 | ----------------------------- 37 | 38 | Some papis commands give you the opportunity of using ``git`` to manage 39 | changes. For instance, if you are adding a new document, you could use 40 | the ``--commit`` flag to also add a commit into your library, so if you do 41 | 42 | :: 43 | 44 | papis add --set author "Pedrito" --set title "Super book" book.pdf --commit 45 | 46 | then also papis will do an automatic commit for the book, so that you can 47 | push your library afterwards to a remote repository. 48 | 49 | You can imagine that papis commands like ``rename`` and ``mv`` should also 50 | offer such functionality, and they indeed do through the ``--git`` flag. 51 | Go to their documentation for more information. 52 | 53 | Updating the libray 54 | ------------------- 55 | 56 | You can use papis' simple ``git`` wrapper, 57 | 58 | :: 59 | 60 | papis git pull 61 | 62 | Usual workflow 63 | -------------- 64 | 65 | Usually the workflow is like this: 66 | 67 | When adding a document that you know for sure you want in your library: 68 | 69 | - Add the document and commit it, either by ``git add --commit`` 70 | or committing the document after adding it to the library. 71 | 72 | - Pull changes from the remote library, maybe you pushed something 73 | at work (reference changes etc..) and you do not have it yet there, 74 | you would do something like 75 | 76 | :: 77 | 78 | papis git pull 79 | 80 | - Push what you just added 81 | 82 | :: 83 | 84 | papis git push 85 | 86 | - Review the status of the library 87 | 88 | :: 89 | 90 | papis git status 91 | 92 | When editing a document's info file: 93 | 94 | - Edit the file and then take a look at the ``diff`` 95 | 96 | :: 97 | 98 | papis git diff 99 | 100 | - Add the changes 101 | 102 | :: 103 | 104 | papis git add --all 105 | 106 | - Commit 107 | 108 | :: 109 | 110 | papis git commit 111 | 112 | - Pull/push: 113 | 114 | :: 115 | 116 | papis git pull 117 | papis git push 118 | 119 | Of course these workflows are just very basic examples. 120 | Your optimal workflow could look completely different. 121 | -------------------------------------------------------------------------------- /doc/source/gui.rst: -------------------------------------------------------------------------------- 1 | Gui 2 | === 3 | 4 | Papis is a program mainly intended for the command line, however, 5 | some experimental frontends have been conceived for it and are 6 | already downloadable from pip. 7 | 8 | See for instance 9 | 10 | - `papis-rofi `_ 11 | - `papis-dmenu `_ 12 | 13 | 14 | And more to come! 15 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. papis documentation master file, created by 2 | sphinx-quickstart on Fri May 12 00:56:29 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to papis' documentation! 7 | ================================= 8 | 9 | Papis is a command-line based document and bibliography manager. Its 10 | command-line interface (*CLI*) is heavily tailored after 11 | `Git `__. 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | quick_start 17 | install 18 | configuration 19 | info_file 20 | library_structure 21 | database_structure 22 | commands 23 | gui 24 | scripting 25 | api 26 | plugins 27 | git 28 | scihub 29 | importing 30 | shell_completion 31 | faq 32 | 33 | 34 | 35 | Indices and tables 36 | ================== 37 | 38 | * :ref:`genindex` 39 | * :ref:`modindex` 40 | * :ref:`search` 41 | 42 | -------------------------------------------------------------------------------- /doc/source/info_file.rst: -------------------------------------------------------------------------------- 1 | The ``info.yaml`` file 2 | ====================== 3 | 4 | At the heart of papis there is the information file. The info file contains 5 | all information about the documents. 6 | 7 | It uses the `yaml `_ syntax to store 8 | information, which is a very human-readable language. It is quite format-free: 9 | `papis` does not assume that any special information should be there. 10 | 11 | If you are storing papers with papis, then you most probably would like to 12 | store author and title in there like this: 13 | 14 | .. code:: yaml 15 | 16 | author: Isaac Newton 17 | title: Opticks, or a treatise of the reflections refractions, inflections and 18 | colours of light 19 | files: 20 | - document.pdf 21 | -------------------------------------------------------------------------------- /doc/source/library_structure.rst: -------------------------------------------------------------------------------- 1 | The library structure 2 | ===================== 3 | 4 | One of the things that makes papis interesting is the fact 5 | that its library structure is nearly nonexistent. 6 | 7 | A papis library is linked to a directory, where all the documents are (and 8 | possibly sublibraries). What papis does is simply to go to the library folder 9 | and look for all subfolders that contain a information file, which by default 10 | is a ``info.yaml`` file. 11 | 12 | Every subfolder that has an ``info.yaml`` file in it is a valid papis document. 13 | As an example let us consider the following library 14 | 15 | :: 16 | 17 | /home/fulano/Documents/papers/ 18 | ├── folder1 19 | │   └── paper.pdf 20 | ├── folder2 21 | │   ├── folder3 22 | │   │   ├── info.yaml 23 | │   │   └── blahblahblah.pdf 24 | │   └── folder4 25 | │   ├── info.yaml 26 | │   └── output.pdf 27 | ├── classics 28 | │   └── folder5 29 | │   ├── info.yaml 30 | │   └── output.pdf 31 | ├── physics 32 | │   └── newton 33 | │   └── principia 34 | │   ├── document.pdf 35 | │   ├── supplements.pdf 36 | │   └── info.yaml 37 | └─── rpa 38 |    └── bohm 39 |    ├── info.yaml 40 |    ├── notes.tex 41 |    └── output.pdf 42 | 43 | The first thing that you might notice is that there are many folders. 44 | Just to check that you understand exactly what is a document, 45 | please think about which of these pdfs is not a valid papis document... That's 46 | right!, ``folder1/paper.pdf`` is not a valid document since the folder1 does not 47 | contain any ``info.yaml`` file. You see also that it does not matter how deep the 48 | folder structure is in your library: you can have a ``physics`` folder in which you 49 | have a ``newton`` folder in which you have a folder containing the actual book 50 | ``document.pdf`` plus some supplementary information ``supplements.pdf``. In this 51 | case, inside the ``info.yaml`` you would have the following ``file`` section 52 | 53 | .. code:: yaml 54 | 55 | files: 56 | - document.pdf 57 | - supplements.pdf 58 | 59 | which tells papis that this folder contains two relevant files. 60 | -------------------------------------------------------------------------------- /doc/source/plugins.rst: -------------------------------------------------------------------------------- 1 | Plugin architecture 2 | =================== 3 | 4 | .. note:: TODO 5 | -------------------------------------------------------------------------------- /doc/source/scihub.rst: -------------------------------------------------------------------------------- 1 | Scihub support 2 | ============== 3 | 4 | .. image:: https://badge.fury.io/py/papis-scihub.svg 5 | :target: https://badge.fury.io/py/papis-scihub 6 | 7 | Papis has a script that uses the scihub platform to download scientific 8 | papers. Due to legal caution the script is not included directly 9 | as a papis command, and it has its own PyPi repository. 10 | 11 | 12 | To install it, just type 13 | 14 | :: 15 | 16 | pip3 install papis-scihub 17 | 18 | 19 | Now you can type 20 | 21 | .. code:: bash 22 | 23 | papis scihub -h 24 | 25 | and see the help message of the script. 26 | 27 | Some usage examples are: 28 | 29 | 30 | - Download via the doi number: 31 | 32 | .. code:: bash 33 | 34 | papis scihub 10.1002/andp.19053220607 \\ 35 | add -d einstein_papers --folder-name photon_definition 36 | 37 | - Download via a url that contains the doi number in the format ``.*/doi/`` 38 | 39 | .. code:: bash 40 | 41 | papis scihub http://physicstoday.scitation.org/doi/10.1063/1.881498 \\ 42 | add --folder-name important_paper 43 | 44 | - Download via the ``doi.org`` url: 45 | 46 | .. code:: bash 47 | 48 | papis scihub https://doi.org/10.1016/j.physrep.2016.12.002 add 49 | 50 | 51 | -------------------------------------------------------------------------------- /doc/source/shell_completion.rst: -------------------------------------------------------------------------------- 1 | Shell auto-completion 2 | ==================== 3 | 4 | Papis has a bash auto-completion script that comes installed 5 | when you install papis with ``pip3``. 6 | 7 | It should be installed in a relative path 8 | 9 | :: 10 | 11 | PREFIX/etc/bash_completion.d/papis 12 | 13 | normally the ``PREFIX`` part is ``/usr/local/``, so you can add the 14 | following line to your ``~/.bashrc`` file 15 | 16 | :: 17 | 18 | source /usr/local/etc/bash_completion.d/papis 19 | 20 | or get the bash script from 21 | `here `_. 22 | 23 | 24 | Zsh 25 | --- 26 | 27 | There is also a way for ``zsh`` users to auto-complete. Either downloading the 28 | script 29 | `here `_. 30 | or adding the following line int the ``.zshrc`` configuration file 31 | 32 | :: 33 | 34 | eval "$(_PAPIS_COMPLETE=source_zsh papis)" 35 | -------------------------------------------------------------------------------- /examples/scripts/papis-ga: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # papis-short-help: Git add 4 | # Copyright © 2017 Alejandro Gallo. GPLv3 5 | import sys 6 | import os 7 | import papis.api 8 | import papis.commands 9 | import subprocess 10 | 11 | 12 | def usage(): 13 | print("Usage: papis ga ") 14 | 15 | 16 | def add(doc): 17 | path = os.path.expanduser(papis.config.get_lib_dirs()[0]) 18 | cmd = ['git', '-C', path, 'add'] + doc.get_files() + [doc.get_info_file()] 19 | print(cmd) 20 | subprocess.call(cmd) 21 | 22 | 23 | if __name__ == "__main__": 24 | if len(sys.argv) < 2: 25 | search = "" 26 | else: 27 | search = sys.argv[1] 28 | if search in ['-h', '--help']: 29 | usage() 30 | sys.exit(0) 31 | 32 | documents = papis.api.get_documents_in_lib( 33 | papis.api.get_lib_name(), 34 | search=search 35 | ) 36 | 37 | doc = papis.api.pick_doc( 38 | documents 39 | ) 40 | 41 | add(doc) 42 | -------------------------------------------------------------------------------- /examples/scripts/papis-mail: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # papis-short-help: Email a paper to my friend 3 | # Copyright © 2017 Alejandro Gallo. GPLv3 4 | 5 | if [[ $1 = "-h" ]]; then 6 | echo "Email a paper to my friend" 7 | cat < ${zip_name})" 23 | zip -r ${zip_name} ${folder_name} 24 | 25 | ${mail_agent} -a ${zip_name} 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/scripts/papis-tags: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Last modified: 2018-11-14 4 | import papis.api 5 | import papis.cli 6 | from papis.commands.open import run as papis_open 7 | import papis.database 8 | import click 9 | 10 | 11 | @click.command() 12 | @click.help_option('-h', '--help') 13 | @papis.cli.query_option() 14 | def main(query): 15 | """ 16 | Search tags of the library and open a document 17 | """ 18 | documents = papis.api.get_documents_in_lib( 19 | papis.api.get_lib_name(), 20 | search=query 21 | ) 22 | 23 | # Create an empty tag list 24 | tag_list = [] 25 | for item in documents: 26 | if 'tags' in item.keys(): 27 | for tag in list([item['tags']]): 28 | # if tag contains a comma, split it to a list 29 | if tag.split(','): 30 | tag = tag.split(',') 31 | else: 32 | continue 33 | tag_list.append(tag) 34 | 35 | # if no tags are found, exit gracefully 36 | if tag_list == []: 37 | print('The library does not have any tags set') 38 | return 39 | 40 | # flatten the list, which may be a list of lists 41 | # also make it lower case and strip out any preceding spaces. The list 42 | # set, is then sorted to alphanumeric order. 43 | flat_list = sorted(set([y.strip(' ').lower() for x in tag_list for y in x])) 44 | 45 | # Allow the list set (no duplicates) )to be sorted into alphabetical 46 | # order and picked from 47 | picked_tag = papis.api.pick(flat_list) 48 | 49 | docs = papis.database.get().query_dict(dict(tags=picked_tag)) 50 | 51 | doc = papis.api.pick_doc(docs) 52 | if not doc: 53 | return 54 | 55 | papis_open(doc) 56 | 57 | 58 | if __name__ == "__main__": 59 | main() 60 | -------------------------------------------------------------------------------- /papis/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Information 4 | __license__ = 'GPLv3' 5 | __version__ = '0.9' 6 | __author__ = __maintainer__ = 'Alejandro Gallo' 7 | __email__ = 'aamsgallo@gmail.com' 8 | 9 | 10 | if os.environ.get('PAPIS_DEBUG'): 11 | import logging 12 | log_format = ( 13 | '%(relativeCreated)d-' + 14 | '%(levelname)s' + 15 | ':' + 16 | '%(name)s' + 17 | ':' + 18 | '%(message)s' 19 | ) 20 | logging.basicConfig(format=log_format, level=logging.DEBUG) 21 | -------------------------------------------------------------------------------- /papis/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | TODO: Create an own python package for this 3 | 4 | For description refer to 5 | https://www.base-search.net/about/download/base_interface.pdf 6 | 7 | """ 8 | import urllib.parse 9 | import urllib.request # import urlencode 10 | import logging 11 | import json 12 | import click 13 | 14 | logger = logging.getLogger('base') 15 | 16 | BASE_BASEURL = ( 17 | "https://api.base-search.net/" 18 | "cgi-bin/BaseHttpSearchInterface.fcgi/" 19 | ) 20 | 21 | 22 | def get_data(query="", max=20): 23 | global BASE_BASEURL 24 | 25 | logger.warning('BASE engine in papis is experimental') 26 | 27 | if max > 125: 28 | logger.error('BASE only allows a maximum of 125 hits') 29 | max = 125 30 | 31 | dict_params = { 32 | "func": "PerformSearch", 33 | "query": query, 34 | "format": "json", 35 | "hits": max, 36 | } 37 | params = urllib.parse.urlencode( 38 | {x: dict_params[x] for x in dict_params if dict_params[x]} 39 | ) 40 | req_url = BASE_BASEURL + "search?" + params 41 | logger.debug("url = " + req_url) 42 | url = urllib.request.Request( 43 | req_url, 44 | headers={ 45 | 'User-Agent': 'papis' 46 | } 47 | ) 48 | jsondoc = json.loads(urllib.request.urlopen(url).read().decode()) 49 | docs = jsondoc.get('response').get('docs') 50 | logger.info("Retrieved {0} documents".format(len(docs))) 51 | return list(map(basedoc_to_papisdoc, docs)) 52 | 53 | 54 | def basedoc_to_papisdoc(basedoc): 55 | """Convert a json doc from the base database into a papis document 56 | 57 | :basedoc: Json doc from base database 58 | :returns: Dictionary containing its data 59 | 60 | """ 61 | doc = dict() 62 | keys_translate = [ 63 | ("dctitle", "title", "single"), 64 | ("dcyear", "year", "single"), 65 | ("dclink", "url", "single"), 66 | ("dcdescription", "abstract", "single"), 67 | ("dcpublisher", "publisher", "multi", lambda x: x[0]), 68 | ("dcperson", "author", "multi", lambda x: " and ".join(x)), 69 | ("dcsubject", "tags", "multi", lambda x: " ".join(x)), 70 | ("dcdoi", "doi", "multi", lambda x: x[0]), 71 | ("dctype", "type", "multi", lambda x: x[0].lower()), 72 | ("dclang", "lang", "mutli", lambda x: x[0]), 73 | ] 74 | for kt in keys_translate: 75 | if kt[0] in basedoc.keys(): 76 | key = kt[1] 77 | if kt[2] == "multi": 78 | value = basedoc[kt[0]] 79 | value = kt[3](value) if kt[3] is not None else value 80 | else: 81 | value = basedoc[kt[0]] 82 | doc[key] = value 83 | return doc 84 | 85 | 86 | @click.command('base') 87 | @click.pass_context 88 | @click.help_option('--help', '-h') 89 | @click.option('--query', '-q', default=None) 90 | def explorer(ctx, query): 91 | """ 92 | Look for documents on the BielefeldAcademicSearchEngine 93 | 94 | Examples of its usage are 95 | 96 | papis explore base -q 'Albert einstein' pick cmd 'firefox {doc[url]}' 97 | 98 | """ 99 | import papis.document 100 | logger = logging.getLogger('explore:base') 101 | logger.info('Looking up...') 102 | data = get_data(query=query) 103 | docs = [papis.document.from_data(data=d) for d in data] 104 | ctx.obj['documents'] += docs 105 | logger.info('{} documents found'.format(len(docs))) 106 | -------------------------------------------------------------------------------- /papis/cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | import papis.config 3 | import difflib 4 | 5 | 6 | class AliasedGroup(click.Group): 7 | """ 8 | This group command is taken from 9 | http://click.palletsprojects.com/en/5.x/advanced/#command-aliases 10 | and is to be used for groups with aliases 11 | """ 12 | 13 | def get_command(self, ctx, cmd_name): 14 | rv = click.Group.get_command(self, ctx, cmd_name) 15 | if rv is not None: 16 | return rv 17 | matches = difflib.get_close_matches( 18 | cmd_name, self.list_commands(ctx), n=2) 19 | if not matches: 20 | return None 21 | elif len(matches) == 1: 22 | return click.Group.get_command(self, ctx, matches[0]) 23 | ctx.fail('Too many matches: %s' % ', '.join(sorted(matches))) 24 | 25 | 26 | def query_option(**attrs): 27 | """Adds a ``query`` argument as a decorator""" 28 | def decorator(f): 29 | attrs.setdefault( 30 | 'default', 31 | lambda: papis.config.get('default-query-string')) 32 | return click.decorators.argument('query', **attrs)(f) 33 | return decorator 34 | 35 | 36 | def doc_folder_option(**attrs): 37 | """Adds a ``query`` argument as a decorator""" 38 | def decorator(f): 39 | attrs.setdefault('default', None) 40 | attrs.setdefault('type', click.Path(exists=True)) 41 | attrs.setdefault('help', 'Apply action to a document path') 42 | return click.decorators.option('--doc-folder', **attrs)(f) 43 | return decorator 44 | 45 | 46 | def git_option(help="Add git interoperability", **attrs): 47 | """Adds a ``git`` option as a decorator""" 48 | def decorator(f): 49 | attrs.setdefault( 50 | 'default', 51 | lambda: True if papis.config.get('use-git') else False) 52 | attrs.setdefault('help', help) 53 | return click.decorators.option('--git/--no-git', **attrs)(f) 54 | return decorator 55 | 56 | 57 | def bypass(group, command, command_name): 58 | """ 59 | This function is specially important for people developing scripts in 60 | papis. 61 | 62 | Suppose you're writing a plugin that uses the ``add`` command as seen 63 | in the command line in papis. However you don't want exactly the ``add`` 64 | command and you want to add some behavior before calling it, and you 65 | don't want to write your own ``add`` function from scratch. 66 | 67 | You can then use the following snippet 68 | 69 | .. code::python 70 | 71 | import click 72 | import papis.cli 73 | import papis.commands.add 74 | 75 | @click.group() 76 | def main(): 77 | \"\"\"Your main app\"\"\" 78 | pass 79 | 80 | @papis.cli.bypass(main, papis.commands.add.cli, "add") 81 | def add(**kwargs): 82 | # do some logic here... 83 | # and call the original add command line function by 84 | papis.commands.add.cli.bypassed(**kwargs) 85 | """ 86 | group.add_command(command, command_name) 87 | 88 | def decorator(new_callback): 89 | setattr(command, "bypassed", command.callback) 90 | command.callback = new_callback 91 | return decorator 92 | -------------------------------------------------------------------------------- /papis/commands/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | from stevedore import extension 4 | import logging 5 | import papis.config 6 | import papis.plugin 7 | import re 8 | 9 | 10 | commands_mgr = None 11 | 12 | 13 | def _create_commands_mgr(): 14 | global commands_mgr 15 | 16 | if commands_mgr is not None: 17 | return 18 | 19 | commands_mgr = extension.ExtensionManager( 20 | namespace='papis.command', 21 | invoke_on_load=False, 22 | verify_requirements=True, 23 | propagate_map_exceptions=True, 24 | on_load_failure_callback=papis.plugin.stevedore_error_handler 25 | ) 26 | 27 | 28 | def get_external_scripts(): 29 | regex = re.compile('.*papis-([^ .]+)$') 30 | paths = [] 31 | scripts = {} 32 | paths.append(papis.config.get_scripts_folder()) 33 | paths += os.environ["PATH"].split(":") 34 | for path in paths: 35 | for script in glob.glob(os.path.join(path, "papis-*")): 36 | m = regex.match(script) 37 | if m is not None: 38 | name = m.group(1) 39 | scripts[name] = dict( 40 | command_name=name, 41 | path=script, 42 | plugin=None 43 | ) 44 | return scripts 45 | 46 | 47 | def get_scripts(): 48 | global commands_mgr 49 | _create_commands_mgr() 50 | scripts_dict = dict() 51 | for command_name in commands_mgr.names(): 52 | scripts_dict[command_name] = dict( 53 | command_name=command_name, 54 | path=None, 55 | plugin=commands_mgr[command_name].plugin 56 | ) 57 | return scripts_dict 58 | -------------------------------------------------------------------------------- /papis/commands/browse.py: -------------------------------------------------------------------------------- 1 | """This command will try its best to find a source in the internet for the 2 | document at hand. 3 | 4 | Of course if the document has an url key in its info file, it will use this url 5 | to open it in a browser. Also if it has a doc_url key, or a doi, it will try 6 | to compose urls out of these to open it. 7 | 8 | If none of the above work, then it will try to use a search engine with the 9 | document's information (using the ``browse-query-format``). You can select 10 | wich search engine you want to use using the ``search-engine`` setting. 11 | 12 | Cli 13 | ^^^ 14 | .. click:: papis.commands.browse:cli 15 | :prog: papis browse 16 | """ 17 | import papis 18 | import papis.utils 19 | import papis.config 20 | import papis.cli 21 | import papis.pick 22 | import click 23 | import papis.database 24 | import papis.strings 25 | import papis.document 26 | from urllib.parse import urlencode 27 | import logging 28 | 29 | 30 | logger = logging.getLogger('browse') 31 | 32 | 33 | def run(document): 34 | """Browse document's url whenever possible. 35 | 36 | :document: Document object 37 | :returns: Returns the url that is composed from the document 38 | :rtype: str 39 | 40 | """ 41 | global logger 42 | url = None 43 | key = papis.config.get("browse-key") 44 | 45 | if document.has(key): 46 | if "doi" == key: 47 | url = 'https://doi.org/{}'.format(document['doi']) 48 | elif "isbn" == key: 49 | url = 'https://isbnsearch.org/isbn/{}'.format(document['isbn']) 50 | else: 51 | url = document[key] 52 | 53 | if url is None or key == 'search-engine': 54 | params = { 55 | 'q': papis.utils.format_doc( 56 | papis.config.get('browse-query-format'), 57 | document 58 | ) 59 | } 60 | url = papis.config.get('search-engine') + '/?' + urlencode(params) 61 | 62 | logger.info("Opening url %s:" % url) 63 | papis.utils.general_open(url, "browser", wait=False) 64 | return url 65 | 66 | 67 | @click.command("browse") 68 | @click.help_option('--help', '-h') 69 | @papis.cli.query_option() 70 | @click.option( 71 | '-k', '--key', default='', 72 | help='Use the value of the document\'s key to open in the browser, e.g.' 73 | 'doi, url, doc_url ...' 74 | ) 75 | @click.option( 76 | '--all', default=False, is_flag=True, 77 | help='Browse all selected documents' 78 | ) 79 | def cli(query, key, all): 80 | """Open document's url in a browser""" 81 | documents = papis.database.get().query(query) 82 | logger = logging.getLogger('cli:browse') 83 | 84 | if len(documents) == 0: 85 | logger.warning(papis.strings.no_documents_retrieved_message) 86 | return 0 87 | 88 | if not all: 89 | document = papis.pick.pick_doc(documents) 90 | if not document: 91 | return 92 | documents = [document] 93 | 94 | if len(key): 95 | papis.config.set('browse-key', key) 96 | 97 | for document in documents: 98 | run(document) 99 | -------------------------------------------------------------------------------- /papis/commands/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | The command config is a useful command because it allows you to check 3 | the configuration settings' values that your current `papis` session 4 | is using. 5 | 6 | For example let's say that you want to see which ``dir`` setting your 7 | current library is using (i.e., the directory or the dir that appears 8 | in the definition of the library in the configuration file), then you 9 | would simply do: 10 | 11 | .. code:: 12 | 13 | papis config dir 14 | 15 | If you wanted to see which ``dir`` the library ``books`` has, for example 16 | then you would do 17 | 18 | .. code:: 19 | 20 | papis -l books config dir 21 | 22 | This works as well for default settings, i.e., settings that you have not 23 | customized, for example the setting ``match-format``, you would check 24 | it with 25 | 26 | .. code:: 27 | 28 | papis config match-format 29 | > {doc[tags]}{doc.subfolder}{doc[title]}{doc[author]}{doc[year]} 30 | 31 | You can find a list of all available settings in the configuration section. 32 | 33 | Cli 34 | ^^^ 35 | .. click:: papis.commands.config:cli 36 | :prog: papis config 37 | """ 38 | import papis.commands 39 | import logging 40 | import click 41 | 42 | 43 | def run(option_string): 44 | logger = logging.getLogger('config:run') 45 | option = option_string.split(".") 46 | if len(option) == 1: 47 | key = option[0] 48 | section = None 49 | elif len(option) == 2: 50 | section = option[0] 51 | key = option[1] 52 | logger.debug("key = %s, sec = %s" % (key, section)) 53 | val = papis.config.get(key, section=section) 54 | return val 55 | 56 | 57 | @click.command("config") 58 | @click.help_option('--help', '-h') 59 | @click.argument("option") 60 | def cli(option): 61 | """Print configuration values""" 62 | logger = logging.getLogger('cli:config') 63 | logger.debug(option) 64 | click.echo(run(option)) 65 | return 0 66 | -------------------------------------------------------------------------------- /papis/commands/edit.py: -------------------------------------------------------------------------------- 1 | """This command edits the information of the documents. 2 | The editor used is defined by the ``editor`` configuration setting. 3 | 4 | 5 | Cli 6 | ^^^ 7 | .. click:: papis.commands.edit:cli 8 | :prog: papis edit 9 | """ 10 | import papis 11 | import os 12 | import papis.api 13 | import papis.pick 14 | import papis.document 15 | import papis.utils 16 | import papis.config 17 | import papis.database 18 | import papis.cli 19 | import click 20 | import logging 21 | import papis.strings 22 | import papis.git 23 | 24 | 25 | def run(document, wait=True, git=False): 26 | database = papis.database.get() 27 | papis.utils.general_open(document.get_info_file(), "editor", wait=wait) 28 | document.load() 29 | database.update(document) 30 | if git: 31 | papis.git.add_and_commit_resource( 32 | document.get_main_folder(), 33 | document.get_info_file(), 34 | "Update information for '{0}'".format( 35 | papis.document.describe(document))) 36 | 37 | 38 | @click.command("edit") 39 | @click.help_option('-h', '--help') 40 | @papis.cli.query_option() 41 | @papis.cli.doc_folder_option() 42 | @papis.cli.git_option(help="Add changes made to the info file") 43 | @click.option( 44 | "-n", 45 | "--notes", 46 | help="Edit notes associated to the document", 47 | default=False, 48 | is_flag=True) 49 | @click.option( 50 | "--all", "_all", 51 | help="Edit all matching documents", 52 | default=False, 53 | is_flag=True) 54 | @click.option( 55 | "-e", 56 | "--editor", 57 | help="Editor to be used", 58 | default=None) 59 | def cli(query, doc_folder, git, notes, _all, editor): 60 | """Edit document information from a given library""" 61 | 62 | logger = logging.getLogger('cli:edit') 63 | 64 | if doc_folder: 65 | documents = [papis.document.from_folder(doc_folder)] 66 | else: 67 | documents = papis.database.get().query(query) 68 | 69 | if editor is not None: 70 | papis.config.set('editor', editor) 71 | 72 | if not _all: 73 | document = papis.pick.pick_doc(documents) 74 | documents = [document] if document else [] 75 | 76 | if len(documents) == 0: 77 | logger.warning(papis.strings.no_documents_retrieved_message) 78 | return 0 79 | 80 | for document in documents: 81 | if notes: 82 | logger.debug("Editing notes") 83 | if not document.has("notes"): 84 | logger.warning( 85 | "The document selected has no notes attached, \n" 86 | "creating a notes files" 87 | ) 88 | document["notes"] = papis.config.get("notes-name") 89 | document.save() 90 | notesPath = os.path.join( 91 | document.get_main_folder(), 92 | document["notes"] 93 | ) 94 | 95 | if not os.path.exists(notesPath): 96 | logger.info("Creating {0}".format(notesPath)) 97 | open(notesPath, "w+").close() 98 | 99 | papis.api.edit_file(notesPath) 100 | if git: 101 | papis.git.add_and_commit_resource( 102 | document.get_main_folder(), 103 | document.get_info_file(), 104 | "Update notes for '{0}'".format( 105 | papis.document.describe(document))) 106 | 107 | else: 108 | run(document, git=git) 109 | -------------------------------------------------------------------------------- /papis/commands/external.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import subprocess 4 | import papis.config 5 | import papis.commands 6 | import click 7 | import logging 8 | 9 | 10 | logger = logging.getLogger("external") 11 | 12 | 13 | def get_command_help(path): 14 | magic_word = papis.config.get("scripts-short-help-regex") 15 | with open(path) as fd: 16 | for line in fd: 17 | m = re.match(magic_word, line) 18 | if m: 19 | return m.group(1) 20 | return "No help message available" 21 | 22 | 23 | def export_variables(): 24 | """Export environment variables so that external script can access to 25 | the information 26 | """ 27 | os.environ["PAPIS_LIB"] = papis.config.get_lib().name 28 | os.environ["PAPIS_LIB_PATH"] = papis.config.get_lib().path_format() 29 | os.environ["PAPIS_CONFIG_PATH"] = papis.config.get_config_folder() 30 | os.environ["PAPIS_CONFIG_FILE"] = papis.config.get_config_file() 31 | os.environ["PAPIS_SCRIPTS_PATH"] = papis.config.get_scripts_folder() 32 | 33 | 34 | @click.command( 35 | context_settings=dict( 36 | ignore_unknown_options=True, 37 | help_option_names=[] 38 | ) 39 | ) 40 | @click.argument("flags", nargs=-1) 41 | @click.pass_context 42 | def external_cli(ctx, flags): 43 | script = ctx.obj 44 | cmd = [script['path']] + list(flags) 45 | logger.debug("Calling {}".format(cmd)) 46 | export_variables() 47 | subprocess.call(cmd) 48 | -------------------------------------------------------------------------------- /papis/commands/git.py: -------------------------------------------------------------------------------- 1 | """ 2 | This command is useful if your library is itself a git repository. 3 | You can use this command to issue git commands in your library 4 | repository without having to change your current directory. 5 | 6 | CLI Examples 7 | ^^^^^^^^^^^^ 8 | 9 | - Check the status of the library repository: 10 | 11 | .. code:: 12 | 13 | papis git status 14 | 15 | - Commit all changes: 16 | 17 | .. code:: 18 | 19 | papis git commit -a 20 | 21 | 22 | """ 23 | import papis.commands 24 | import papis.commands.run 25 | import papis.config 26 | import logging 27 | import click 28 | 29 | logger = logging.getLogger('git') 30 | 31 | 32 | def run(folder, command=[]): 33 | return papis.commands.run.run(folder, command=["git"] + command) 34 | 35 | 36 | @click.command("git", context_settings=dict(ignore_unknown_options=True)) 37 | @click.help_option('--help', '-h') 38 | @click.argument("command", nargs=-1) 39 | def cli(command): 40 | "Run a git command in the library folder" 41 | for d in papis.config.get_lib_dirs(): 42 | run(d, command=list(command)) 43 | -------------------------------------------------------------------------------- /papis/commands/mv.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Cli 4 | ^^^ 5 | .. click:: papis.commands.mv:cli 6 | :prog: papis mv 7 | """ 8 | import papis 9 | import os 10 | import papis.config 11 | import papis.utils 12 | import papis.database 13 | import subprocess 14 | import logging 15 | import papis.cli 16 | import papis.pick 17 | import papis.strings 18 | import click 19 | 20 | 21 | def run(document, new_folder_path, git=False): 22 | logger = logging.getLogger('mv:run') 23 | folder = document.get_main_folder() 24 | cmd = ['git', '-C', folder] if git else [] 25 | cmd += ['mv', folder, new_folder_path] 26 | db = papis.database.get() 27 | logger.debug(cmd) 28 | subprocess.call(cmd) 29 | db.delete(document) 30 | new_document_folder = os.path.join( 31 | new_folder_path, 32 | os.path.basename(document.get_main_folder()) 33 | ) 34 | logger.debug("New document folder: {}".format(new_document_folder)) 35 | document.set_folder(new_document_folder) 36 | db.add(document) 37 | 38 | 39 | @click.command("mv") 40 | @click.help_option('--help', '-h') 41 | @papis.cli.query_option() 42 | @papis.cli.git_option() 43 | def cli(query, git): 44 | """Move a document into some other path""" 45 | # Leave this imports here for performance 46 | import prompt_toolkit 47 | import prompt_toolkit.completion 48 | 49 | logger = logging.getLogger('cli:mv') 50 | 51 | documents = papis.database.get().query(query) 52 | if not documents: 53 | logger.warning(papis.strings.no_documents_retrieved_message) 54 | return 55 | 56 | document = papis.pick.pick_doc(documents) 57 | if not document: 58 | return 0 59 | 60 | lib_dir = os.path.expanduser(papis.config.get_lib_dirs()[0]) 61 | 62 | completer = prompt_toolkit.completion.PathCompleter( 63 | only_directories=True, 64 | get_paths=lambda: [lib_dir] 65 | ) 66 | 67 | try: 68 | new_folder = os.path.join( 69 | lib_dir, 70 | prompt_toolkit.prompt( 71 | message=( 72 | "Enter directory : (Tab completion enabled)\n" 73 | "Current directory: ({dir})\n".format( 74 | dir=document.get_main_folder_name() 75 | ) + 76 | "> " 77 | ), 78 | completer=completer, 79 | complete_while_typing=True 80 | )) 81 | except Exception as e: 82 | logger.error(e) 83 | return 0 84 | 85 | logger.info(new_folder) 86 | 87 | if not os.path.exists(new_folder): 88 | logger.info("Creating path %s" % new_folder) 89 | os.makedirs(new_folder, mode=papis.config.getint('dir-umask')) 90 | 91 | run(document, new_folder, git=git) 92 | -------------------------------------------------------------------------------- /papis/commands/rename.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Cli 4 | ^^^ 5 | .. click:: papis.commands.rename:cli 6 | :prog: papis rename 7 | """ 8 | import papis 9 | import os 10 | import papis.pick 11 | import papis.utils 12 | import subprocess 13 | import logging 14 | import click 15 | import papis.cli 16 | import papis.database 17 | import papis.strings 18 | 19 | 20 | def run(document, new_name, git=False): 21 | db = papis.database.get() 22 | logger = logging.getLogger('rename:run') 23 | folder = document.get_main_folder() 24 | subfolder = os.path.dirname(folder) 25 | 26 | new_folder_path = os.path.join(subfolder, new_name) 27 | 28 | if os.path.exists(new_folder_path): 29 | logger.warning("Path %s already exists" % new_folder_path) 30 | return 1 31 | 32 | cmd = ['git', '-C', folder] if git else [] 33 | cmd += ['mv', folder, new_folder_path] 34 | 35 | logger.debug(cmd) 36 | subprocess.call(cmd) 37 | 38 | if git: 39 | papis.utils.git_commit(message="Rename %s" % folder) 40 | 41 | db.delete(document) 42 | logger.debug("New document folder: {}".format(new_folder_path)) 43 | document.set_folder(new_folder_path) 44 | db.add(document) 45 | return 0 46 | 47 | 48 | @click.command("rename") 49 | @click.help_option('--help', '-h') 50 | @papis.cli.query_option() 51 | @papis.cli.git_option() 52 | def cli(query, git): 53 | """Rename entry""" 54 | 55 | documents = papis.database.get().query(query) 56 | logger = logging.getLogger('cli:rename') 57 | 58 | if not documents: 59 | logger.warning(papis.strings.no_documents_retrieved_message) 60 | document = papis.pick.pick_doc(documents) 61 | if not document: 62 | return 0 63 | 64 | new_name = papis.utils.input( 65 | "Enter new folder name:\n" 66 | ">", 67 | default=document.get_main_folder_name() 68 | ) 69 | return run(document, new_name, git=git) 70 | -------------------------------------------------------------------------------- /papis/commands/run.py: -------------------------------------------------------------------------------- 1 | """ 2 | This command is useful to issue commands in the directory of your library. 3 | 4 | CLI Examples 5 | ^^^^^^^^^^^^ 6 | 7 | - List files in your directory 8 | 9 | .. code:: 10 | 11 | papis run ls 12 | 13 | - Find a file in your directory using the ``find`` command 14 | 15 | .. code:: 16 | 17 | papis run find -name 'document.pdf' 18 | 19 | Python examples 20 | ^^^^^^^^^^^^^^^ 21 | 22 | .. code::python 23 | 24 | from papis.commands.run import run 25 | 26 | run(library='papers', command=["ls", "-a"]) 27 | 28 | Cli 29 | ^^^ 30 | .. click:: papis.commands.run:cli 31 | :prog: papis run 32 | """ 33 | import os 34 | import papis.config 35 | import papis.exceptions 36 | import logging 37 | import click 38 | 39 | logger = logging.getLogger('run') 40 | 41 | 42 | def run(folder, command=[]): 43 | logger.debug("Changing directory into %s" % folder) 44 | os.chdir(os.path.expanduser(folder)) 45 | try: 46 | commandstr = os.path.expanduser( 47 | papis.config.get("".join(command)) 48 | ) 49 | except papis.exceptions.DefaultSettingValueMissing: 50 | commandstr = " ".join(command) 51 | logger.debug("Command = %s" % commandstr) 52 | return os.system(commandstr) 53 | 54 | 55 | @click.command("run", context_settings=dict(ignore_unknown_options=True)) 56 | @click.help_option('--help', '-h') 57 | @click.argument("run_command", nargs=-1) 58 | def cli(run_command): 59 | """Run an arbitrary shell command in the library folder""" 60 | for folder in papis.config.get_lib_dirs(): 61 | run(folder, command=run_command) 62 | -------------------------------------------------------------------------------- /papis/database/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger('database') 3 | 4 | DATABASES = dict() 5 | 6 | 7 | def get(library=None): 8 | global DATABASES 9 | import papis.config 10 | if library is None: 11 | library = papis.config.get_lib() 12 | elif isinstance(library, str): 13 | library = papis.config.get_lib_from_name(library) 14 | backend = papis.config.get('database-backend') 15 | database = DATABASES.get(library) 16 | # if there is already a database and the backend of the database 17 | # is the same as the config backend, then return that library 18 | # else we will (re)define the database in the dictionary DATABASES 19 | if database is not None and database.get_backend_name() == backend: 20 | return DATABASES.get(library) 21 | if backend == "papis": 22 | import papis.database.cache 23 | DATABASES[library] = papis.database.cache.Database(library) 24 | return DATABASES.get(library) 25 | elif backend == "whoosh": 26 | import papis.database.whoosh 27 | DATABASES[library] = papis.database.whoosh.Database(library) 28 | return DATABASES.get(library) 29 | else: 30 | raise Exception('No valid database type: {}'.format(backend)) 31 | 32 | 33 | def get_all_query_string(): 34 | return get().get_all_query_string() 35 | 36 | 37 | def clear_cached(): 38 | global DATABASES 39 | DATABASES = dict() 40 | -------------------------------------------------------------------------------- /papis/database/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Here the database abstraction for the libraries is defined. 3 | """ 4 | 5 | import papis.utils 6 | import papis.config 7 | import papis.library 8 | 9 | 10 | class Database(object): 11 | """Abstract class for the database backends 12 | """ 13 | 14 | def __init__(self, library=None): 15 | self.lib = library or papis.config.get_lib() 16 | assert(isinstance(self.lib, papis.library.Library)) 17 | 18 | def initialize(self): 19 | raise NotImplementedError('Initialize not implemented') 20 | 21 | def get_backend_name(self): 22 | raise NotImplementedError('Get backend name not implemented') 23 | 24 | def get_lib(self): 25 | """Get library name 26 | """ 27 | return self.lib.name 28 | 29 | def get_dirs(self): 30 | """Get directories of the library 31 | """ 32 | return self.lib.paths 33 | 34 | def match(self, document, query_string): 35 | """Wether or not document matches query_string 36 | 37 | :param document: Document to be matched 38 | :type document: papis.document.Document 39 | :param query_string: Query string 40 | :type query_string: str 41 | """ 42 | raise NotImplementedError('Match not implemented') 43 | 44 | def clear(self): 45 | raise NotImplementedError('Clear not implemented') 46 | 47 | def add(self, document): 48 | raise NotImplementedError('Add not implemented') 49 | 50 | def update(self, document): 51 | raise NotImplementedError('Update not implemented') 52 | 53 | def delete(self, document): 54 | raise NotImplementedError('Delete not implemented') 55 | 56 | def query(self, query_string): 57 | raise NotImplementedError('Query not implemented') 58 | 59 | def query_dict(self, query_string): 60 | raise NotImplementedError('Query dict not implemented') 61 | 62 | def get_all_documents(self): 63 | raise NotImplementedError('Get all docs not implemented') 64 | 65 | def get_all_query_string(self): 66 | raise NotImplementedError('Get all query string not implemented') 67 | -------------------------------------------------------------------------------- /papis/dissemin.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import urllib.request # urlopen, Request 3 | import urllib.parse # import urlencode 4 | import papis.config 5 | import json 6 | import click 7 | import papis.document 8 | 9 | 10 | logger = logging.getLogger('dissemin') 11 | 12 | 13 | def dissemin_authors_to_papis_authors(data): 14 | new_data = dict() 15 | if 'authors' in data.keys(): 16 | authors = [] 17 | for author in data['authors']: 18 | # keys = ('first', 'last') 19 | authors.append( 20 | dict( 21 | given_name=author['name']['first'], 22 | surname=author['name']['last'] 23 | ) 24 | ) 25 | new_data["author_list"] = authors 26 | new_data["author"] = ",".join( 27 | ["{a[given_name]} {a[surname]}".format(a=a) for a in authors] 28 | ) 29 | return new_data 30 | 31 | 32 | def dissemindoc_to_papis(data): 33 | common_data = dict() 34 | result = [] 35 | common_data['title'] = data.get('title') 36 | common_data['type'] = data.get('type') 37 | common_data.update(dissemin_authors_to_papis_authors(data)) 38 | for record in data['records']: 39 | new_data = dict() 40 | new_data.update(common_data) 41 | new_data.update(record) 42 | new_data['doc_url'] = new_data.get('pdf_url') 43 | new_data['url'] = new_data.get('splash_url') 44 | new_data['tags'] = new_data.get('keywords') 45 | 46 | new_data = {key: new_data[key] for key in new_data if new_data[key]} 47 | result.append(new_data) 48 | return result 49 | 50 | 51 | def get_data(query=None): 52 | """ 53 | Get data using the dissemin API 54 | https://dissem.in/api/search/?q=pregroup 55 | """ 56 | dict_params = {"q": query} 57 | params = urllib.parse.urlencode(dict_params) 58 | main_url = "https://dissem.in/api/search/?" 59 | req_url = main_url + params 60 | logger.debug("url = " + req_url) 61 | url = urllib.request.Request( 62 | req_url, 63 | headers={ 64 | 'User-Agent': papis.config.get('user-agent') 65 | } 66 | ) 67 | jsondoc = urllib.request.urlopen(url).read().decode() 68 | paperlist = json.loads(jsondoc) 69 | return sum([dissemindoc_to_papis(d) for d in paperlist['papers']], []) 70 | 71 | 72 | @click.command('dissemin') 73 | @click.pass_context 74 | @click.help_option('--help', '-h') 75 | @click.option('--query', '-q', default=None) 76 | def explorer(ctx, query): 77 | """ 78 | Look for documents on dissem.in 79 | 80 | Examples of its usage are 81 | 82 | papis explore dissemin -q 'Albert einstein' pick cmd 'firefox {doc[url]}' 83 | 84 | """ 85 | logger = logging.getLogger('explore:dissemin') 86 | logger.info('Looking up...') 87 | data = get_data(query=query) 88 | docs = [papis.document.from_data(data=d) for d in data] 89 | ctx.obj['documents'] += docs 90 | logger.info('{} documents found'.format(len(docs))) 91 | -------------------------------------------------------------------------------- /papis/downloaders/annualreviews.py: -------------------------------------------------------------------------------- 1 | import re 2 | import papis.downloaders.base 3 | 4 | 5 | class Downloader(papis.downloaders.base.Downloader): 6 | 7 | def __init__(self, url): 8 | papis.downloaders.base.Downloader.__init__( 9 | self, url, name="annualreviews" 10 | ) 11 | self.expected_document_extension = 'pdf' 12 | self.priority = 10 13 | 14 | @classmethod 15 | def match(cls, url): 16 | if re.match(r".*annualreviews.org.*", url): 17 | return Downloader(url) 18 | else: 19 | return False 20 | 21 | def get_document_url(self): 22 | if 'doi' in self.ctx.data: 23 | doi = self.ctx.data['doi'] 24 | url = "http://annualreviews.org/doi/pdf/%s" % doi 25 | self.logger.debug("doc url = %s" % url) 26 | return url 27 | 28 | def get_bibtex_url(self): 29 | if 'doi' in self.ctx.data: 30 | url = ("http://annualreviews.org/action/downloadCitation" 31 | "?format=bibtex&cookieSet=1&doi=%s" % self.ctx.data['doi']) 32 | self.logger.debug("bibtex url = %s" % url) 33 | return url 34 | 35 | def get_data(self): 36 | data = dict() 37 | soup = self._get_soup() 38 | data.update(papis.downloaders.base.parse_meta_headers(soup)) 39 | 40 | if 'author_list' in data: 41 | return data 42 | 43 | # Read brute force the authors from the source 44 | author_list = [] 45 | authors = soup.find_all(name='span', attrs={'class': 'contribDegrees'}) 46 | cleanregex = re.compile(r'(^\s*|\s*$|&)') 47 | editorregex = re.compile(r'([\n|]|\(Reviewing\s*Editor\))') 48 | morespace = re.compile(r'\s+') 49 | for author in authors: 50 | affspan = author.find_all('span', attrs={'class': 'overlay'}) 51 | afftext = affspan[0].text if affspan else '' 52 | fullname = re.sub(',', '', 53 | cleanregex.sub('', 54 | author.text.replace(afftext, ''))) 55 | splitted = re.split(r'\s+', fullname) 56 | cafftext = re.sub(' ,', ',', 57 | morespace.sub(' ', cleanregex.sub('', afftext))) 58 | if 'Reviewing Editor' in fullname: 59 | data['editor'] = cleanregex.sub( 60 | ' ', editorregex.sub('', fullname)) 61 | continue 62 | given = splitted[0] 63 | family = ' '.join(splitted[1:]) 64 | author_list.append( 65 | dict( 66 | given=given, 67 | family=family, 68 | affiliation=[dict(name=cafftext)] if cafftext else [] 69 | ) 70 | ) 71 | 72 | data['author_list'] = author_list 73 | data['author'] = papis.document.author_list_to_author(data) 74 | 75 | return data 76 | -------------------------------------------------------------------------------- /papis/downloaders/aps.py: -------------------------------------------------------------------------------- 1 | import re 2 | import papis.downloaders.fallback 3 | 4 | 5 | class Downloader(papis.downloaders.fallback.Downloader): 6 | 7 | def __init__(self, url): 8 | papis.downloaders.fallback.Downloader.__init__(self, url, name="aps") 9 | self.expected_document_extension = 'pdf' 10 | self.priority = 10 11 | 12 | @classmethod 13 | def match(cls, url): 14 | if re.match(r".*aps.org.*", url): 15 | return Downloader(url) 16 | else: 17 | return False 18 | 19 | def get_bibtex_url(self): 20 | url = self.uri 21 | burl = re.sub(r'/abstract', r'/export', url)\ 22 | + "?type=bibtex&download=true" 23 | self.logger.debug("[bibtex url] = %s" % burl) 24 | return burl 25 | -------------------------------------------------------------------------------- /papis/downloaders/fallback.py: -------------------------------------------------------------------------------- 1 | import doi 2 | import papis.downloaders.base 3 | 4 | 5 | class Downloader(papis.downloaders.base.Downloader): 6 | 7 | def __init__(self, url, name="fallback"): 8 | papis.downloaders.base.Downloader.__init__(self, url, name=name) 9 | self.priority = -1 10 | 11 | @classmethod 12 | def match(cls, url): 13 | return Downloader(url) 14 | 15 | def get_data(self): 16 | data = dict() 17 | soup = self._get_soup() 18 | data.update(papis.downloaders.base.parse_meta_headers(soup)) 19 | return data 20 | 21 | def get_doi(self): 22 | if 'doi' in self.ctx.data: 23 | return self.ctx.data['doi'] 24 | soup = self._get_soup() 25 | self.logger.info('trying to parse doi...') 26 | return doi.find_doi_in_text(str(soup)) 27 | 28 | def get_document_url(self): 29 | if 'pdf_url' in self.ctx.data: 30 | url = self.ctx.data.get('pdf_url') 31 | self.logger.debug("got a pdf url = %s" % url) 32 | return url 33 | -------------------------------------------------------------------------------- /papis/downloaders/frontiersin.py: -------------------------------------------------------------------------------- 1 | import re 2 | import papis.downloaders.base 3 | 4 | 5 | class Downloader(papis.downloaders.base.Downloader): 6 | 7 | def __init__(self, url): 8 | papis.downloaders.base.Downloader.__init__( 9 | self, url, name="frontiersin" 10 | ) 11 | self.expected_document_extension = 'pdf' 12 | self.cookies = { 13 | 'gdpr': 'true', 14 | } 15 | 16 | @classmethod 17 | def match(cls, url): 18 | if re.match(r".*frontiersin.org.*", url): 19 | return Downloader(url) 20 | else: 21 | return False 22 | 23 | def get_doi(self): 24 | url = self.uri 25 | self.logger.info('Parsing doi from {0}'.format(url)) 26 | mdoi = re.match(r'.*/articles/([^/]+/[^/?&%^$]+).*', url) 27 | if mdoi: 28 | doi = mdoi.group(1) 29 | return doi 30 | return None 31 | 32 | def get_document_url(self): 33 | durl = "https://www.frontiersin.org/articles/{doi}/pdf".format( 34 | doi=self.get_doi()) 35 | self.logger.debug("[doc url] = %s" % durl) 36 | return durl 37 | 38 | def get_bibtex_url(self): 39 | url = "https://www.frontiersin.org/articles/{doi}/bibTex".format( 40 | doi=self.get_doi()) 41 | self.logger.debug("[bibtex url] = %s" % url) 42 | return url 43 | -------------------------------------------------------------------------------- /papis/downloaders/get.py: -------------------------------------------------------------------------------- 1 | import re 2 | import papis.downloaders.base 3 | 4 | 5 | class Downloader(papis.downloaders.base.Downloader): 6 | def __init__(self, url): 7 | papis.downloaders.base.Downloader.__init__(self, url, name="get") 8 | self.priority = 0 9 | 10 | @classmethod 11 | def match(cls, url): 12 | """ 13 | >>> Downloader.match('http://wha2341!@#!@$%!@#file.pdf') is False 14 | False 15 | >>> Downloader.match('https://whateverpt?is?therefile.epub') is False 16 | False 17 | >>> not Downloader.match('http://whatever?path?is?therefile') 18 | True 19 | """ 20 | endings = "pdf|djvu|epub|mobi|jpg|png|md" 21 | m = re.match(r"^http.*\.(%s)$" % endings, url, re.IGNORECASE) 22 | if m: 23 | d = Downloader(url) 24 | extension = m.group(1) 25 | d.logger.info( 26 | 'Expecting a document of type "{0}"'.format(extension)) 27 | d.expected_document_extension = extension 28 | return d 29 | else: 30 | return None 31 | 32 | def get_document_url(self): 33 | return self.uri 34 | -------------------------------------------------------------------------------- /papis/downloaders/hal.py: -------------------------------------------------------------------------------- 1 | import re 2 | import papis.downloaders.fallback 3 | 4 | 5 | class Downloader(papis.downloaders.fallback.Downloader): 6 | 7 | def __init__(self, url): 8 | papis.downloaders.fallback.Downloader.__init__(self, url, name="hal") 9 | self.expected_document_extension = 'pdf' 10 | self.priority = 10 11 | 12 | @classmethod 13 | def match(cls, url): 14 | if re.match(r".*hal\.archives-ouvertes\.fr.*", url): 15 | return Downloader(url) 16 | else: 17 | return False 18 | 19 | def get_bibtex_url(self): 20 | if 'pdf_url' in self.ctx.data: 21 | url = re.sub(r'document', 'bibtex', self.uri) 22 | self.logger.debug('bibtex url = {url}'.format(url=url)) 23 | return url 24 | -------------------------------------------------------------------------------- /papis/downloaders/ieee.py: -------------------------------------------------------------------------------- 1 | import re 2 | import urllib.parse 3 | import urllib.request 4 | import papis.downloaders.base 5 | 6 | 7 | class Downloader(papis.downloaders.base.Downloader): 8 | 9 | def __init__(self, url): 10 | papis.downloaders.base.Downloader.__init__(self, url, name="ieee") 11 | self.expected_document_extension = 'pdf' 12 | 13 | @classmethod 14 | def match(cls, url): 15 | m = re.match(r"^ieee:(.*)", url, re.IGNORECASE) 16 | if m: 17 | url = "http://ieeexplore.ieee.org/document/{m}".format( 18 | m=m.group(1)) 19 | return Downloader(url) 20 | if re.match(r".*ieee.org.*", url): 21 | url = re.sub(r"\.pdf.*$", "", url) 22 | return Downloader(url) 23 | else: 24 | return False 25 | 26 | def get_identifier(self): 27 | url = self.uri 28 | return re.sub(r'^.*ieeexplore\.ieee\.org/document/(.*)\/', r'\1', url) 29 | 30 | def get_bibtex_url(self): 31 | identifier = self.get_identifier() 32 | bibtex_url = \ 33 | 'http://ieeexplore.ieee.org/xpl/downloadCitations?reload=true' 34 | data = { 35 | 'recordIds': identifier, 36 | 'citations-format': 'citation-and-abstract', 37 | 'download-format': 'download-bibtex', 38 | 'x': '0', 39 | 'y': '0' 40 | } 41 | return bibtex_url, data 42 | 43 | def download_bibtex(self): 44 | bib_url, values = self.get_bibtex_url() 45 | post_data = urllib.parse.urlencode(values) 46 | post_data = post_data.encode('ascii') 47 | 48 | self.logger.debug("[bibtex url] = %s" % bib_url) 49 | 50 | req = urllib.request.Request(bib_url, post_data) 51 | with urllib.request.urlopen(req) as response: 52 | data = response.read() 53 | text = data.decode('utf-8') 54 | text = text.replace('
', '') 55 | self.bibtex_data = text 56 | 57 | def get_document_url(self): 58 | identifier = self.get_identifier() 59 | self.logger.debug("[paper id] = %s" % identifier) 60 | pdf_url = "{}{}".format( 61 | "http://ieeexplore.ieee.org/" 62 | "stampPDF/getPDF.jsp?tp=&isnumber=&arnumber=", 63 | identifier 64 | ) 65 | self.logger.debug("[pdf url] = %s" % pdf_url) 66 | return pdf_url 67 | -------------------------------------------------------------------------------- /papis/downloaders/iopscience.py: -------------------------------------------------------------------------------- 1 | import re 2 | import papis.downloaders.base 3 | 4 | 5 | class Downloader(papis.downloaders.base.Downloader): 6 | 7 | def __init__(self, url): 8 | papis.downloaders.base.Downloader.__init__( 9 | self, url, name="iopscience" 10 | ) 11 | self.expected_document_extension = 'pdf' 12 | self.priority = 10 13 | 14 | @classmethod 15 | def match(cls, url): 16 | url = re.sub(r'/pdf', '', url) 17 | if re.match(r".*iopscience\.iop\.org.*", url): 18 | return Downloader(url) 19 | else: 20 | return False 21 | 22 | def get_doi(self): 23 | return self.ctx.data.get('doi') 24 | 25 | def get_document_url(self): 26 | if 'pdf_url' in self.ctx.data: 27 | return self.ctx.data.get('pdf_url') 28 | doi = self.get_doi() 29 | if doi: 30 | durl = 'https://iopscience.iop.org/article/{0}/pdf'.format(doi) 31 | self.logger.debug("doc url = %s" % durl) 32 | return durl 33 | 34 | def _get_article_id(self): 35 | """Get article's id for IOP 36 | :returns: Article id 37 | """ 38 | doi = self.get_doi() 39 | if doi: 40 | articleId = doi.replace('10.1088/', '') 41 | self.logger.debug("articleId = %s" % articleId) 42 | return articleId 43 | 44 | def get_bibtex_url(self): 45 | aid = self._get_article_id() 46 | if aid: 47 | url = "{0}{1}{2}".format( 48 | "http://iopscience.iop.org/export?aid=", 49 | aid, 50 | "&exportFormat=iopexport_bib&exportType=abs" 51 | "&navsubmit=Export%2Babstract" 52 | ) 53 | self.logger.debug("bibtex url = %s" % url) 54 | return url 55 | 56 | def get_data(self): 57 | data = dict() 58 | soup = self._get_soup() 59 | data.update(papis.downloaders.base.parse_meta_headers(soup)) 60 | abstract_nodes = soup.find_all( 61 | 'div', attrs={'class': 'wd-jnl-art-abstract'}) 62 | if abstract_nodes: 63 | data['abstract'] = ' '.join(a.text for a in abstract_nodes) 64 | return data 65 | -------------------------------------------------------------------------------- /papis/downloaders/scitationaip.py: -------------------------------------------------------------------------------- 1 | import re 2 | import papis.downloaders.base 3 | 4 | 5 | class Downloader(papis.downloaders.base.Downloader): 6 | 7 | def __init__(self, url): 8 | papis.downloaders.base.Downloader.__init__( 9 | self, url, name="scitationaip" 10 | ) 11 | self.expected_document_extension = 'pdf' 12 | 13 | @classmethod 14 | def match(cls, url): 15 | # http://aip.scitation.org/doi/10.1063/1.4873138 16 | if re.match(r".*(aip|aapt)\.scitation\.org.*", url): 17 | return Downloader(url) 18 | else: 19 | return False 20 | 21 | def get_doi(self): 22 | mdoi = re.match(r'.*/doi/(.*/[^?&%^$]*).*', self.uri) 23 | if mdoi: 24 | doi = mdoi.group(1).replace("abs/", "").replace("full/", "") 25 | return doi 26 | else: 27 | return None 28 | 29 | def get_document_url(self): 30 | # http://aip.scitation.org/doi/pdf/10.1063/1.4873138 31 | durl = "http://aip.scitation.org/doi/pdf/%s" % self.get_doi() 32 | self.logger.debug("[doc url] = %s" % durl) 33 | return durl 34 | 35 | def get_bibtex_url(self): 36 | url = "http://aip.scitation.org/action/downloadCitation"\ 37 | "?format=bibtex&cookieSet=1&doi=%s" % self.get_doi() 38 | self.logger.debug("[bibtex url] = %s" % url) 39 | return url 40 | -------------------------------------------------------------------------------- /papis/downloaders/springer.py: -------------------------------------------------------------------------------- 1 | import re 2 | import papis.downloaders.base 3 | import papis.document 4 | 5 | 6 | class Downloader(papis.downloaders.base.Downloader): 7 | 8 | def __init__(self, url): 9 | papis.downloaders.base.Downloader.__init__( 10 | self, url, name="springer") 11 | self.expected_document_extension = 'pdf' 12 | self.priority = 10 13 | 14 | @classmethod 15 | def match(cls, url): 16 | return (Downloader(url) 17 | if re.match(r".*link\.springer.com.*", url) else False) 18 | 19 | def get_data(self): 20 | data = dict() 21 | soup = self._get_soup() 22 | metas = soup.find_all(name="meta") 23 | author_list = [] 24 | for meta in metas: 25 | if meta.attrs.get('name') == 'citation_title': 26 | data['title'] = meta.attrs.get('content') 27 | elif meta.attrs.get('name') == 'dc.Subject': 28 | data['subject'] = meta.attrs.get('content') 29 | elif meta.attrs.get('name') == 'citation_doi': 30 | data['doi'] = meta.attrs.get('content') 31 | elif meta.attrs.get('name') == 'citation_publisher': 32 | data['publisher'] = meta.attrs.get('content') 33 | elif meta.attrs.get('name') == 'citation_journal_title': 34 | data['journal'] = meta.attrs.get('content') 35 | elif meta.attrs.get('name') == 'citation_issn': 36 | data['issn'] = meta.attrs.get('content') 37 | elif meta.attrs.get('name') == 'citation_author': 38 | fnam = meta.attrs.get('content') 39 | fnams = re.split('\s+', fnam) 40 | author_list.append( 41 | dict( 42 | given=fnams[0], 43 | family=' '.join(fnams[1:]), 44 | affiliation=[])) 45 | elif meta.attrs.get('name') == 'citation_author_institution': 46 | if not author_list: 47 | continue 48 | author_list[-1]['affiliation'].append( 49 | dict(name=meta.attrs.get('content'))) 50 | 51 | data['author_list'] = author_list 52 | data['author'] = papis.document.author_list_to_author(data) 53 | 54 | return data 55 | 56 | def get_bibtex_url(self): 57 | if 'doi' in self.ctx.data: 58 | url = ( 59 | "http://citation-needed.springer.com/v2/" 60 | "references/{doi}?format=bibtex&flavour=citation" 61 | .format(doi=self.ctx.data['doi'])) 62 | self.logger.debug("bibtex url = %s" % url) 63 | return url 64 | 65 | def get_document_url(self): 66 | if 'doi' in self.ctx.data: 67 | url = ( 68 | "https://link.springer.com/content/pdf/" 69 | "{doi}.pdf".format(doi=self.ctx.data['doi'])) 70 | self.logger.debug("doc url = %s" % url) 71 | return url 72 | -------------------------------------------------------------------------------- /papis/downloaders/tandfonline.py: -------------------------------------------------------------------------------- 1 | import re 2 | import urllib.parse 3 | 4 | import papis.downloaders.base 5 | import papis.document 6 | 7 | 8 | class Downloader(papis.downloaders.base.Downloader): 9 | re_clean = re.compile(r'(^\s*|\s*$|\s{2,}?|&)') 10 | re_comma = re.compile(r'(\s*,\s*)') 11 | re_add_dot = re.compile(r'(\b\w\b)') 12 | 13 | def __init__(self, url): 14 | papis.downloaders.base.Downloader.__init__( 15 | self, url, name="tandfonline") 16 | self.expected_document_extension = 'pdf' 17 | self.priority = 10 18 | 19 | @classmethod 20 | def match(cls, url): 21 | return (Downloader(url) 22 | if re.match(r".*tandfonline.com.*", url) else False) 23 | 24 | def get_data(self): 25 | data = dict() 26 | soup = self._get_soup() 27 | data.update(papis.downloaders.base.parse_meta_headers(soup)) 28 | 29 | # `author` and `author_list` are already in meta_headers, but we 30 | # brute-force them here again to get exact affiliation information 31 | author_list = [] 32 | authors = soup.find_all(name='span', attrs={'class': 'contribDegrees'}) 33 | for author in authors: 34 | affiliation = author.find_all('span', attrs={'class': 'overlay'}) 35 | if affiliation: 36 | # the span contains other things like the email, but we only 37 | # want the starting text with the affiliation address 38 | affiliation = next(affiliation[0].children) 39 | 40 | affiliation = self.re_comma.sub(', ', 41 | self.re_clean.sub('', affiliation)) 42 | affiliation = [dict(name=affiliation)] 43 | 44 | # find href="/author/escaped_fullname" 45 | fullname = author.find_all('a', attrs={'class': 'entryAuthor'}) 46 | fullname = fullname[0].get('href').split('/')[-1] 47 | 48 | fullname = urllib.parse.unquote_plus(fullname) 49 | family, given = re.split(r',\s+', fullname) 50 | given = self.re_add_dot.sub(r'\1.', given) 51 | 52 | if 'Reviewing Editor' in author.text: 53 | data['editor'] = \ 54 | papis.config.get('multiple-authors-format').format( 55 | au=dict(family=family, given=given)) 56 | continue 57 | 58 | new_author = dict(given=given, family=family) 59 | if affiliation: 60 | new_author['affiliation'] = affiliation 61 | author_list.append(new_author) 62 | 63 | data['author_list'] = author_list 64 | data['author'] = papis.document.author_list_to_author(data) 65 | 66 | return data 67 | 68 | def get_bibtex_url(self): 69 | if 'doi' in self.ctx.data: 70 | url = ( 71 | "http://www.tandfonline.com/action/downloadCitation" 72 | "?format=bibtex&cookieSet=1&doi=%s" % self.ctx.data['doi']) 73 | self.logger.debug("bibtex url = %s" % url) 74 | return url 75 | 76 | def get_document_url(self): 77 | if 'doi' in self.ctx.data: 78 | durl = ( 79 | "http://www.tandfonline.com/doi/pdf/{doi}" 80 | .format(doi=self.ctx.data['doi'])) 81 | self.logger.debug("doc url = %s" % durl) 82 | return durl 83 | -------------------------------------------------------------------------------- /papis/downloaders/thesesfr.py: -------------------------------------------------------------------------------- 1 | import re 2 | import papis.downloaders.base 3 | import bs4 4 | 5 | 6 | class Downloader(papis.downloaders.base.Downloader): 7 | 8 | def __init__(self, url): 9 | papis.downloaders.base.Downloader.__init__(self, url, name="thesesfr") 10 | self.expected_document_extension = 'pdf' 11 | 12 | @classmethod 13 | def match(cls, url): 14 | if re.match(r".*theses.fr.*", url): 15 | return Downloader(url) 16 | else: 17 | return False 18 | 19 | def get_identifier(self): 20 | """ 21 | >>> d = Downloader("http://www.theses.fr/2014TOU30305") 22 | >>> d.get_identifier() 23 | '2014TOU30305' 24 | >>> d = Downloader("http://www.theses.fr/2014TOU30305.bib/?asdf=2") 25 | >>> d.get_identifier() 26 | '2014TOU30305' 27 | """ 28 | m = re.match(r".*theses.fr/([^/?.&]+).*", self.uri) 29 | return m.group(1) if m is not None else None 30 | 31 | def get_document_url(self): 32 | """ 33 | >>> d = Downloader("http://www.theses.fr/2014TOU30305") 34 | >>> d.get_document_url() 35 | 'http://thesesups.ups-tlse.fr/2722/1/2014TOU30305.pdf' 36 | >>> d = Downloader("http://theses.fr/1998ENPC9815") 37 | >>> d.get_document_url() 38 | """ 39 | raw_data = self.session.get(self.uri).content.decode('utf-8') 40 | soup = bs4.BeautifulSoup(raw_data, "html.parser") 41 | a = list(filter( 42 | lambda t: re.match(r'.*en ligne.*', t.text), 43 | soup.find_all('a') 44 | )) 45 | 46 | if not a: 47 | self.logger.error('No document url in theses.fr') 48 | return None 49 | 50 | second_url = a[0]['href'] 51 | raw_data = self.session.get(second_url).content.decode('utf-8') 52 | soup = bs4.BeautifulSoup(raw_data, "html.parser") 53 | a = list(filter( 54 | lambda t: re.match(r'.*pdf$', t['href']), 55 | soup.find_all('a') 56 | )) 57 | 58 | if not a: 59 | self.logger.error('No document url in {0}'.format(second_url)) 60 | return None 61 | 62 | return a[0]['href'] 63 | 64 | def get_bibtex_url(self): 65 | """ 66 | >>> d = Downloader("http://www.theses.fr/2014TOU30305") 67 | >>> d.get_bibtex_url() 68 | 'http://www.theses.fr/2014TOU30305.bib' 69 | """ 70 | url = "http://www.theses.fr/{id}.bib".format(id=self.get_identifier()) 71 | self.logger.debug("[bibtex url] = %s" % url) 72 | return url 73 | -------------------------------------------------------------------------------- /papis/downloaders/worldscientific.py: -------------------------------------------------------------------------------- 1 | import re 2 | import papis.downloaders.base 3 | 4 | 5 | class Downloader(papis.downloaders.base.Downloader): 6 | 7 | def __init__(self, url): 8 | papis.downloaders.base.Downloader.__init__( 9 | self, url, name="worldscientific" 10 | ) 11 | self.expected_document_extension = 'pdf' 12 | self.cookies = { 13 | 'gdpr': 'true', 14 | } 15 | 16 | @classmethod 17 | def match(cls, url): 18 | if re.match(r".*worldscientific.com.*", url): 19 | return Downloader(url) 20 | else: 21 | return False 22 | 23 | def get_doi(self): 24 | url = self.uri 25 | self.logger.debug('Parsing doi from {0}'.format(url)) 26 | mdoi = re.match(r'.*/doi/(.*/[^?&%^$]*).*', url) 27 | if mdoi: 28 | doi = mdoi.group(1).replace("abs/", "").replace("full/", "") 29 | return doi 30 | 31 | mdoi = re.match(r'.*/worldscibooks/(.*/[^?&%^$]*).*', url) 32 | if mdoi: 33 | doi = mdoi.group(1).replace("abs/", "").replace("full/", "") 34 | return doi 35 | 36 | return None 37 | 38 | def get_document_url(self): 39 | durl = "https://www.worldscientific.com/doi/pdf/%s" % self.get_doi() 40 | self.logger.debug("[doc url] = %s" % durl) 41 | return durl 42 | 43 | def get_bibtex_url(self): 44 | url = "https://www.worldscientific.com/action/downloadCitation"\ 45 | "?format=bibtex&cookieSet=1&doi=%s" % self.get_doi() 46 | self.logger.debug("[bibtex url] = %s" % url) 47 | return url 48 | -------------------------------------------------------------------------------- /papis/exceptions.py: -------------------------------------------------------------------------------- 1 | """This module implements custom exceptions used to make the code more 2 | readable. 3 | """ 4 | 5 | 6 | class DefaultSettingValueMissing(Exception): 7 | """This exception is when a setting's value has no default value. 8 | """ 9 | 10 | def __init__(self, key): 11 | message = """ 12 | 13 | The configuration setting '{0}' is not defined. 14 | Try setting its value in your configuration file as such: 15 | 16 | [settings] 17 | {0} = some-value 18 | 19 | Don't forget to check the documentation. 20 | """.format(key) 21 | super(DefaultSettingValueMissing, self).__init__(message) 22 | -------------------------------------------------------------------------------- /papis/git.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | import shlex 4 | import logging 5 | 6 | logger = logging.getLogger("papis.git") 7 | 8 | 9 | def _issue_git_command(path, cmd): 10 | """Issues a general git command ``cmd`` at ``path``. 11 | 12 | :param path: Folder where a git repo exists. 13 | :type path: str 14 | :param message: Git command 15 | :type message: str 16 | :returns: None 17 | 18 | """ 19 | global logger 20 | assert(type(cmd) == str) 21 | path = os.path.expanduser(path) 22 | assert(os.path.exists(path)) 23 | cmd = shlex.split(cmd) 24 | os.chdir(path) 25 | logger.debug(cmd) 26 | subprocess.call(cmd) 27 | 28 | 29 | def commit(path, message): 30 | """Commits changes in the path with a message. 31 | 32 | :param path: Folder where a git repo exists. 33 | :type path: str 34 | :param message: Commit message 35 | :type message: str 36 | :returns: None 37 | 38 | """ 39 | global logger 40 | logger.info('Commiting {path} with message {message}'.format(**locals())) 41 | cmd = 'git commit -m "{0}"'.format(message) 42 | _issue_git_command(path, cmd) 43 | 44 | 45 | def add(path, resource): 46 | """Adds changes in the path with a message. 47 | 48 | :param path: Folder where a git repo exists. 49 | :type path: str 50 | :param resource: Commit resource 51 | :type resource: str 52 | :returns: None 53 | 54 | """ 55 | global logger 56 | logger.info('Adding {path}'.format(**locals())) 57 | cmd = 'git add "{0}"'.format(resource) 58 | _issue_git_command(path, cmd) 59 | 60 | 61 | def rm(path, resource, recursive=False): 62 | """Adds changes in the path with a message. 63 | 64 | :param path: Folder where a git repo exists. 65 | :type path: str 66 | :param resource: Commit resource 67 | :type resource: str 68 | :returns: None 69 | 70 | """ 71 | global logger 72 | logger.info('Removing {path}'.format(**locals())) 73 | # force removal always 74 | cmd = 'git rm -f {r} "{0}"'.format(resource, r="-r" if recursive else "") 75 | _issue_git_command(path, cmd) 76 | 77 | 78 | 79 | def add_and_commit_resource(path, resource, message): 80 | add(path, resource) 81 | commit(path, message) 82 | -------------------------------------------------------------------------------- /papis/isbn.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import isbnlib 3 | import click 4 | import papis.document 5 | # See https://github.com/xlcnd/isbnlib for details 6 | 7 | logger = logging.getLogger('papis:isbnlib') 8 | 9 | 10 | def get_data(query="", service=None): 11 | global logger 12 | results = [] 13 | logger.debug('Trying to retrieve isbn') 14 | isbn = isbnlib.isbn_from_words(query) 15 | data = isbnlib.meta(isbn, service=service) 16 | if data is None: 17 | return results 18 | else: 19 | logger.debug('Trying to retrieve isbn') 20 | assert(isinstance(data, dict)) 21 | results.append(data_to_papis(data)) 22 | return results 23 | 24 | 25 | def data_to_papis(data): 26 | """Convert data from isbnlib into papis formated data 27 | 28 | :param data: Dictionary with data 29 | :type data: dict 30 | :returns: Dictionary with papis keynames 31 | 32 | """ 33 | data = {k.lower(): data[k] for k in data} 34 | return data 35 | 36 | 37 | @click.command('isbn') 38 | @click.pass_context 39 | @click.help_option('--help', '-h') 40 | @click.option('--query', '-q', default=None) 41 | @click.option( 42 | '--service', 43 | '-s', 44 | default='goob', 45 | type=click.Choice(['wcat', 'goob', 'openl']) 46 | ) 47 | def explorer(ctx, query, service): 48 | """ 49 | Look for documents using isbnlib 50 | 51 | Examples of its usage are 52 | 53 | papis explore isbn -q 'Albert einstein' pick cmd 'firefox {doc[url]}' 54 | 55 | """ 56 | logger = logging.getLogger('explore:isbn') 57 | logger.info('Looking up...') 58 | data = get_data( 59 | query=query, 60 | service=service, 61 | ) 62 | docs = [papis.document.from_data(data=d) for d in data] 63 | logger.info('{} documents found'.format(len(docs))) 64 | ctx.obj['documents'] += docs 65 | -------------------------------------------------------------------------------- /papis/json.py: -------------------------------------------------------------------------------- 1 | import click 2 | import papis.document 3 | import json 4 | import logging 5 | 6 | 7 | @click.command('json') 8 | @click.pass_context 9 | @click.argument('jsonfile', type=click.Path(exists=True)) 10 | @click.help_option('--help', '-h') 11 | def explorer(ctx, jsonfile): 12 | """ 13 | Import documents from a json file 14 | 15 | Examples of its usage are 16 | 17 | papis explore json lib.json pick 18 | 19 | """ 20 | logger = logging.getLogger('explore:json') 21 | logger.info('Reading in json file {}'.format(jsonfile)) 22 | docs = [ 23 | papis.document.from_data(d) for d in json.load(open(jsonfile)) 24 | ] 25 | ctx.obj['documents'] += docs 26 | logger.info('{} documents found'.format(len(docs))) 27 | -------------------------------------------------------------------------------- /papis/library.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | 4 | 5 | class Library: 6 | 7 | def __init__(self, name, paths): 8 | assert(isinstance(name, str)), '`name` must be a string' 9 | assert(isinstance(paths, list)), '`paths` must be a list' 10 | self.name = name 11 | self.paths = sum( 12 | [glob.glob(os.path.expanduser(p)) for p in paths], 13 | [] 14 | ) 15 | 16 | def path_format(self): 17 | return ":".join(self.paths) 18 | 19 | def __str__(self): 20 | return self.name 21 | 22 | 23 | def from_paths(paths): 24 | name = ":".join(paths) 25 | return Library(name, paths) 26 | -------------------------------------------------------------------------------- /papis/pick.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import papis.config 5 | from papis.tui.app import Picker 6 | from stevedore import extension 7 | import papis.plugin 8 | 9 | logger = logging.getLogger("pick") 10 | 11 | 12 | def available_pickers(): 13 | return pickers_mgr.entry_points_names() 14 | 15 | 16 | def papis_pick( 17 | options, default_index=0, 18 | header_filter=lambda x: x, match_filter=lambda x: x 19 | ): 20 | if len(options) == 0: 21 | return "" 22 | if len(options) == 1: 23 | return options[0] 24 | 25 | # patch stdout to stderr if the output is not a tty (terminal) 26 | oldstdout = sys.stdout 27 | if not sys.stdout.isatty(): 28 | sys.stdout = sys.stderr 29 | sys.__stdout__ = sys.stderr 30 | 31 | picker = Picker( 32 | options, 33 | default_index, 34 | header_filter, 35 | match_filter 36 | ) 37 | picker.run() 38 | result = picker.options_list.get_selection() 39 | 40 | # restore the stdout to normality 41 | sys.stdout = oldstdout 42 | sys.__stdout__ = oldstdout 43 | 44 | return result 45 | 46 | 47 | pickers_mgr = extension.ExtensionManager( 48 | namespace='papis.picker', 49 | invoke_on_load=False, 50 | verify_requirements=True, 51 | propagate_map_exceptions=True, 52 | on_load_failure_callback=papis.plugin.stevedore_error_handler 53 | ) 54 | 55 | 56 | def pick( 57 | options, 58 | default_index=0, 59 | header_filter=lambda x: x, 60 | match_filter=lambda x: x 61 | ): 62 | """Construct and start a :class:`Picker `. 63 | """ 64 | name = papis.config.get("picktool") 65 | try: 66 | picker = pickers_mgr[name].plugin 67 | except KeyError: 68 | logger.error("Invalid picker ({0})".format(name)) 69 | logger.error( 70 | "Registered pickers are: {0}".format(available_pickers())) 71 | else: 72 | return picker( 73 | options, 74 | default_index=default_index, 75 | header_filter=header_filter, 76 | match_filter=match_filter 77 | ) 78 | 79 | 80 | def pick_doc(documents): 81 | """Pick a document from documents with the correct formatting 82 | 83 | :documents: List of documents 84 | :returns: Document 85 | 86 | """ 87 | header_format_path = papis.config.get('header-format-file') 88 | if header_format_path is not None: 89 | with open(os.path.expanduser(header_format_path)) as fd: 90 | header_format = fd.read() 91 | else: 92 | header_format = papis.config.get("header-format") 93 | match_format = papis.config.get("match-format") 94 | pick_config = dict( 95 | header_filter=lambda x: papis.utils.format_doc(header_format, x), 96 | match_filter=lambda x: papis.utils.format_doc(match_format, x) 97 | ) 98 | return pick(documents, **pick_config) 99 | -------------------------------------------------------------------------------- /papis/plugin.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger("plugin") 4 | 5 | 6 | def stevedore_error_handler(manager, entrypoint, exception): 7 | logger.error("Error while loading entrypoint [%s]" % entrypoint) 8 | logger.error(exception) 9 | -------------------------------------------------------------------------------- /papis/pubmed.py: -------------------------------------------------------------------------------- 1 | import papis.importer 2 | import papis.downloaders.base 3 | 4 | 5 | class Downloader(papis.downloaders.base.Downloader): 6 | 7 | def __init__(self, url): 8 | papis.downloaders.base.Downloader.__init__( 9 | self, uri=url, name="pubmed") 10 | 11 | def get_bibtex_url(self): 12 | return ( 13 | "http://pubmed.macropus.org/articles/" 14 | "?format=text%2Fbibtex&id={pmid}" 15 | .format(pmid=self.uri) 16 | ) 17 | 18 | 19 | class Importer(papis.importer.Importer): 20 | 21 | """Importer downloading data from a pubmed id""" 22 | 23 | def __init__(self, uri='', **kwargs): 24 | papis.importer.Importer.__init__( 25 | self, name='pubmed', uri=uri, **kwargs) 26 | self.downloader = Downloader(uri) 27 | 28 | @classmethod 29 | def match(cls, uri): 30 | # TODO: 31 | pass 32 | 33 | def fetch(self): 34 | self.downloader.fetch() 35 | self.ctx = self.downloader.ctx 36 | -------------------------------------------------------------------------------- /papis/strings.py: -------------------------------------------------------------------------------- 1 | no_documents_retrieved_message = "No documents retrieved" 2 | -------------------------------------------------------------------------------- /papis/tui/__init__.py: -------------------------------------------------------------------------------- 1 | def get_default_settings(): 2 | return dict(tui={ 3 | "status_line_format": ( 4 | "{selected_index}/{number_of_documents} " + 5 | "F1:help " + 6 | "c-l:redraw " 7 | ), 8 | 9 | "status_line_style": 'bg:ansiwhite fg:ansiblack', 10 | 'message_toolbar_style': 'bg:ansiyellow fg:ansiblack', 11 | 'options_list.selected_margin_style': 'bg:ansiblack fg:ansigreen', 12 | 'options_list.unselected_margin_style': 'bg:ansiwhite', 13 | 'error_toolbar_style': 'bg:ansired fg:ansiblack', 14 | 15 | 'move_down_key': 'down', 16 | 'move_up_key': 'up', 17 | 'move_down_while_info_window_active_key': 'c-n', 18 | 'move_up_while_info_window_active_key': 'c-p', 19 | 'focus_command_line_key': 'tab', 20 | 'edit_document_key': 'c-e', 21 | 'open_document_key': 'c-o', 22 | 'show_help_key': 'f1', 23 | 'show_info_key': 's-tab', 24 | 'go_top_key': 'home', 25 | 'go_bottom_key': 'end', 26 | 27 | "editmode": "emacs", 28 | }) 29 | -------------------------------------------------------------------------------- /papis/tui/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit.formatted_text.html import HTML 2 | from prompt_toolkit.filters import has_focus, Condition 3 | from prompt_toolkit.buffer import Buffer 4 | from prompt_toolkit.layout.containers import ( 5 | HSplit, Window, WindowAlign, ConditionalContainer 6 | ) 7 | from prompt_toolkit.layout import Dimension 8 | from prompt_toolkit.layout.controls import ( 9 | BufferControl, 10 | FormattedTextControl 11 | ) 12 | from prompt_toolkit.widgets import ( 13 | HorizontalLine 14 | ) 15 | from prompt_toolkit.lexers import PygmentsLexer 16 | from pygments.lexers import find_lexer_class_by_name 17 | import logging 18 | 19 | from .list import OptionsList 20 | from .command_line_prompt import CommandLinePrompt 21 | 22 | logger = logging.getLogger('pick') 23 | 24 | 25 | class MessageToolbar(ConditionalContainer): 26 | 27 | def __init__(self, style=""): 28 | self.message = None 29 | self.text_control = FormattedTextControl(text="") 30 | super(MessageToolbar, self).__init__( 31 | content=Window( 32 | style=style, content=self.text_control, 33 | height=1 34 | ), 35 | filter=Condition(lambda: self.text) 36 | ) 37 | 38 | @property 39 | def text(self): 40 | return self.text_control.text 41 | 42 | @text.setter 43 | def text(self, value): 44 | self.text_control.text = value 45 | 46 | 47 | class InfoWindow(ConditionalContainer): 48 | 49 | def __init__(self, lexer_name='yaml'): 50 | self.buf = Buffer() 51 | self.buf.text = '' 52 | self.lexer = PygmentsLexer(find_lexer_class_by_name(lexer_name)) 53 | self.window = HSplit([ 54 | HorizontalLine(), 55 | Window( 56 | content=BufferControl(buffer=self.buf, lexer=self.lexer) 57 | ) 58 | ], height=Dimension(min=5, max=20, weight=1)) 59 | super(InfoWindow, self).__init__( 60 | content=self.window, 61 | filter=has_focus(self) 62 | ) 63 | 64 | @property 65 | def text(self): 66 | return self.buf.text 67 | 68 | @text.setter 69 | def text(self, text): 70 | self.buf.text = text 71 | 72 | 73 | class HelpWindow(ConditionalContainer): 74 | 75 | def __init__(self): 76 | self.text_control = FormattedTextControl( 77 | text=HTML('') 78 | ) 79 | self.window = Window( 80 | content=self.text_control, 81 | always_hide_cursor=True, 82 | align=WindowAlign.LEFT 83 | ) 84 | super(HelpWindow, self).__init__( 85 | content=self.window, 86 | filter=has_focus(self.window) 87 | ) 88 | 89 | @property 90 | def text(self): 91 | return self.text_control.text 92 | 93 | @text.setter 94 | def text(self, value): 95 | self.text_control.text = value 96 | -------------------------------------------------------------------------------- /papis/tui/widgets/command_line_prompt.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit.layout.processors import BeforeInput 2 | from prompt_toolkit.buffer import Buffer 3 | from prompt_toolkit.completion import WordCompleter 4 | from prompt_toolkit.layout.containers import ( 5 | Window, ConditionalContainer 6 | ) 7 | from prompt_toolkit.layout.controls import ( 8 | BufferControl, 9 | ) 10 | from prompt_toolkit.application.current import get_app 11 | from prompt_toolkit.filters import has_focus 12 | import shlex 13 | 14 | 15 | class Command: 16 | """ 17 | :param name: Name of the command 18 | :type name: parameter_type 19 | :param run: A callable object where the first argument is the cmd itself. 20 | :type run: callable 21 | """ 22 | def __init__(self, name, run, aliases=[]): 23 | assert(isinstance(name, str)), 'name should be a string' 24 | assert(callable(run)), 'run should be callable' 25 | assert(isinstance(aliases, list)) 26 | self.name = name 27 | self.run = run 28 | self.aliases = aliases 29 | 30 | @property 31 | def app(self): 32 | return get_app() 33 | 34 | @property 35 | def names(self): 36 | return [self.name] + self.aliases 37 | 38 | def __call__(self, *args, **kwargs): 39 | return self.run(self, *args, **kwargs) 40 | 41 | 42 | class CommandLinePrompt(ConditionalContainer): 43 | """ 44 | A vim-like command line prompt widget. 45 | It's supposed to be instantiated only once. 46 | """ 47 | def __init__(self, commands=[]): 48 | assert(isinstance(commands, list)) 49 | for c in commands: 50 | assert(isinstance(c, Command)) 51 | self.commands = commands 52 | wc = WordCompleter(sum([c.names for c in commands], [])) 53 | self.buf = Buffer( 54 | completer=wc, complete_while_typing=True 55 | ) 56 | self.buf.text = '' 57 | self.window = Window( 58 | content=BufferControl( 59 | buffer=self.buf, 60 | input_processors=[BeforeInput(':')] 61 | ), 62 | height=1 63 | ) 64 | super(CommandLinePrompt, self).__init__( 65 | content=self.window, 66 | filter=has_focus(self.window) 67 | ) 68 | 69 | def trigger(self): 70 | input_cmd = shlex.split(self.buf.text) 71 | if not input_cmd: 72 | return 73 | name = input_cmd[0] 74 | cmds = list(filter(lambda c: name in c.names, self.commands)) 75 | 76 | if len(cmds) > 1: 77 | raise Exception('More than one command matches the input') 78 | elif len(cmds) == 0: 79 | raise Exception('No command found ({0})'.format(name)) 80 | 81 | input_cmd.pop(0) 82 | return cmds[0](*input_cmd) 83 | 84 | def clear(self): 85 | self.text = '' 86 | 87 | @property 88 | def text(self): 89 | return self.buf.text 90 | 91 | @text.setter 92 | def text(self, text): 93 | self.buf.text = text 94 | -------------------------------------------------------------------------------- /papis/yaml.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import logging 3 | import papis.config 4 | import papis.importer 5 | import click 6 | import papis.utils 7 | import os 8 | 9 | logger = logging.getLogger("yaml") 10 | 11 | 12 | def data_to_yaml(yaml_path, data): 13 | """ 14 | Save data to yaml at path outpath 15 | 16 | :param yaml_path: Path to a yaml file 17 | :type yaml_path: str 18 | :param data: Data in a dictionary 19 | :type data: dict 20 | """ 21 | with open(yaml_path, 'w+') as fd: 22 | yaml.dump( 23 | data, 24 | fd, 25 | allow_unicode=papis.config.getboolean("info-allow-unicode"), 26 | default_flow_style=False 27 | ) 28 | 29 | 30 | def yaml_to_data(yaml_path, raise_exception=False): 31 | """ 32 | Convert a yaml file into a dictionary using the yaml module. 33 | 34 | :param yaml_path: Path to a yaml file 35 | :type yaml_path: str 36 | :returns: Dictionary containing the info of the yaml file 37 | :rtype: dict 38 | :raises ValueError: If a yaml parsing error happens 39 | """ 40 | global logger 41 | with open(yaml_path) as fd: 42 | try: 43 | data = yaml.safe_load(fd) 44 | except Exception as e: 45 | if raise_exception: 46 | raise ValueError(e) 47 | logger.error("Yaml syntax error: \n\n{0}".format(e)) 48 | return dict() 49 | else: 50 | return data 51 | 52 | 53 | @click.command('yaml') 54 | @click.pass_context 55 | @click.argument('yamlfile', type=click.Path(exists=True)) 56 | @click.help_option('--help', '-h') 57 | def explorer(ctx, yamlfile): 58 | """ 59 | Import documents from a yaml file 60 | 61 | Examples of its usage are 62 | 63 | papis explore yaml lib.yaml pick 64 | 65 | """ 66 | logger = logging.getLogger('explore:yaml') 67 | logger.info('reading in yaml file {}'.format(yamlfile)) 68 | docs = [ 69 | papis.document.from_data(d) for d in yaml.safe_load_all(open(yamlfile)) 70 | ] 71 | ctx.obj['documents'] += docs 72 | logger.info('{} documents found'.format(len(docs))) 73 | 74 | 75 | class Importer(papis.importer.Importer): 76 | 77 | """Importer that parses a yaml file""" 78 | 79 | def __init__(self, **kwargs): 80 | papis.importer.Importer.__init__(self, name='yaml', **kwargs) 81 | 82 | @classmethod 83 | def match(cls, uri): 84 | importer = Importer(uri=uri) 85 | if os.path.exists(uri) and not os.path.isdir(uri): 86 | importer.fetch() 87 | return importer if importer.ctx.data else None 88 | return None 89 | 90 | @papis.importer.cache 91 | def fetch(self): 92 | self.ctx.data = yaml_to_data(self.uri, raise_exception=False) 93 | if self.ctx: 94 | self.logger.info("successfully read file = %s" % self.uri) 95 | -------------------------------------------------------------------------------- /scripts/shell_completion/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: zsh 2 | 3 | all: dist/bash-completion.sh 4 | 5 | dist/bash-completion.sh: 6 | ./tools/bash.sh 7 | 8 | zsh: 9 | # run from a zsh shell 10 | mkdir -p click/zsh/ 11 | echo "#compdef papis" > click/zsh/_papis 12 | _PAPIS_COMPLETE=source_zsh papis >> click/zsh/_papis 13 | 14 | clean: 15 | rm -rf dist 16 | -------------------------------------------------------------------------------- /scripts/shell_completion/click/papis.sh: -------------------------------------------------------------------------------- 1 | _papis_completion() { 2 | local IFS=$' 3 | ' 4 | COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \ 5 | COMP_CWORD=$COMP_CWORD \ 6 | _PAPIS_COMPLETE=complete $1 ) ) 7 | return 0 8 | } 9 | 10 | _papis_completionetup() { 11 | local COMPLETION_OPTIONS="" 12 | local BASH_VERSION_ARR=(${BASH_VERSION//./ }) 13 | # Only BASH version 4.4 and later have the nosort option. 14 | if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] && [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then 15 | COMPLETION_OPTIONS="-o nosort" 16 | fi 17 | 18 | complete $COMPLETION_OPTIONS -F _papis_completion papis 19 | } 20 | 21 | _papis_completionetup; 22 | -------------------------------------------------------------------------------- /scripts/shell_completion/click/zsh/_papis: -------------------------------------------------------------------------------- 1 | #compdef papis 2 | 3 | _papis_completion() { 4 | local -a completions 5 | local -a completions_with_descriptions 6 | local -a response 7 | response=("${(@f)$( env COMP_WORDS="${words[*]}" \ 8 | COMP_CWORD=$((CURRENT-1)) \ 9 | _PAPIS_COMPLETE="complete_zsh" \ 10 | papis )}") 11 | 12 | for key descr in ${(kv)response}; do 13 | if [[ "$descr" == "_" ]]; then 14 | completions+=("$key") 15 | else 16 | completions_with_descriptions+=("$key":"$descr") 17 | fi 18 | done 19 | 20 | if [ -n "$completions_with_descriptions" ]; then 21 | _describe -V unsorted completions_with_descriptions -U -Q 22 | fi 23 | 24 | if [ -n "$completions" ]; then 25 | compadd -U -V unsorted -Q -a completions 26 | fi 27 | compstate[insert]="automenu" 28 | } 29 | 30 | _papis_completion $@ 31 | -------------------------------------------------------------------------------- /scripts/shell_completion/tools/bash.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | source tools/lib.sh 4 | 5 | COMMANDS=($(get_papis_commands)) 6 | 7 | out=build/bash/papis 8 | mkdir -p build/bash 9 | 10 | echo > ${out} 11 | 12 | cat >> ${out} <> ${out} <> ${out} <= k[1]) 27 | 28 | def test_list_libs(self): 29 | libs = run([], libraries=True) 30 | assert(len(libs) >= 1) 31 | 32 | def test_list_folders(self): 33 | folders = run( 34 | papis.database.get().get_all_documents(), 35 | folders=True 36 | ) 37 | assert(len(folders) >= 1) 38 | assert(isinstance(folders, list)) 39 | for f in folders: 40 | assert(os.path.exists(f)) 41 | -------------------------------------------------------------------------------- /tests/commands/test_mv.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import tests 4 | import tempfile 5 | import papis.database 6 | from papis.commands.mv import run 7 | 8 | 9 | class Test(unittest.TestCase): 10 | 11 | @classmethod 12 | def setUpClass(self): 13 | tests.setup_test_library() 14 | 15 | def get_docs(self): 16 | db = papis.database.get() 17 | return db.get_all_documents() 18 | 19 | def test_simple_update(self): 20 | docs = self.get_docs() 21 | document = docs[0] 22 | title = document['title'] 23 | new_dir = tempfile.mkdtemp() 24 | self.assertTrue(os.path.exists(new_dir)) 25 | run(document, new_dir) 26 | docs = papis.database.get().query_dict(dict(title=title)) 27 | self.assertTrue(len(docs) == 1) 28 | self.assertEqual(os.path.dirname(docs[0].get_main_folder()), new_dir) 29 | self.assertEqual( 30 | docs[0].get_main_folder(), 31 | os.path.join(new_dir, os.path.basename(docs[0].get_main_folder())) 32 | ) 33 | -------------------------------------------------------------------------------- /tests/commands/test_open.py: -------------------------------------------------------------------------------- 1 | import papis.bibtex 2 | import json 3 | import yaml 4 | import tempfile 5 | import unittest 6 | import tests 7 | import tests.cli 8 | import papis.config 9 | import papis.document 10 | from papis.commands.open import run, cli 11 | import re 12 | import os 13 | 14 | 15 | class TestRun(unittest.TestCase): 16 | 17 | @classmethod 18 | def setUpClass(self): 19 | tests.setup_test_library() 20 | 21 | def get_docs(self): 22 | db = papis.database.get() 23 | return db.get_all_documents() 24 | 25 | 26 | class TestCli(tests.cli.TestCli): 27 | 28 | cli = cli 29 | 30 | def test_main(self): 31 | self.do_test_cli_function_exists() 32 | self.do_test_help() 33 | 34 | def test_tool(self): 35 | result = self.invoke([ 36 | 'doc without files' 37 | ]) 38 | self.assertTrue(result.exit_code == 0) 39 | 40 | result = self.invoke([ 41 | 'Krishnamurti', 42 | '--tool', 'nonexistingcommand' 43 | ]) 44 | self.assertTrue(result.exit_code != 0) 45 | 46 | result = self.invoke([ 47 | 'Krishnamurti', 48 | '--tool', 'nonexistingcommand', '--folder' 49 | ]) 50 | self.assertTrue(result.exit_code != 0) 51 | 52 | result = self.invoke([ 53 | 'Krishnamurti', '--mark', '--all', '--tool', 'dir' 54 | ]) 55 | self.assertTrue(result.exit_code == 0) 56 | -------------------------------------------------------------------------------- /tests/commands/test_rename.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import tests 4 | from papis.commands.rename import run 5 | import papis.database 6 | 7 | 8 | class Test(unittest.TestCase): 9 | 10 | @classmethod 11 | def setUpClass(self): 12 | tests.setup_test_library() 13 | 14 | def get_docs(self): 15 | db = papis.database.get() 16 | return db.get_all_documents() 17 | 18 | def test_simple_update(self): 19 | docs = self.get_docs() 20 | document = docs[0] 21 | title = document['title'] 22 | new_name = 'Some title with spaces too' 23 | run(document, new_name) 24 | docs = papis.database.get().query_dict(dict(title=title)) 25 | self.assertTrue(len(docs) == 1) 26 | self.assertEqual(docs[0].get_main_folder_name(), new_name) 27 | self.assertTrue(os.path.exists(docs[0].get_main_folder())) 28 | -------------------------------------------------------------------------------- /tests/commands/test_run.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import tests 3 | from papis.commands.run import run 4 | import papis.config 5 | 6 | 7 | class Test(unittest.TestCase): 8 | 9 | @classmethod 10 | def setUpClass(self): 11 | tests.setup_test_library() 12 | 13 | @classmethod 14 | def tearDownClass(self): 15 | pass 16 | 17 | def test_run_ls(self): 18 | status = run(papis.config.get_lib_dirs()[0], command=['ls']) 19 | assert(status == 0) 20 | 21 | def test_run_nonexistent(self): 22 | status = run(papis.config.get_lib_dirs()[0], command=['nonexistent']) 23 | assert(not status == 1) 24 | -------------------------------------------------------------------------------- /tests/database/test_base.py: -------------------------------------------------------------------------------- 1 | import papis.library 2 | import papis.database.base 3 | 4 | 5 | def test_main_database_methods(): 6 | db = papis.database.base.Database(papis.library.Library('nonexistent', [])) 7 | document = None 8 | query_string = '' 9 | 10 | try: 11 | db.initialize() 12 | except NotImplementedError: 13 | assert(True) 14 | else: 15 | assert(False) 16 | 17 | try: 18 | db.get_backend_name() 19 | except NotImplementedError: 20 | assert(True) 21 | else: 22 | assert(False) 23 | 24 | assert(db.get_lib() == 'nonexistent') 25 | assert(db.get_dirs() == []) 26 | 27 | try: 28 | db.match(document, query_string) 29 | except NotImplementedError: 30 | assert(True) 31 | else: 32 | assert(False) 33 | 34 | try: 35 | db.clear() 36 | except NotImplementedError: 37 | assert(True) 38 | else: 39 | assert(False) 40 | 41 | try: 42 | db.add(document) 43 | except NotImplementedError: 44 | assert(True) 45 | else: 46 | assert(False) 47 | 48 | try: 49 | db.update(document) 50 | except NotImplementedError: 51 | assert(True) 52 | else: 53 | assert(False) 54 | 55 | try: 56 | db.delete(document) 57 | except NotImplementedError: 58 | assert(True) 59 | else: 60 | assert(False) 61 | 62 | try: 63 | db.query(query_string) 64 | except NotImplementedError: 65 | assert(True) 66 | else: 67 | assert(False) 68 | 69 | try: 70 | db.query_dict(query_string) 71 | except NotImplementedError: 72 | assert(True) 73 | else: 74 | assert(False) 75 | 76 | try: 77 | db.get_all_documents() 78 | except NotImplementedError: 79 | assert(True) 80 | else: 81 | assert(False) 82 | 83 | try: 84 | db.get_all_query_string() 85 | except NotImplementedError: 86 | assert(True) 87 | else: 88 | assert(False) 89 | 90 | -------------------------------------------------------------------------------- /tests/database/test_papis.py: -------------------------------------------------------------------------------- 1 | import tests.database 2 | import papis.config 3 | import papis.database 4 | import os 5 | 6 | class Test(tests.database.DatabaseTest): 7 | 8 | @classmethod 9 | def setUpClass(cls): 10 | papis.config.set('database-backend', 'papis') 11 | tests.database.DatabaseTest.setUpClass() 12 | 13 | def test_backend_name(self): 14 | self.assertTrue(papis.config.get('database-backend') == 'papis') 15 | 16 | def test_query(self): 17 | database = papis.database.get() 18 | docs = database.query('.') 19 | self.assertTrue(len(docs) > 0) 20 | 21 | def test_cache_path(self): 22 | database = papis.database.get() 23 | assert(os.path.exists(database._get_cache_file_path())) 24 | 25 | def test_load_again(self): 26 | db = papis.database.get() 27 | Ni = len(db.get_documents()) 28 | db.save() 29 | db.documents = None 30 | # Now the pickled path exists but no documents 31 | Nf = len(db.get_documents()) 32 | self.assertEqual(Ni, Nf) 33 | 34 | def test_failed_location_in_cache(self): 35 | db = papis.database.get() 36 | doc = db.get_documents()[0] 37 | db.delete(doc) 38 | try: 39 | db._locate_document(doc) 40 | except Exception as e: 41 | self.assertTrue(True) 42 | else: 43 | self.assertTrue(False) 44 | -------------------------------------------------------------------------------- /tests/database/test_whoosh.py: -------------------------------------------------------------------------------- 1 | import tests.database 2 | import papis.config 3 | import papis.database 4 | 5 | class Test(tests.database.DatabaseTest): 6 | 7 | @classmethod 8 | def setUpClass(cls): 9 | papis.config.set('database-backend', 'whoosh') 10 | tests.database.DatabaseTest.setUpClass() 11 | 12 | def test_backend_name(self): 13 | self.assertTrue(papis.config.get('database-backend') == 'whoosh') 14 | 15 | def test_query(self): 16 | # The database is existing right now, which means that the 17 | # test library is in place and therefore we have some documents 18 | database = papis.database.get() 19 | docs = database.query('*') 20 | self.assertTrue(len(docs) > 0) 21 | -------------------------------------------------------------------------------- /tests/downloaders/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | 5 | def get_resource(name): 6 | path = os.path.join(os.path.dirname(__file__), 'resources', name) 7 | assert os.path.exists(path) 8 | with open(path, errors='ignore') as f: 9 | return f.read() 10 | 11 | 12 | def get_json_resource(name): 13 | return json.loads(get_resource(name)) 14 | -------------------------------------------------------------------------------- /tests/downloaders/resources/acs_2_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "publisher": " American Chemical Society ", 3 | "volume": "105", 4 | "ref": "doi:10.1021/jp003647e", 5 | "type": "article", 6 | "title": "Heisenberg Hamiltonian for Poly-ynes. Extraction and Tests", 7 | "author_list": [ 8 | { 9 | "given": "Rachida", 10 | "affiliation": [ 11 | { 12 | "name": "Laboratoire de Chimie Théorique, Faculté des Sciences, Université Mohamed V, RabatMaroc, and Laboratoire de Phyique Quantique, IRSAMC, Université Paul Sabatier, 118 Route de Narbonne 31062, Toulouse Cedex " 13 | } 14 | ], 15 | "family": "Ghailane" 16 | }, 17 | { 18 | "given": "Jean", 19 | "affiliation": [ 20 | { 21 | "name": "Laboratoire de Chimie Théorique, Faculté des Sciences, Université Mohamed V, RabatMaroc, and Laboratoire de Phyique Quantique, IRSAMC, Université Paul Sabatier, 118 Route de Narbonne 31062, Toulouse Cedex " 22 | } 23 | ], 24 | "family": "Paul Malrieu" 25 | }, 26 | { 27 | "given": "Daniel", 28 | "affiliation": [ 29 | { 30 | "name": "Laboratoire de Chimie Théorique, Faculté des Sciences, Université Mohamed V, RabatMaroc, and Laboratoire de Phyique Quantique, IRSAMC, Université Paul Sabatier, 118 Route de Narbonne 31062, Toulouse Cedex " 31 | } 32 | ], 33 | "family": "Maynau" 34 | } 35 | ], 36 | "year": "2001", 37 | "pages": "3365-3370", 38 | "url": " \nhttps://doi.org/10.1021/jp003647e\n", 39 | "abstract": "Heisenberg Hamiltonians, with distance-dependent spin couplings and σ-bond potential, have proved to be very efficient for the treatment of conjugated hydrocarbons. A similar approach is applied to C⋮C triple bonds. The effective spin couplings are extracted from accurate CI calculations on acetylene. Tests show that the treatment of poly-ynes gives reliable results. The asymptotic trends of the lowest excited states geometry and energy are discussed.", 40 | "journal": "The Journal of Physical Chemistry A", 41 | "author": "Ghailane, Rachida and Malrieu, Jean Paul and Maynau, Daniel", 42 | "eprint": " \nhttps://doi.org/10.1021/jp003647e\n", 43 | "doi": "10.1021/jp003647e", 44 | "number": "13" 45 | } 46 | -------------------------------------------------------------------------------- /tests/downloaders/resources/annualreviews_1_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": " \nhttps://doi.org/10.1146/annurev-conmatphys-031214-014726\n", 3 | "description": "We review some recent developments in the statistical mechanics of isolated quantum systems. We provide a brief introduction to quantum thermalization, paying particular attention to the eigenstate thermalization hypothesis (ETH) and the resulting single-eigenstate statistical mechanics. We then focus on a class of systems that fail to quantum thermalize and whose eigenstates violate the ETH: These are the many-body Anderson-localized systems; their long-time properties are not captured by the conventional ensembles of quantum statistical mechanics. These systems can forever locally remember information about their local initial conditions and are thus of interest for possibilities of storing quantum information. We discuss key features of many-body localization (MBL) and review a phenomenology of the MBL phase. Single-eigenstate statistical mechanics within the MBL phase reveal dynamically stable ordered phases, and phase transitions among them, that are invisible to equilibrium statistical mechanics and can occur at high energy and low spatial dimensionality, where equilibrium ordering is forbidden.", 4 | "volume": "6", 5 | "journal": "Annual Review of Condensed Matter Physics", 6 | "number": "1", 7 | "publisher": "Annual Reviews", 8 | "language": "EN", 9 | "title": "Many-Body Localization and Thermalization in Quantum Statistical Mechanics", 10 | "type": "article", 11 | "author_list": [], 12 | "author": "Nandkishore, Rahul and Huse, David A.", 13 | "doi": "10.1146/annurev-conmatphys-031214-014726", 14 | "pages": "15-38", 15 | "ref": "doi:10.1146/annurev-conmatphys-031214-014726", 16 | "keywords": "closed systems, entanglement, eigenstate, nonequilibrium, glass", 17 | "eprint": " \nhttps://doi.org/10.1146/annurev-conmatphys-031214-014726\n", 18 | "date": "2015-03-10", 19 | "year": "2015", 20 | "subject": "closed systems; entanglement; eigenstate; nonequilibrium; glass" 21 | } 22 | -------------------------------------------------------------------------------- /tests/downloaders/resources/fallback_1_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://onlinelibrary.wiley.com/doi/full/10.1002/andp.19263851302", 3 | "volume": "385", 4 | "publisher": "John Wiley & Sons, Ltd", 5 | "language": "en", 6 | "lastpage": "490", 7 | "issue": "13", 8 | "journal": "Annalen der Physik", 9 | "doi": "10.1002/andp.19263851302", 10 | "author": "Schrödinger, E.", 11 | "title": "Quantisierung als Eigenwertproblem", 12 | "pdf_url": "https://onlinelibrary.wiley.com/doi/pdf/10.1002/andp.19263851302", 13 | "firstpage": "437", 14 | "author_list": [ 15 | { 16 | "family": "Schrödinger", 17 | "affiliation": [ 18 | { 19 | "name": "Physikalisches Institut der Universität, Zürich" 20 | } 21 | ], 22 | "given": "E." 23 | } 24 | ], 25 | "publication_date": "1926/01/01", 26 | "issn": "1521-3889", 27 | "online_date": "2006/03/16" 28 | } 29 | -------------------------------------------------------------------------------- /tests/downloaders/resources/hal_1_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "doi": "10.1051/jphysrad:01955001606044400", 3 | "pdf_url": "https://hal.archives-ouvertes.fr/jpa-00235190/document", 4 | "issue": "6", 5 | "subject": "alloys ; nuclear magnetic resonance", 6 | "date": "1955", 7 | "author_list": [ 8 | { 9 | "family": "Friedel", 10 | "given": "Jacques", 11 | "affiliation": [] 12 | } 13 | ], 14 | "year": "1955", 15 | "url": "https://hal.archives-ouvertes.fr/jpa-00235190", 16 | "journal_abbrev": "J. Phys. Radium", 17 | "description": "Des calculs récents de la densité électronique dans les alliages prévoient que le déplacement de Knight des noyaux de la matrice n'est pratiquement pas affecté par la substitution d'atomes en soluté, même à assez fortes concentrations.", 18 | "type": "journal", 19 | "online_date": "1955/01/01", 20 | "language": "fr", 21 | "firstpage": "444-445", 22 | "abstract": "Des calculs récents de la densité électronique dans les alliages prévoient que le déplacement de Knight des noyaux de la matrice n'est pratiquement pas affecté par la substitution d'atomes en soluté, même à assez fortes concentrations.", 23 | "author": "Friedel, Jacques", 24 | "title": "Déplacement de Knight dans les alliages", 25 | "journal": "J. Phys. Radium", 26 | "keywords": "alloys ; nuclear magnetic resonance", 27 | "volume": "16", 28 | "publication_date": "1955" 29 | } 30 | -------------------------------------------------------------------------------- /tests/downloaders/resources/iopscience_1_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "issn": "0026-1394", 3 | "url": "https://iopscience.iop.org/article/10.1088/0026-1394/12/4/002", 4 | "date": "October 1976", 5 | "abstract": "\nIn the temperature range from 2 to 30 K a comparison has been made of a magnetic thermometer based on the magnetic susceptibilities of manganous ammonium sulfate (MAS) and gadolinium molybdate (GM) with the readings of a germanium resistance thermometer. The magnetic susceptibility was measured by a screened selfchecking mutual inductance bridge with an uncertainty of the relative measurements being not worse that ±5 × 10-6. The magnetic thermometer sensitivity near 30 K is 0.3 mK with MAS and 0.07 mK with GM. The magnetic thermometer was calibrated in terms of the IPTS-68 and T58 temperature scale with an uncertainty of ±0.5 mK. The magnetic temperature scale was determined using the MAS data obtained in three experimental runs and GM data taken in two runs. The scale was transferred on a germanium resistance thermometer with a root-mean-square deviation of the data from the smooth curve of 0.7 mK. The inconsistency previously reported in the literature between the IPTS-68 and the T58 temperature scale has been confirmed. The helium normal boiling point temperature on the T58 scale is according to our data too low and must be raised by 9.5 ± 1.5 mK. An indirect comparison of our magnetic temperature scale with the results obtained by Van Rijn and Durieux as well as with those by Cetas and Swenson has been carried out in terms of the NBS P 2-20 K scale. The estimated difference between our data and the results of the above authors does not exceed ±3 mK. ", 6 | "journal": "Metrologia", 7 | "firstpage": "143", 8 | "issue": "4", 9 | "volume": "12", 10 | "publisher": "IOP Publishing", 11 | "journal_abbrev": "Metrologia", 12 | "doi": "10.1088/0026-1394/12/4/002", 13 | "type": "Text", 14 | "language": "en", 15 | "online_date": "2005-01-31", 16 | "author": "N Astrov, D and A Pavlov, V and T Shkraba, V", 17 | "title": "Magnetic Temperature Scale in the 2 K to 30 K Range", 18 | "author_list": [ 19 | { 20 | "family": "N Astrov", 21 | "affiliation": [], 22 | "given": "D" 23 | }, 24 | { 25 | "family": "A Pavlov", 26 | "affiliation": [], 27 | "given": "V" 28 | }, 29 | { 30 | "family": "T Shkraba", 31 | "affiliation": [], 32 | "given": "V" 33 | } 34 | ], 35 | "pdf_url": "https://iopscience.iop.org/article/10.1088/0026-1394/12/4/002/pdf" 36 | } 37 | -------------------------------------------------------------------------------- /tests/downloaders/resources/prl_1_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "issue": "14", 3 | "abstract": "A chiral material exposed to a magnetic field allows phonons to travel faster in one direction than another, an effect that might be used to create an acoustic diode.", 4 | "ref": "PhysRevLett.122.145901", 5 | "title": "Phonon Magnetochiral Effect", 6 | "firstpage": "145901", 7 | "type": "article", 8 | "publisher": "American Physical Society", 9 | "journal": "Phys. Rev. Lett.", 10 | "month": "Apr", 11 | "author_list": [ 12 | { 13 | "given": "T.", 14 | "family": "Nomura", 15 | "affiliation": [ 16 | { 17 | "name": "Hochfeld-Magnetlabor Dresden (HLD-EMFL), Helmholtz-Zentrum Dresden-Rossendorf, 01328 Dresden, Germany" 18 | } 19 | ] 20 | }, 21 | { 22 | "given": "X.-X.", 23 | "family": "Zhang", 24 | "affiliation": [ 25 | { 26 | "name": "Department of Applied Physics, The University of Tokyo, Tokyo 113-8656, Japan" 27 | } 28 | ] 29 | }, 30 | { 31 | "given": "S.", 32 | "family": "Zherlitsyn", 33 | "affiliation": [ 34 | { 35 | "name": "Quantum Matter Institute, University of British Columbia, Vancouver BC V6T 1Z4, Canada" 36 | } 37 | ] 38 | }, 39 | { 40 | "given": "J.", 41 | "family": "Wosnitza", 42 | "affiliation": [ 43 | { 44 | "name": "Hochfeld-Magnetlabor Dresden (HLD-EMFL), Helmholtz-Zentrum Dresden-Rossendorf, 01328 Dresden, Germany" 45 | } 46 | ] 47 | }, 48 | { 49 | "given": "Y.", 50 | "family": "Tokura", 51 | "affiliation": [ 52 | { 53 | "name": "Hochfeld-Magnetlabor Dresden (HLD-EMFL), Helmholtz-Zentrum Dresden-Rossendorf, 01328 Dresden, Germany" 54 | } 55 | ] 56 | }, 57 | { 58 | "given": "N.", 59 | "family": "Nagaosa", 60 | "affiliation": [ 61 | { 62 | "name": "Institut für Festkörper-und Materialphysik, TU-Dresden, 01062 Dresden, Germany" 63 | } 64 | ] 65 | }, 66 | { 67 | "given": "S.", 68 | "family": "Seki", 69 | "affiliation": [ 70 | { 71 | "name": "Department of Applied Physics, The University of Tokyo, Tokyo 113-8656, Japan" 72 | } 73 | ] 74 | } 75 | ], 76 | "doi": "10.1103/PhysRevLett.122.145901", 77 | "journal_abbrev": "Phys. Rev. Lett.", 78 | "pages": "145901", 79 | "numpages": "5", 80 | "volume": "122", 81 | "url": "https://link.aps.org/doi/10.1103/PhysRevLett.122.145901", 82 | "pdf_url": "http://link.aps.org/pdf/10.1103/PhysRevLett.122.145901", 83 | "year": "2019", 84 | "author": "Nomura, T. and Zhang, X.-X. and Zherlitsyn, S. and Wosnitza, J. and Tokura, Y. and Nagaosa, N. and Seki, S.", 85 | "description": "A chiral material exposed to a magnetic field allows phonons to travel faster in one direction than another, an effect that might be used to create an acoustic diode." 86 | } 87 | -------------------------------------------------------------------------------- /tests/downloaders/resources/sciencedirect_1_authors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "#name": "author", 4 | "$$": [ 5 | { 6 | "#name": "given-name", 7 | "_": "Alexander O." 8 | }, 9 | { 10 | "#name": "surname", 11 | "_": "Mitrushenkov" 12 | }, 13 | { 14 | "#name": "cross-ref", 15 | "$$": [ 16 | { 17 | "#name": "sup", 18 | "_": "a", 19 | "$": { 20 | "loc": "post" 21 | } 22 | } 23 | ], 24 | "$": { 25 | "refid": "AFF1" 26 | } 27 | } 28 | ], 29 | "$": { 30 | "id": "aep-author-id1" 31 | } 32 | }, 33 | { 34 | "#name": "author", 35 | "$$": [ 36 | { 37 | "#name": "given-name", 38 | "_": "Paolo" 39 | }, 40 | { 41 | "#name": "surname", 42 | "_": "Palmieri" 43 | }, 44 | { 45 | "#name": "cross-ref", 46 | "$$": [ 47 | { 48 | "#name": "sup", 49 | "_": "b", 50 | "$": { 51 | "loc": "post" 52 | } 53 | } 54 | ], 55 | "$": { 56 | "refid": "AFF2" 57 | } 58 | } 59 | ], 60 | "$": { 61 | "id": "aep-author-id2" 62 | } 63 | }, 64 | { 65 | "#name": "affiliation", 66 | "$$": [ 67 | { 68 | "#name": "label", 69 | "_": "a" 70 | }, 71 | { 72 | "#name": "textfn", 73 | "_": "Department of Theoretical Physics, Institute of Physics, St. Petersburg University, 198904 St. Petersburg, Russia" 74 | } 75 | ], 76 | "$": { 77 | "id": "AFF1" 78 | } 79 | }, 80 | { 81 | "#name": "affiliation", 82 | "$$": [ 83 | { 84 | "#name": "label", 85 | "_": "b" 86 | }, 87 | { 88 | "#name": "textfn", 89 | "_": "Dipartimento di Chimica Fisica ed Inorganica, Università di Bologna, Viale Risorgimento 4, 40136 Bologna, Italy" 90 | } 91 | ], 92 | "$": { 93 | "id": "AFF2" 94 | } 95 | } 96 | ] 97 | -------------------------------------------------------------------------------- /tests/downloaders/resources/sciencedirect_1_authors_out.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "given": "Alexander O.", 4 | "affiliation": [ 5 | { 6 | "name": "Department of Theoretical Physics, Institute of Physics, St. Petersburg University, 198904 St. Petersburg, Russia" 7 | } 8 | ], 9 | "family": "Mitrushenkov" 10 | }, 11 | { 12 | "given": "Paolo", 13 | "affiliation": [ 14 | { 15 | "name": "Dipartimento di Chimica Fisica ed Inorganica, Università di Bologna, Viale Risorgimento 4, 40136 Bologna, Italy" 16 | } 17 | ], 18 | "family": "Palmieri" 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /tests/downloaders/resources/sciencedirect_1_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "volume": "278", 3 | "issn": "00092614", 4 | "doi": "10.1016/S0009-2614(97)04014-1", 5 | "author": "Mitrushenkov, Alexander O. and Palmieri, Paolo", 6 | "publication-date": "31 October 1997", 7 | "abstract": "The Cr 2 potential energy curve is obtained by treating the dynamical correlation energy terms using a CI procedure based on determinants, equivalent to multi-reference second-order perturbation theory with the Epstein-Nesbet partition of the Hamiltonian. One of the advantages of this treatment is that the level shifts required for the equivalent Møller-Plesset type of treatment to eliminate intruder states and avoid divergences in the ground state second-order energy expression, are not required. Moreover, by truncating the reference space, the dynamical and the non-dynamical correlation energy is included also for the 3s, 3p electrons. The potentials are compared to experiment and to the most accurate theoretical potential curves in the literature.", 8 | "pii": "S0009261497040141", 9 | "date": "1997-10-31", 10 | "language": "en", 11 | "type": "converted-article", 12 | "accepted-date": "26 August 1997", 13 | "author_list": [ 14 | { 15 | "given": "Alexander O.", 16 | "affiliation": [ 17 | { 18 | "name": "Department of Theoretical Physics, Institute of Physics, St. Petersburg University, 198904 St. Petersburg, Russia" 19 | } 20 | ], 21 | "family": "Mitrushenkov" 22 | }, 23 | { 24 | "given": "Paolo", 25 | "affiliation": [ 26 | { 27 | "name": "Dipartimento di Chimica Fisica ed Inorganica, Università di Bologna, Viale Risorgimento 4, 40136 Bologna, Italy" 28 | } 29 | ], 30 | "family": "Palmieri" 31 | } 32 | ], 33 | "title": "Epstein-Nesbet second-order perturbation treatment of dynamical electron correlation and ground state potential energy curve of Cr2", 34 | "journal": "Chemical Physics Letters", 35 | "pages": "285--290", 36 | "year": "1997" 37 | } 38 | -------------------------------------------------------------------------------- /tests/downloaders/resources/sciencedirect_2_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "volume": "1144", 3 | "issn": "2210271X", 4 | "doi": "10.1016/j.comptc.2018.10.004", 5 | "author": "Finzel, Kati", 6 | "publication-date": "15 November 2018", 7 | "abstract": "This work presents an implementation of the original orbital-free Hohenberg-Kohn density functional theory in a form that is able to predict chemical bonding in molecules. The method is completely parameter-free and does not require analytical functional approximations. Instead, the proposed method is based on the idea that atoms are meaningful pieces of a molecule and thus, a promolecule, build from frozen spherical atomic entities, serves as a suitable model for the latter. This idea is imposed on the physical equations, originating from density functional theory converted into a bifunctional formalism. The viewpoint proposed in this study offers a new strategic way of subsequent approximation levels in orbital-free density functional theory. In this work the zeroth order approximation is shown to predict chemical bonding in molecules, providing a concept of the chemical bond without involving orbitals.", 8 | "pii": "S2210271X18305656", 9 | "date": "2018-11-15", 10 | "language": "en", 11 | "type": "article", 12 | "accepted-date": "20 October 2018", 13 | "author_list": [ 14 | { 15 | "given": "Kati", 16 | "family": "Finzel", 17 | "affiliation": [ 18 | { 19 | "name": "Faculty of Chemistry and Food Chemistry, Technische Universität Dresden, Bergstraße 66, 01069 Dresden, Germany" 20 | } 21 | ] 22 | } 23 | ], 24 | "title": "Chemical bonding without orbitals", 25 | "journal": "Computational and Theoretical Chemistry", 26 | "pages": "50--55", 27 | "year": "2018" 28 | } 29 | -------------------------------------------------------------------------------- /tests/downloaders/resources/springer_1_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": "231--234", 3 | "year": "2010", 4 | "ref": "Bandyopadhyay-Ghosh2010", 5 | "author": "Bandyopadhyay-Ghosh, Sanchita\nand Jeng, Robert\nand Mukherjee, Joydeep\nand Sain, Mohini", 6 | "number": "3", 7 | "url": "https://doi.org/10.1007/s10924-010-0192-1", 8 | "issn": "1572-8900", 9 | "journal": "Journal of Polymers and the Environment", 10 | "author_list": [ 11 | { 12 | "given": "Sanchita", 13 | "affiliation": [ 14 | { 15 | "name": "University of Toronto" 16 | } 17 | ], 18 | "family": "Bandyopadhyay-Ghosh" 19 | }, 20 | { 21 | "given": "Robert", 22 | "affiliation": [ 23 | { 24 | "name": "University of Toronto" 25 | } 26 | ], 27 | "family": "Jeng" 28 | }, 29 | { 30 | "given": "Joydeep", 31 | "affiliation": [ 32 | { 33 | "name": "University of Toronto" 34 | } 35 | ], 36 | "family": "Mukherjee" 37 | }, 38 | { 39 | "given": "Mohini", 40 | "affiliation": [ 41 | { 42 | "name": "University of Toronto" 43 | } 44 | ], 45 | "family": "Sain" 46 | } 47 | ], 48 | "doi": "10.1007/s10924-010-0192-1", 49 | "title": "In Vitro Cytotoxicity of Amylose-Based Bioplastic for Packaging Applications", 50 | "type": "article", 51 | "volume": "18", 52 | "publisher": "Springer US", 53 | "day": "01", 54 | "abstract": "Amylose containing polysaccharides are one of the most abundant and inexpensive naturally occurring biopolymers. Therefore, they are one of the most promising candidates to produce substitute plastics, especially in packaging applications. To determine the suitability for packaging applications, cytotoxicity of a modified amylose based bioplastic was investigated using NIH 3T3 Fibroblast cells from observation of cell morphology and MTS assay. Chemical durability of the amylose based bioplastic film was also studied by ion release and pH measurement after immersing the film into water. In vitro cytotoxicity (Cell morphology study and MTS assay) showed that the amylose based bioplastic film has in vitro biocompatibility and can be used for packaging applications. The ion release and pH measurement also supported the results.", 55 | "month": "Sep" 56 | } 57 | -------------------------------------------------------------------------------- /tests/downloaders/resources/springer_2_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "article", 3 | "author_list": [ 4 | { 5 | "family": "M. Leinaas", 6 | "affiliation": [ 7 | { 8 | "name": "University of Oslo" 9 | } 10 | ], 11 | "given": "J." 12 | }, 13 | { 14 | "family": "Myrheim", 15 | "affiliation": [ 16 | { 17 | "name": "University of Oslo" 18 | } 19 | ], 20 | "given": "J." 21 | } 22 | ], 23 | "day": "01", 24 | "author": "Leinaas, J. M.\nand Myrheim, J.", 25 | "month": "Jan", 26 | "year": "1977", 27 | "pages": "1--23", 28 | "publisher": "Società Italiana di Fisica", 29 | "url": "https://doi.org/10.1007/BF02727953", 30 | "number": "1", 31 | "ref": "Leinaas1977", 32 | "doi": "10.1007/BF02727953", 33 | "volume": "37", 34 | "abstract": "The classical configuration space of a system of identical particles is examined. Due to the identification of points which are related by permutations of particle indices, it is essentially different, globally, from the Cartesian product of the one-particle spaces. This fact is explicity taken into account in a quantization of the theory. As a consequence, no symmetry constraints on the wave functions and the observables need to be postulated. The two possibilities, corresponding to symmetric and antisymmetric wave functions, appear in a natural way in the formalism. But this is only the case in which the particles move in three- or higher-dimensional space. In one and two dimensions a continuum of possible intermediate cases connects the boson and fermion cases. The effect of particle spin in the present formalism is discussed.", 35 | "issn": "1826-9877", 36 | "title": "On the theory of identical particles", 37 | "journal": "Il Nuovo Cimento B (1971-1996)" 38 | } 39 | -------------------------------------------------------------------------------- /tests/downloaders/resources/tandfonline_1_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "abstract": "Contrarily to what happens with the Epstein–Nesbet (EN) zeroth-order Hamiltonian, the Møller–Plesset (MP) perturbation operator has diagonal matrix elements, the expression of which is recalled. It is a balance between hole–hole and particle–particle repulsions on one hand and of hole–particle attractions on the other hand. For the double excitations, which dominate the correlation effects, the attractive terms prevail and the second-order MP energy is underestimated, at least for atoms of the first rows of the periodic table. It will be shown that when the perturbation expansion reaches multiple excitations, the diagonal terms of the MP perturbation operator may become larger than the zeroth-order MP excitation energy and creates an oscillating divergence of the series. Several situations of this type will be presented. This divergence is linked to the non-additivity of excitation energies, while this additivity is an underlying assumption for the linked cluster theorem and the coupled cluster method. Th...", 3 | "author": " Jean-Paul Malrieu and Celestino Angeli ", 4 | "author_list": [ 5 | { 6 | "affiliation": [ 7 | { 8 | "name": "Laboratoire de Chimie et Physique Quantiques, Université Paul Sabatier, Toulouse Cedex, 118 route de Narbonne, F-31062, France" 9 | } 10 | ], 11 | "family": "Malrieu", 12 | "given": "Jean-Paul" 13 | }, 14 | { 15 | "affiliation": [ 16 | { 17 | "name": "Università di Ferrara, Dipartimento di Scienze Chimiche e Farmaceutiche, Ferrara, Via Borsari 46, I-44121, Italy" 18 | } 19 | ], 20 | "family": "Angeli", 21 | "given": "Celestino" 22 | } 23 | ], 24 | "date": "24 Apr 2013", 25 | "doi": "10.1080/00268976.2013.788745", 26 | "eprint": " \nhttps://doi.org/10.1080/00268976.2013.788745\n", 27 | "journal": "Molecular Physics", 28 | "keywords": "Møller–Plesset perturbation theory; Epstein–Nesbet perturbation theory; divergences in perturbation theory; linked cluster theorem; coupled cluster", 29 | "language": "en", 30 | "number": "9-11", 31 | "pages": "1092-1099", 32 | "publisher": "Taylor & Francis", 33 | "ref": "doi:10.1080/00268976.2013.788745", 34 | "title": "The Møller–Plesset perturbation revisited: origin of high-order divergences", 35 | "type": "article", 36 | "url": " \nhttps://doi.org/10.1080/00268976.2013.788745\n", 37 | "volume": "111", 38 | "year": "2013" 39 | } -------------------------------------------------------------------------------- /tests/downloaders/resources/tandfonline_2_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "abstract": "AbstractFood packaging as a vital part of the subject of food technology is involved with protection and preservation of all types of foods. Due to economical abundance, petrochemical plastics have been largely used as packaging material due to their desirable properties of good barrier properties towards O2, aroma compounds, tensile strength and tear strength. Meanwhile, they have many disadvantages like very low water vapour transmission rate and the major disadvantage is that they are non-biodegradable and result in environmental pollution. Keeping in view the non-renewable nature and waste disposal problem of petroleum, newer concept of use of bioplastics came into existence. Bioplastics of renewable origin are compostable or degradable by the enzymatic action of micro-organisms. Generally biodegradable polymers get hydrolysed into CO2, CH4, inorganic compounds or biomass. The use of bio-origin materials obtained through microbial fermentations, starch and cellulose has led to their tremendous innovat...", 3 | "author": "Nafisa Jabeen and Ishrat Majid and Gulzar Ahmad Nayik", 4 | "author_list": [ 5 | { 6 | "affiliation": [ 7 | { 8 | "name": "Division of Post Harvest Technology, SKUAST-Kashmir, Srinagar 190025, India" 9 | } 10 | ], 11 | "family": "Jabeen", 12 | "given": "Nafisa" 13 | }, 14 | { 15 | "affiliation": [ 16 | { 17 | "name": "Department of Food Engineering Technology, SLIET, Longowal 148106, Punjab, India" 18 | } 19 | ], 20 | "family": "Majid", 21 | "given": "Ishrat" 22 | }, 23 | { 24 | "affiliation": [ 25 | { 26 | "name": "Department of Food Engineering Technology, SLIET, Longowal 148106, Punjab, India" 27 | } 28 | ], 29 | "family": "Nayik", 30 | "given": "Gulzar Ahmad" 31 | } 32 | ], 33 | "date": "14 Dec 2015", 34 | "doi": "10.1080/23311932.2015.1117749", 35 | "editor": "Fatih Yildiz", 36 | "eprint": " \nhttps://doi.org/10.1080/23311932.2015.1117749\n", 37 | "journal": "Cogent Food \\& Agriculture", 38 | "keywords": "bioplastic; petrochemical; polymerase; packaging; biodegradable", 39 | "language": "en", 40 | "number": "1", 41 | "pages": "1117749", 42 | "publisher": "Cogent OA", 43 | "ref": "doi:10.1080/23311932.2015.1117749", 44 | "title": "Bioplastics and food packaging: A review", 45 | "type": "article", 46 | "url": " \nhttps://doi.org/10.1080/23311932.2015.1117749\n", 47 | "volume": "1", 48 | "year": "2015" 49 | } -------------------------------------------------------------------------------- /tests/downloaders/test_acs.py: -------------------------------------------------------------------------------- 1 | import papis.downloaders 2 | from papis.downloaders.acs import Downloader 3 | from unittest.mock import patch 4 | from tests.downloaders import get_resource, get_json_resource 5 | import logging 6 | logging.basicConfig(level=logging.DEBUG) 7 | 8 | 9 | def test_match(): 10 | assert(Downloader.match( 11 | 'https://www.acs.org/science/article/pii/S0009261497040141' 12 | )) 13 | assert(Downloader.match( 14 | 'https://www.acs.org/science/article/pii/S2210271X18305656' 15 | )) 16 | 17 | 18 | def test_acs_1(): 19 | url = 'https://pubs.acs.org/pdf/10.1021/acscombsci.5b00087' 20 | down = papis.downloaders.get_downloader(url) 21 | assert(not down.ctx) 22 | with patch.object(down, '_get_body', lambda: get_resource('acs_1.html')): 23 | with patch.object(down, 'download_document', lambda: None): 24 | down.fetch() 25 | with open('acs_1_out.json', 'w+') as f: 26 | import json 27 | json.dump(down.ctx.data, f) 28 | correct_data = get_json_resource('acs_1_out.json') 29 | assert(down.ctx.data == correct_data) 30 | 31 | 32 | def test_acs_2(): 33 | url = 'https://pubs.acs.org/pdf/10.1021/acscombsci.5b00087' 34 | down = papis.downloaders.get_downloader(url) 35 | assert(not down.ctx) 36 | with patch.object(down, '_get_body', lambda: get_resource('acs_2.html')): 37 | with patch.object(down, 'download_document', lambda: None): 38 | down.fetch() 39 | with open('acs_2_out.json', 'w+') as f: 40 | import json 41 | json.dump(down.ctx.data, f) 42 | correct_data = get_json_resource('acs_2_out.json') 43 | assert(down.ctx.data == correct_data) 44 | -------------------------------------------------------------------------------- /tests/downloaders/test_annualreviews.py: -------------------------------------------------------------------------------- 1 | import papis.downloaders 2 | from papis.downloaders.annualreviews import Downloader 3 | from unittest.mock import patch 4 | from tests.downloaders import get_resource, get_json_resource 5 | import logging 6 | logging.basicConfig(level=logging.DEBUG) 7 | 8 | 9 | def test_match(): 10 | assert(Downloader.match( 11 | 'http://anualreviews.org/doi/pdf/' 12 | '10.1146/annurev-conmatphys-031214-014726' 13 | ) is False) 14 | 15 | assert(Downloader.match( 16 | 'http://annualreviews.org/doi/pdf/' 17 | '10.1146/annurev-conmatphys-031214-014726' 18 | )) 19 | 20 | 21 | def test_1(): 22 | url = ('https://www.annualreviews.org/doi/10.1146/' 23 | 'annurev-conmatphys-031214-014726') 24 | down = papis.downloaders.get_downloader(url) 25 | assert(down.name == 'annualreviews') 26 | with patch.object(down, '_get_body', 27 | lambda: get_resource('annualreviews_1.html')): 28 | with patch.object(down, 'download_document', lambda: None): 29 | down.fetch() 30 | # with open('annualreviews_1_out.json', 'w+') as f: 31 | # import json 32 | # json.dump(down.ctx.data, f) 33 | correct_data = get_json_resource('annualreviews_1_out.json') 34 | assert(down.ctx.data == correct_data) 35 | -------------------------------------------------------------------------------- /tests/downloaders/test_aps.py: -------------------------------------------------------------------------------- 1 | import papis.downloaders 2 | from papis.downloaders.aps import Downloader 3 | from unittest.mock import patch 4 | from tests.downloaders import get_resource, get_json_resource 5 | import logging 6 | logging.basicConfig(level=logging.DEBUG) 7 | 8 | 9 | def test_1(): 10 | url = 'https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.122.145901' 11 | down = papis.downloaders.get_downloader(url) 12 | assert(down.name == 'aps') 13 | with patch.object(down, '_get_body', 14 | lambda: get_resource('prl_1.html')): 15 | with patch.object(down, 'download_document', lambda: None): 16 | down.fetch() 17 | correct_data = get_json_resource('prl_1_out.json') 18 | assert(down.ctx.data == correct_data) 19 | # with open('prl_1_out.json', 'w+') as f: 20 | # import json 21 | # json.dump(down.ctx.data, f) 22 | -------------------------------------------------------------------------------- /tests/downloaders/test_arxiv.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alejandrogallo/papis/472c497f222e48012edffde0867c2b6d7b798b3d/tests/downloaders/test_arxiv.py -------------------------------------------------------------------------------- /tests/downloaders/test_fallback.py: -------------------------------------------------------------------------------- 1 | import papis.downloaders 2 | from papis.downloaders.fallback import Downloader 3 | from unittest.mock import patch 4 | from tests.downloaders import get_resource, get_json_resource 5 | import logging 6 | logging.basicConfig(level=logging.DEBUG) 7 | 8 | 9 | def test_1(): 10 | url = 'asdfadfasdfasdfasdf' 11 | down = papis.downloaders.get_downloader(url) 12 | assert(down.name == 'fallback') 13 | with patch.object(down, '_get_body', lambda: get_resource('wiley_1.html')): 14 | with patch.object(down, 'download_document', lambda: None): 15 | down.fetch() 16 | correct_data = get_json_resource('fallback_1_out.json') 17 | assert(down.ctx.data == correct_data) 18 | # with open('fallback_1_out.json', 'w+') as f: 19 | # import json 20 | # json.dump(down.ctx.data, f) 21 | 22 | def test_2(): 23 | url = 'https://link.fallback.com/article/10.1007%2FBF02727953' 24 | down = papis.downloaders.get_downloader(url) 25 | assert(down.name == 'fallback') 26 | with patch.object(down, '_get_body', 27 | lambda: get_resource('fallback_2.html')): 28 | with patch.object(down, 'download_document', lambda: None): 29 | down.fetch() 30 | correct_data = get_json_resource('fallback_2_out.json') 31 | assert(down.ctx.data == correct_data) 32 | # with open('fallback_2_out.json', 'w+') as f: 33 | # import json 34 | # json.dump(down.ctx.data, f) 35 | -------------------------------------------------------------------------------- /tests/downloaders/test_hal.py: -------------------------------------------------------------------------------- 1 | import papis.downloaders 2 | from papis.downloaders.hal import Downloader 3 | from unittest.mock import patch 4 | from tests.downloaders import get_resource, get_json_resource 5 | import logging 6 | logging.basicConfig(level=logging.DEBUG) 7 | 8 | 9 | def test_1(): 10 | url = 'https://hal.archives-ouvertes.fr/jpa-00235190' 11 | down = papis.downloaders.get_downloader(url) 12 | assert(down.name == 'hal') 13 | with patch.object(down, '_get_body', lambda: get_resource('hal_1.html')): 14 | with patch.object(down, 'download_document', lambda: None): 15 | down.fetch() 16 | correct_data = get_json_resource('hal_1_out.json') 17 | assert(down.ctx.data == correct_data) 18 | # with open('hal_1_out.json', 'w+') as f: 19 | # import json 20 | # json.dump(down.ctx.data, f) 21 | -------------------------------------------------------------------------------- /tests/downloaders/test_iopscience.py: -------------------------------------------------------------------------------- 1 | import papis.downloaders 2 | from tests.downloaders import get_resource, get_json_resource 3 | from unittest.mock import patch 4 | from papis.downloaders.iopscience import Downloader 5 | import papis.bibtex 6 | 7 | 8 | def test_1(): 9 | # One old paper 10 | url = 'https://iopscience.iop.org/article/10.1088/0026-1394/12/4/002' 11 | down = papis.downloaders.get_downloader(url) 12 | assert(down.name == 'iopscience') 13 | with patch.object(down, '_get_body', 14 | lambda: get_resource('iopscience_1.html')): 15 | with patch.object(down, 'download_document', lambda: None): 16 | down.fetch() 17 | correct_data = get_json_resource('iopscience_1_out.json') 18 | assert(down.ctx.data == correct_data) 19 | # with open('iopscience_1_out.json', 'w+') as f: 20 | # import json 21 | # json.dump(down.ctx.data, f) 22 | 23 | 24 | def test_2(): 25 | # Multiple authors with affiliations 26 | url = 'https://iopscience.iop.org/article/10.1088/1748-605X/ab007b' 27 | down = papis.downloaders.get_downloader(url) 28 | assert(down.name == 'iopscience') 29 | with patch.object(down, '_get_body', 30 | lambda: get_resource('iopscience_2.html')): 31 | with patch.object(down, 'download_document', lambda: None): 32 | down.fetch() 33 | correct_data = get_json_resource('iopscience_2_out.json') 34 | assert(down.ctx.data == correct_data) 35 | # with open('iopscience_2_out.json', 'w+') as f: 36 | # import json 37 | # json.dump(down.ctx.data, f) 38 | -------------------------------------------------------------------------------- /tests/downloaders/test_sciencedirect.py: -------------------------------------------------------------------------------- 1 | import papis.downloaders 2 | from papis.downloaders.sciencedirect import Downloader, get_author_list 3 | from unittest.mock import patch 4 | from tests.downloaders import get_resource, get_json_resource 5 | import logging 6 | logging.basicConfig(level=logging.DEBUG) 7 | 8 | 9 | def test_match(): 10 | assert(Downloader.match( 11 | 'https://www.sciencedirect.com/science/article/pii/S0009261497040141' 12 | )) 13 | assert(Downloader.match( 14 | 'https://www.sciencedirect.com/science/article/pii/S2210271X18305656' 15 | )) 16 | 17 | 18 | def test_1(): 19 | url = 'https://www.sciencedirect.com/science/article/pii/bguss1' 20 | down = papis.downloaders.get_downloader(url) 21 | assert(not down.ctx) 22 | with patch.object(down, '_get_body', 23 | lambda: get_resource('sciencedirect_1.html')): 24 | with patch.object(down, 'download_document', lambda: None): 25 | down.fetch() 26 | correct_data = get_json_resource('sciencedirect_1_out.json') 27 | assert(down.ctx.data == correct_data) 28 | 29 | 30 | def test_2(): 31 | url = 'https://www.sciencedirect.com/science/article/pii/bogus' 32 | down = papis.downloaders.get_downloader(url) 33 | assert(not down.ctx) 34 | with patch.object(down, '_get_body', 35 | lambda: get_resource('sciencedirect_2.html')): 36 | with patch.object(down, 'download_document', lambda: None): 37 | down.fetch() 38 | correct_data = get_json_resource('sciencedirect_2_out.json') 39 | assert(down.ctx.data == correct_data) 40 | 41 | 42 | def test_get_authors(): 43 | rawdata = get_json_resource('sciencedirect_1_authors.json') 44 | correct_data = get_json_resource('sciencedirect_1_authors_out.json') 45 | data = get_author_list(rawdata) 46 | assert(correct_data == data) 47 | -------------------------------------------------------------------------------- /tests/downloaders/test_springer.py: -------------------------------------------------------------------------------- 1 | import papis.downloaders 2 | from papis.downloaders.springer import Downloader 3 | from unittest.mock import patch 4 | from tests.downloaders import get_resource, get_json_resource 5 | import logging 6 | logging.basicConfig(level=logging.DEBUG) 7 | 8 | 9 | def test_1(): 10 | url = 'https://link.springer.com/article/10.1007/s10924-010-0192-1#citeas' 11 | down = papis.downloaders.get_downloader(url) 12 | assert(down.name == 'springer') 13 | with patch.object(down, '_get_body', 14 | lambda: get_resource('springer_1.html')): 15 | with patch.object(down, 'download_document', lambda: None): 16 | down.fetch() 17 | correct_data = get_json_resource('springer_1_out.json') 18 | assert(down.ctx.data == correct_data) 19 | # with open('springer_1_out.json', 'w+') as f: 20 | # import json 21 | # json.dump(down.ctx.data, f) 22 | 23 | def test_2(): 24 | url = 'https://link.springer.com/article/10.1007%2FBF02727953' 25 | down = papis.downloaders.get_downloader(url) 26 | assert(down.name == 'springer') 27 | with patch.object(down, '_get_body', 28 | lambda: get_resource('springer_2.html')): 29 | with patch.object(down, 'download_document', lambda: None): 30 | down.fetch() 31 | correct_data = get_json_resource('springer_2_out.json') 32 | assert(down.ctx.data == correct_data) 33 | # with open('springer_2_out.json', 'w+') as f: 34 | # import json 35 | # json.dump(down.ctx.data, f) 36 | -------------------------------------------------------------------------------- /tests/downloaders/test_tandfonline.py: -------------------------------------------------------------------------------- 1 | import papis.downloaders 2 | from papis.downloaders.tandfonline import Downloader 3 | from unittest.mock import patch 4 | from tests.downloaders import get_resource, get_json_resource 5 | import logging 6 | logging.basicConfig(level=logging.DEBUG) 7 | 8 | 9 | def test_match(): 10 | assert(Downloader.match( 11 | 'https://www.tandfonline.com/doi/full/10.1080/00268976.2013.788745' 12 | ).name == 'tandfonline') 13 | assert(Downloader.match( 14 | 'https://www.tandfonline.com/doi/full/10.1080/23311932.2015.1117749' 15 | ).name == 'tandfonline') 16 | 17 | 18 | def test_1(): 19 | url = 'https://www.tandfonline.com/doi/full/10.1080/00268976.2013.788745' 20 | down = papis.downloaders.get_downloader(url) 21 | assert(down.name == 'tandfonline') 22 | 23 | # with open('tandfonline_1.html', 'w+') as f: 24 | # f.write(down.session.get(url).content.decode()) 25 | 26 | with patch.object(down, '_get_body', 27 | lambda: get_resource('tandfonline_1.html')): 28 | down.fetch() 29 | with patch.object(down, 'download_document', lambda: None): 30 | # with open('tandfonline_1_out.json', 'w+') as f: 31 | # import json 32 | # json.dump(down.ctx.data, f, 33 | # indent=2, 34 | # sort_keys=True, 35 | # ensure_ascii=False) 36 | correct_data = get_json_resource('tandfonline_1_out.json') 37 | assert(down.ctx.data == correct_data) 38 | 39 | 40 | def test_2(): 41 | url = 'https://www.tandfonline.com/doi/full/10.1080/23311932.2015.1117749' 42 | down = papis.downloaders.get_downloader(url) 43 | assert(down.name == 'tandfonline') 44 | 45 | # with open('tandfonline_2.html', 'w+') as f: 46 | # f.write(down.session.get(url).content.decode()) 47 | 48 | with patch.object(down, '_get_body', 49 | lambda: get_resource('tandfonline_2.html')): 50 | down.fetch() 51 | with patch.object(down, 'download_document', lambda: None): 52 | # with open('tandfonline_2_out.json', 'w+') as f: 53 | # import json 54 | # json.dump(down.ctx.data, f, 55 | # indent=2, 56 | # sort_keys=True, 57 | # ensure_ascii=False) 58 | correct_data = get_json_resource('tandfonline_2_out.json') 59 | assert(down.ctx.data == correct_data) 60 | -------------------------------------------------------------------------------- /tests/downloaders/test_utils.py: -------------------------------------------------------------------------------- 1 | from papis.downloaders import ( 2 | get_available_downloaders, 3 | get_downloader 4 | ) 5 | 6 | 7 | def test_get_available_downloaders(): 8 | downloaders = get_available_downloaders() 9 | assert(len(downloaders) > 0) 10 | for d in downloaders: 11 | assert(d is not None) 12 | assert(callable(d.match)) 13 | 14 | 15 | def test_get_downloader(): 16 | down = get_downloader('https://google.com', 'get') 17 | assert(down is not None) 18 | assert(down.name == 'get') 19 | 20 | down = get_downloader('arXiv:1701.08223v2') 21 | assert(down is not None) 22 | assert(down.name == 'arxiv') 23 | -------------------------------------------------------------------------------- /tests/resources/bibtex/1.bib: -------------------------------------------------------------------------------- 1 | % some comments here 2 | @article{PhysRevLett.105.040504, 3 | title = { 4 | Room-Temperature Implementation of the Deutsch-Jozsa Algorithm with a Single 5 | Electronic Spin in Diamond 6 | }, 7 | author = { 8 | Shi, 9 | % some comments here 10 | Fazhan and Rong, 11 | Xing and Xu, 12 | Nanyang and Wang, 13 | Ya and Wu, 14 | Jie and Chong, 15 | Bo and Peng, 16 | Xinhua and Kniepert, 17 | Juliane and Schoenfeld, 18 | Rolf-Simon and Harneit, 19 | Wolfgang and Feng, 20 | Mang and Du, 21 | Jiangfeng}, 22 | journal = {Phys. {Rev}. Lett.}, 23 | 24 | abstract = {... to 100 {\%}(concurrent intercalation)...}, 25 | 26 | volume = {105}, 27 | issue = {4}, 28 | 29 | pages = {040504}, 30 | 31 | 32 | 33 | numpages = {4}, 34 | year = {2010}, 35 | month = "Jul", 36 | publisher = {American Physical Society}, 37 | doi = {10.1103/PhysRevLett.105.040504}, 38 | url = {http://link.aps.org/doi/10.1103/PhysRevLett.105.040504} 39 | } 40 | 41 | -------------------------------------------------------------------------------- /tests/resources/bibtex/1_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "abstract": "... to 100 {\\%}(concurrent intercalation)...", 3 | "author": "Shi, % some comments here Fazhan and Rong, Xing and Xu, Nanyang and Wang, Ya and Wu, Jie and Chong, Bo and Peng, Xinhua and Kniepert, Juliane and Schoenfeld, Rolf-Simon and Harneit, Wolfgang and Feng, Mang and Du, Jiangfeng", 4 | "author_list": [ 5 | { 6 | "family": "Shi", 7 | "given": "% some comments here Fazhan" 8 | }, 9 | { 10 | "family": "Rong", 11 | "given": "Xing" 12 | }, 13 | { 14 | "family": "Xu", 15 | "given": "Nanyang" 16 | }, 17 | { 18 | "family": "Wang", 19 | "given": "Ya" 20 | }, 21 | { 22 | "family": "Wu", 23 | "given": "Jie" 24 | }, 25 | { 26 | "family": "Chong", 27 | "given": "Bo" 28 | }, 29 | { 30 | "family": "Peng", 31 | "given": "Xinhua" 32 | }, 33 | { 34 | "family": "Kniepert", 35 | "given": "Juliane" 36 | }, 37 | { 38 | "family": "Schoenfeld", 39 | "given": "Rolf-Simon" 40 | }, 41 | { 42 | "family": "Harneit", 43 | "given": "Wolfgang" 44 | }, 45 | { 46 | "family": "Feng", 47 | "given": "Mang" 48 | }, 49 | { 50 | "family": "Du", 51 | "given": "Jiangfeng" 52 | } 53 | ], 54 | "doi": "10.1103/PhysRevLett.105.040504", 55 | "issue": "4", 56 | "journal": "Phys. {Rev}. Lett.", 57 | "month": "Jul", 58 | "numpages": "4", 59 | "pages": "040504", 60 | "publisher": "American Physical Society", 61 | "ref": "PhysRevLett.105.040504", 62 | "title": "\nRoom-Temperature Implementation of the Deutsch-Jozsa Algorithm with a Single\nElectronic Spin in Diamond\n", 63 | "type": "article", 64 | "url": "http://link.aps.org/doi/10.1103/PhysRevLett.105.040504", 65 | "volume": "105", 66 | "year": "2010" 67 | } -------------------------------------------------------------------------------- /tests/resources/bibtex/2.bib: -------------------------------------------------------------------------------- 1 | @article{10.1016j.jcp.2005.08.004, 2 | author = {Marianne M. Francois and Sharen J. Cummins and Edward D. Dendy and Douglas B. Kothe and James M. Sicilian and Matthew W. Williams}, 3 | doi = {10.1016/j.jcp.2005.08.004}, 4 | issue = {1}, 5 | journal = {Journal of Computational Physics}, 6 | language = {en}, 7 | month = {3}, 8 | pages = {141--173}, 9 | publisher = {Elsevier BV}, 10 | title = {A balanced-force algorithm for continuous and sharp interfacial surface tension models within a volume tracking framework}, 11 | type = {article}, 12 | url = {http://dx.doi.org/10.1016/j.jcp.2005.08.004}, 13 | volume = {213}, 14 | year = {2006}, 15 | } 16 | -------------------------------------------------------------------------------- /tests/resources/bibtex/2_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Francois, Marianne M. and Cummins, Sharen J. and Dendy, Edward D. and Kothe, Douglas B. and Sicilian, James M. and Williams, Matthew W.", 3 | "author_list": [ 4 | { 5 | "family": "Francois", 6 | "given": "Marianne M." 7 | }, 8 | { 9 | "family": "Cummins", 10 | "given": "Sharen J." 11 | }, 12 | { 13 | "family": "Dendy", 14 | "given": "Edward D." 15 | }, 16 | { 17 | "family": "Kothe", 18 | "given": "Douglas B." 19 | }, 20 | { 21 | "family": "Sicilian", 22 | "given": "James M." 23 | }, 24 | { 25 | "family": "Williams", 26 | "given": "Matthew W." 27 | } 28 | ], 29 | "doi": "10.1016/j.jcp.2005.08.004", 30 | "issue": "1", 31 | "journal": "Journal of Computational Physics", 32 | "language": "en", 33 | "month": "3", 34 | "pages": "141--173", 35 | "publisher": "Elsevier BV", 36 | "ref": "10.1016j.jcp.2005.08.004", 37 | "title": "A balanced-force algorithm for continuous and sharp interfacial surface tension models within a volume tracking framework", 38 | "type": "article", 39 | "url": "http://dx.doi.org/10.1016/j.jcp.2005.08.004", 40 | "volume": "213", 41 | "year": "2006" 42 | } -------------------------------------------------------------------------------- /tests/resources/bibtex/3.bib: -------------------------------------------------------------------------------- 1 | @article{bibtextest3, 2 | author = {Charles Louis Xavier Joseph de la Vallee Poussin and von Beethoven, Ludwig and Ford, Jr., Henry} 3 | } 4 | -------------------------------------------------------------------------------- /tests/resources/bibtex/3_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "de la Vallee Poussin, Charles Louis Xavier Joseph and von Beethoven, Ludwig and Ford Jr., Henry", 3 | "author_list": [ 4 | { 5 | "family": "de la Vallee Poussin", 6 | "given": "Charles Louis Xavier Joseph" 7 | }, 8 | { 9 | "family": "von Beethoven", 10 | "given": "Ludwig" 11 | }, 12 | { 13 | "family": "Ford Jr.", 14 | "given": "Henry" 15 | } 16 | ], 17 | "ref": "bibtextest3", 18 | "type": "article" 19 | } -------------------------------------------------------------------------------- /tests/resources/commands/update/positron.json: -------------------------------------------------------------------------------- 1 | [{"tags": "positron", "ref": "10.1103/PhysRev.43.491", "full_journal_title": "Physical Review", "url": "http://link.aps.org/article/10.1103/PhysRev.43.491", "journal": "Physical Review", "doi": "10.1103/PhysRev.43.491", "volume": "43", "last_page": "494", "title": "The Positive Electron", "author": "Anderson, Carl D.", "author_list": [{"surname": "Anderson", "given_name": "Carl D."}], "issue": "6", "first_page": "491", "abbrev_journal_title": "Phys. Rev.", "files": ["The-Positive-Electron-Anderson-Carl-D..pdf"], "year": "1933", "citations": [{"doi": "10.1119/1.1933285"}, {"doi": "10.1126/science.76.1967.238"}], "pages": "491--494", "doc_url": "http://harvest.aps.org/v2/journals/articles/10.1103/PhysRev.43.491/fulltext"}] -------------------------------------------------------------------------------- /tests/resources/commands/update/russell.yaml: -------------------------------------------------------------------------------- 1 | abbrev_journal_title: The Journal of Philosophy 2 | author: Russell, Bertrand 3 | author_list: 4 | - {given_name: Bertrand, surname: Russell} 5 | citations: [] 6 | doi: 10.2307/2021897 7 | files: [logic-and-ontology-russell-bertrand.pdf] 8 | full_journal_title: The Journal of Philosophy 9 | issue: '9' 10 | journal: The Journal of Philosophy 11 | month: '04' 12 | ref: 10.2307/2021897 13 | title: Logic and Ontology 14 | type: article 15 | volume: '54' 16 | year: '1957' 17 | -------------------------------------------------------------------------------- /tests/resources/commands/update/wannier.bib: -------------------------------------------------------------------------------- 1 | @article{10.1103PhysRev.52.191, 2 | author = {Wannier, Gregory H.}, 3 | doi = {10.1103/PhysRev.52.191}, 4 | issue = {3}, 5 | journal = {Physical Review}, 6 | month = {8}, 7 | pages = {191--197}, 8 | title = {The Structure of Electronic Excitation Levels in Insulating Crystals}, 9 | type = {article}, 10 | url = {http://link.aps.org/article/10.1103/PhysRev.52.191}, 11 | volume = {52}, 12 | year = {1937}, 13 | } 14 | -------------------------------------------------------------------------------- /tests/resources/config_1.ini: -------------------------------------------------------------------------------- 1 | [papers] 2 | dir = /tmp/papis/papers 3 | 4 | [settings] 5 | default = papers 6 | 7 | 8 | 9 | # vim: ft=dosini 10 | -------------------------------------------------------------------------------- /tests/resources/crossref/test_2_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "journal": "USURJ: University of Saskatchewan Undergraduate Research Journal", 3 | "title": "Industrial Potential of Polyhydroxyalkanoate Bioplastic: A Brief Review", 4 | "doc_url": "https://usurj.journals.usask.ca/article/download/55/17", 5 | "url": "http://dx.doi.org/10.32396/usurj.v1i1.55", 6 | "author": "Bernard, Matthew", 7 | "type": "article", 8 | "month": 2, 9 | "abstract": "In the international community, human dependence on plastic is increasing. Meanwhile, global petroleum reserves are diminishing. The cost of this demand on petroleum use is not only economic; there are also escalating human and animal health concerns, environmental implications, and the inherent obligation to prepare feasible alternatives in the event that petroleum depletion occurs. While the bioproduct industry is heavily invested in finding fuel substitutes, innovative efforts in other petroleum-dominated industries, such as plastics, may be worthwhile. Fortunately, there are naturally-occurring compounds in bacteria with structures analogous to those currently derived from petroleum. These compounds offer potentially sustainable and healthier alternatives to petroleum. One such compound gaining attention today is polyhydroxyalkanoate (PHA). PHA has several attractive properties as an achievable bioplastic source material, either as a direct substitute or as a blend with petroleum. Genetic modification (GM) may be necessary to achieve adequate yields; accordingly, source and host genetics, agronomic practices, and industry-related technology must be examined in this context. This review will compare properties of petroleum-based to PHA-derived plastics, as well as summarize the obligations of, mechanisms by, and implications with which PHA is being introduced to the plastic industry.", 10 | "doi": "10.32396/usurj.v1i1.55", 11 | "author_list": [ 12 | { 13 | "affiliation": [], 14 | "family": "Bernard", 15 | "given": "Matthew" 16 | } 17 | ], 18 | "year": 2014, 19 | "publisher": "University of Saskatchewan Library", 20 | "volume": "1", 21 | "issue": "1" 22 | } 23 | -------------------------------------------------------------------------------- /tests/resources/document/info.yaml: -------------------------------------------------------------------------------- 1 | abbrev_journal_title: The Journal of Philosophy 2 | author: Russell, Bertrand 3 | author_list: 4 | - {given_name: Bertrand, surname: Russell} 5 | citations: [] 6 | doi: 10.2307/2021897 7 | files: [logic-and-ontology-russell-bertrand.pdf] 8 | full_journal_title: The Journal of Philosophy 9 | issue: '9' 10 | journal: The Journal of Philosophy 11 | month: '04' 12 | ref: 10.2307/2021897 13 | title: Logic and Ontology 14 | type: article 15 | volume: '54' 16 | year: '1957' 17 | -------------------------------------------------------------------------------- /tests/resources/example_document.txt: -------------------------------------------------------------------------------- 1 | content 2 | -------------------------------------------------------------------------------- /tests/resources/minimal.ini: -------------------------------------------------------------------------------- 1 | [papers] 2 | dir = ~/Documents/papers 3 | 4 | [something_cool_and_complicated=???] 5 | dir = ~/Documents/books 6 | 7 | 8 | 9 | # vim: ft=dosini 10 | -------------------------------------------------------------------------------- /tests/test_arxiv.py: -------------------------------------------------------------------------------- 1 | import papis.downloaders 2 | from papis.arxiv import ( 3 | Downloader, get_data, find_arxivid_in_text, validate_arxivid 4 | ) 5 | import papis.bibtex 6 | 7 | def test_general(): 8 | data = get_data( 9 | author='Garnet Chan', 10 | max_results=1, 11 | title='Finite Temperature' 12 | ) 13 | assert(data) 14 | assert(len(data) == 1) 15 | 16 | 17 | def test_find_arxiv_id(): 18 | test_data = [ 19 | ('/URI(http://arxiv.org/abs/1305.2291v2)>>', '1305.2291v2'), 20 | ('/URI(http://arxiv.org/abs/1205.0093)>>', '1205.0093'), 21 | ('/URI(http://arxiv.org/abs/1205.1494)>>', '1205.1494'), 22 | ('/URI(http://arxiv.org/abs/1011.2840)>>', '1011.2840'), 23 | ('/URI(http://arxiv.org/abs/1110.3658)>>', '1110.3658'), 24 | ('http://arxiv.org/abs/1110.3658>', '1110.3658'), 25 | ('http://arxiv.com/abs/1110.3658>', '1110.3658'), 26 | ('http://arxiv.org/1110.3658>', '1110.3658'), 27 | ] 28 | for url, arxivid in test_data: 29 | assert(find_arxivid_in_text(url) == arxivid) 30 | 31 | 32 | def test_match(): 33 | assert(Downloader.match('arxiv.org/sdf')) 34 | assert(Downloader.match('arxiv.com/!@#!@$!%!@%!$chemed.6b00559') is False) 35 | 36 | down = Downloader.match('arXiv:1701.08223v2?234') 37 | assert(down) 38 | assert(down.uri == 'https://arxiv.org/abs/1701.08223v2') 39 | assert(down._get_identifier() == '1701.08223v2') 40 | 41 | 42 | def test_downloader_getter(): 43 | url = 'https://arxiv.org/abs/1001.3032' 44 | down = papis.downloaders.get_downloader(url) 45 | assert(down.name == 'arxiv') 46 | assert(down.expected_document_extension == 'pdf') 47 | #assert(down.get_doi() == '10.1021/ed044p128') 48 | assert(len(down.get_bibtex_url()) > 0) 49 | assert(len(down.get_bibtex_data()) > 0) 50 | bibs = papis.bibtex.bibtex_to_dict(down.get_bibtex_data()) 51 | assert(len(bibs) == 1) 52 | doc = down.get_document_data() 53 | assert(doc is not None) 54 | assert(down.check_document_format()) 55 | 56 | 57 | def test_validate_arxivid(): 58 | # good 59 | validate_arxivid('1206.6272') 60 | validate_arxivid('1206.6272v1') 61 | validate_arxivid('1206.6272v2') 62 | assert(True) 63 | 64 | for bad in ['1206.6272v3', 'blahv2']: 65 | try: 66 | validate_arxivid(bad) 67 | except ValueError: 68 | assert(True) 69 | else: 70 | assert(False) 71 | -------------------------------------------------------------------------------- /tests/test_bibtex.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import pytest 4 | 5 | import papis 6 | import papis.bibtex 7 | import papis.document 8 | 9 | import logging 10 | logging.basicConfig(level=logging.DEBUG) 11 | 12 | 13 | def test_bibtex_to_dict(): 14 | bibpath = os.path.join( 15 | os.path.dirname(__file__), "resources", "bibtex", "1.bib") 16 | bib = papis.bibtex.bibtex_to_dict(bibpath)[0] 17 | keys = [ 18 | "title", 19 | "author", 20 | "journal", 21 | "abstract", 22 | "volume", 23 | "issue", 24 | "pages", 25 | "numpages", 26 | "year", 27 | "month", 28 | "publisher", 29 | "doi", 30 | "url" 31 | ] 32 | print(bib) 33 | 34 | for key in keys: 35 | assert key in bib 36 | 37 | assert bib["type"] == "article" 38 | assert re.match(r".*Rev.*", bib["journal"]) 39 | assert re.match(r".*concurrent inter.*", bib["abstract"]) 40 | 41 | 42 | def test_bibkeys_exist(): 43 | assert(len(papis.bibtex.bibtex_keys) != 0) 44 | 45 | 46 | def test_bibtypes_exist(): 47 | assert(len(papis.bibtex.bibtex_types) != 0) 48 | 49 | 50 | @pytest.mark.parametrize("bibfile", ["1.bib", "2.bib", "3.bib"]) 51 | def test_author_list_conversion(bibfile, overwrite=False): 52 | jsonfile = "{}_out.json".format(os.path.splitext(bibfile)[0]) 53 | bibpath = os.path.join(os.path.dirname(__file__), 54 | "resources", "bibtex", bibfile) 55 | jsonpath = os.path.join(os.path.dirname(__file__), 56 | "resources", "bibtex", jsonfile) 57 | 58 | bib = papis.bibtex.bibtex_to_dict(bibpath)[0] 59 | if overwrite or not os.path.exists(jsonpath): 60 | with open(jsonpath, "w") as f: 61 | import json 62 | json.dump(bib, f, 63 | indent=2, 64 | sort_keys=True, 65 | ensure_ascii=False) 66 | 67 | with open(jsonpath, "r") as f: 68 | import json 69 | expected = json.loads(f.read()) 70 | 71 | assert bib['author_list'] == expected['author_list'] 72 | -------------------------------------------------------------------------------- /tests/test_crossref.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest.mock import patch 3 | import json 4 | from papis.crossref import ( 5 | get_data, doi_to_data 6 | ) 7 | 8 | 9 | def _get_test_json(filename): 10 | resources = os.path.join( 11 | os.path.dirname(os.path.abspath(__file__)), 12 | 'resources', 'crossref' 13 | ) 14 | filepath = os.path.join(resources, filename) 15 | with open(filepath) as fd: 16 | return json.load(fd) 17 | 18 | 19 | def test_get_data(): 20 | data = get_data( 21 | author='Albert Einstein', 22 | max_results=1, 23 | ) 24 | assert(data) 25 | assert(len(data) == 1) 26 | 27 | 28 | @patch( 29 | 'papis.crossref._get_crossref_works', 30 | lambda **x: _get_test_json('test1.json') 31 | ) 32 | def test_doi_to_data(): 33 | data = doi_to_data('10.1103/physrevb.89.140501') 34 | assert(isinstance(data, dict)) 35 | result = _get_test_json('test1_out.json') 36 | assert(result == data) 37 | 38 | 39 | @patch( 40 | 'papis.crossref._get_crossref_works', 41 | lambda **x: _get_test_json('test_2.json') 42 | ) 43 | def test_doi_to_data(): 44 | data = doi_to_data('10.1103/physrevb.89.140501') 45 | assert(isinstance(data, dict)) 46 | result = _get_test_json('test_2_out.json') 47 | assert(result == data) 48 | 49 | 50 | @patch( 51 | 'papis.crossref._get_crossref_works', 52 | lambda **x: _get_test_json('test_conference.json') 53 | ) 54 | def test_doi_to_data_conference(): 55 | data = doi_to_data('') 56 | assert(isinstance(data, dict)) 57 | result = _get_test_json('test_conference_out.json') 58 | assert(result == data) 59 | -------------------------------------------------------------------------------- /tests/test_deps.py: -------------------------------------------------------------------------------- 1 | def test_pygments(): 2 | # This function exists after version 2.2.0 only 3 | from pygments.lexers import find_lexer_class_by_name 4 | yaml = find_lexer_class_by_name('yaml') 5 | assert(yaml is not None) 6 | 7 | 8 | def test_colorama(): 9 | import colorama 10 | assert(colorama.Back) 11 | assert(colorama.Style) 12 | assert(colorama.Style.RESET_ALL) 13 | assert(colorama.Fore.RED) 14 | assert(colorama.Fore.YELLOW) 15 | assert(colorama.init) 16 | 17 | 18 | def test_prompt_toolkit(): 19 | from prompt_toolkit.formatted_text.html import HTML, html_escape 20 | from prompt_toolkit.application import Application 21 | 22 | from prompt_toolkit.history import FileHistory 23 | from prompt_toolkit.buffer import Buffer 24 | from prompt_toolkit.enums import EditingMode 25 | from prompt_toolkit.key_binding import KeyBindings 26 | from prompt_toolkit.layout.screen import Point 27 | from prompt_toolkit.layout.containers import HSplit, Window 28 | from prompt_toolkit.layout.controls import ( 29 | BufferControl, 30 | FormattedTextControl 31 | ) 32 | from prompt_toolkit.layout.layout import Layout 33 | from prompt_toolkit.widgets import ( 34 | HorizontalLine 35 | ) 36 | assert(True) 37 | -------------------------------------------------------------------------------- /tests/test_docmatcher.py: -------------------------------------------------------------------------------- 1 | from papis.docmatcher import parse_query, DocMatcher 2 | import os 3 | import yaml 4 | 5 | 6 | def get_docs(): 7 | yamlfile = os.path.join(os.path.dirname(__file__), 'data', 'licl.yaml') 8 | with open(yamlfile) as f: 9 | return list(yaml.safe_load_all(f)) 10 | 11 | 12 | def test_docmatcher(): 13 | 14 | DocMatcher.set_search("author:einstein") 15 | assert(DocMatcher.search == "author:einstein") 16 | DocMatcher.set_search("author:seitz") 17 | assert(DocMatcher.search == "author:seitz") 18 | 19 | DocMatcher.parse() 20 | assert(DocMatcher.parsed_search is not None) 21 | docs = get_docs() 22 | assert(len(list(docs)) == 16) 23 | for res in [(True, 16), (False, 0)]: 24 | DocMatcher.set_matcher(lambda doc, search, sformat: res[0]) 25 | filtered = list( 26 | filter(lambda x: x is not None, 27 | map(DocMatcher.return_if_match, docs))) 28 | assert(len(filtered) == res[1]) 29 | 30 | 31 | def test_parse_query(): 32 | r = parse_query('hello author : einstein') 33 | assert(r[0][0] == 'hello') 34 | assert(r[1][0] == 'author') 35 | assert(r[1][1] == ':') 36 | assert(r[1][2] == 'einstein') 37 | 38 | r = parse_query('doi : 123.123/124_123') 39 | re = ["doi", ":", "123.123/124_123"] 40 | for i in range(len(re)): 41 | assert(r[0][i] == re[i]) 42 | 43 | r = parse_query('doi : 123.123/124_123(80)12') 44 | re = ["doi", ":", "123.123/124_123(80)12"] 45 | for i in range(len(re)): 46 | assert(r[0][i] == re[i]) 47 | 48 | r = parse_query('tt : asfd author : "Albert einstein"') 49 | assert(r[0][0] == 'tt') 50 | assert(r[0][1] == ':') 51 | assert(r[0][2] == 'asfd') 52 | assert(r[1][0] == 'author') 53 | assert(r[1][1] == ':') 54 | assert(r[1][2] == 'Albert einstein') 55 | -------------------------------------------------------------------------------- /tests/test_importer.py: -------------------------------------------------------------------------------- 1 | from papis.importer import ( 2 | Importer, Context, cache, get_importer_by_name, available_importers 3 | ) 4 | import time 5 | 6 | 7 | def test_context(): 8 | ctx = Context() 9 | assert(ctx.data == dict()) 10 | assert(ctx.files == []) 11 | assert(not ctx) 12 | ctx.files = ['a'] 13 | assert(ctx) 14 | ctx.files = [] 15 | ctx.data['key'] = 42 16 | assert(ctx) 17 | ctx = Context() 18 | assert(not ctx) 19 | 20 | 21 | def test_cache(): 22 | 23 | data = {'time': time.time()} 24 | 25 | class SimpleImporter(Importer): 26 | 27 | def __init__(self, uri='', **kwargs): 28 | Importer.__init__(self, uri=uri, name='SimpleImporter', **kwargs) 29 | 30 | @classmethod 31 | def match(cls, uri): 32 | importer = SimpleImporter(uri=uri) 33 | importer.ctx.data = data 34 | return importer 35 | 36 | @cache 37 | def fetch(self): 38 | self.ctx.data = {'time': time.time()} 39 | 40 | importer = SimpleImporter() 41 | importer.fetch() 42 | assert(importer.ctx) 43 | assert(not importer.ctx.data['time'] == data['time']) 44 | 45 | importer = SimpleImporter.match('uri') 46 | importer.fetch() 47 | assert(importer.ctx.data['time'] == data['time']) 48 | 49 | 50 | def test_get_importer(): 51 | names = available_importers() 52 | assert(isinstance(names, list)) 53 | assert(names) 54 | for name in names: 55 | assert(get_importer_by_name(name) is not None) 56 | -------------------------------------------------------------------------------- /tests/test_isbn.py: -------------------------------------------------------------------------------- 1 | from papis.isbn import * 2 | 3 | def test_get_data(): 4 | mattuck = get_data(query='Mattuck feynan diagrams') 5 | assert(mattuck) 6 | assert(isinstance(mattuck, list)) 7 | assert(isinstance(mattuck[0], dict)) 8 | assert(mattuck[0]['isbn-13'] == '9780486670478') 9 | -------------------------------------------------------------------------------- /tests/tui/test_app.py: -------------------------------------------------------------------------------- 1 | from papis.tui.app import * 2 | import papis.config as config 3 | from prompt_toolkit.application.current import get_app 4 | 5 | 6 | def test_settings(): 7 | config.get('status_line_format', section='tui') 8 | config.get("status_line_style", section='tui') 9 | config.get('message_toolbar_style', section='tui') 10 | config.get('options_list.selected_margin_style', section='tui') 11 | config.get('options_list.unselected_margin_style', section='tui') 12 | config.get('error_toolbar_style', section='tui') 13 | config.get('move_down_key', section='tui') 14 | config.get('move_up_key', section='tui') 15 | config.get('move_down_while_info_window_active_key', section='tui') 16 | config.get('move_up_while_info_window_active_key', section='tui') 17 | config.get('focus_command_line_key', section='tui') 18 | config.get('edit_document_key', section='tui') 19 | config.get('open_document_key', section='tui') 20 | config.get('show_help_key', section='tui') 21 | config.get('show_info_key', section='tui') 22 | config.get('go_top_key', section='tui') 23 | config.get('go_bottom_key', section='tui') 24 | config.get("editmode", section='tui') 25 | 26 | ki = get_keys_info() 27 | -------------------------------------------------------------------------------- /tests/tui/widgets/test_command_line_prompt.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit.layout.containers import to_container 2 | from prompt_toolkit.key_binding import KeyBindings 3 | 4 | from papis.tui.widgets.command_line_prompt import * 5 | from papis.tui.widgets import * 6 | 7 | 8 | def test_simple_command(): 9 | cmd = Command('test', lambda c: 1+1) 10 | assert(cmd.app is not None) 11 | r = cmd() 12 | assert(r == 2) 13 | assert(cmd.names == ['test']) 14 | 15 | cmd = Command('test', lambda c: 1+1, aliases=['t', 'e']) 16 | assert(cmd.names == ['test', 't', 'e']) 17 | 18 | 19 | 20 | def test_commandlineprompt(): 21 | cmds = [Command('test', lambda c: 1+1)] 22 | prompt = CommandLinePrompt(commands=cmds) 23 | prompt.text = 'test' 24 | re = prompt.trigger() 25 | assert(re == 2) 26 | try: 27 | prompt.text = 'est' 28 | e = prompt.trigger() 29 | except Exception as e: 30 | assert(str(e) == 'No command found (est)') 31 | else: 32 | assert(False) 33 | 34 | prompt.text = '' 35 | assert(prompt.trigger() is None) 36 | 37 | prompt.commands = 2*[Command('test', lambda c: 1+1)] 38 | 39 | prompt.text = 'sdf asldfj dsafds' 40 | prompt.clear() 41 | assert(prompt.text == '') 42 | 43 | prompt.text = 'test' 44 | try: 45 | prompt.trigger() 46 | except Exception as e: 47 | assert(str(e) == 'More than one command matches the input') 48 | else: 49 | assert(False) 50 | 51 | -------------------------------------------------------------------------------- /tests/tui/widgets/test_general_widgets.py: -------------------------------------------------------------------------------- 1 | from papis.tui.widgets import * 2 | from prompt_toolkit.formatted_text.html import HTML 3 | from prompt_toolkit.application.current import get_app 4 | 5 | 6 | def test_message_toolbar(): 7 | app = get_app() 8 | mt = MessageToolbar() 9 | assert(not app.layout.has_focus(mt)) 10 | mt.text = 'Hello world' 11 | assert(mt.filter()) 12 | mt.text = '' 13 | assert(not mt.filter()) 14 | mt.text = None 15 | assert(not mt.filter()) 16 | 17 | 18 | def test_info_window(): 19 | app = get_app() 20 | iw = InfoWindow() 21 | assert(iw.text == '') 22 | iw.text = ' info' 23 | assert(iw.text == ' info') 24 | assert(not iw.filter()) 25 | app.layout.focus(iw.window) 26 | assert(app.layout.has_focus(iw)) 27 | # TODO: why this does not work? 28 | #assert(iw.filter()) 29 | 30 | 31 | def test_help_window(): 32 | hw = HelpWindow() 33 | assert(hw.text.value == '') 34 | hw.text = 'Help?' 35 | assert(hw.text == 'Help?') 36 | -------------------------------------------------------------------------------- /tests/tui/widgets/test_options_list.py: -------------------------------------------------------------------------------- 1 | from papis.tui.widgets.list import * 2 | from prompt_toolkit.layout.screen import Point 3 | import re 4 | 5 | 6 | def test_basic(): 7 | ol = OptionsList(['hello', 'world', '