pax_global_header00006660000000000000000000000064147123457200014517gustar00rootroot0000000000000052 comment=b9288f3fd3c6b0d093b7ddb77b467ca574c99b23 yokadi-1.3.0/000077500000000000000000000000001471234572000130005ustar00rootroot00000000000000yokadi-1.3.0/.coveragerc000066400000000000000000000001351471234572000151200ustar00rootroot00000000000000[report] exclude_lines = pragma: no cover raise NotImplementedError def __repr__ yokadi-1.3.0/.github/000077500000000000000000000000001471234572000143405ustar00rootroot00000000000000yokadi-1.3.0/.github/workflows/000077500000000000000000000000001471234572000163755ustar00rootroot00000000000000yokadi-1.3.0/.github/workflows/main.yml000066400000000000000000000016171471234572000200510ustar00rootroot00000000000000name: main on: workflow_dispatch: push: branches: - master - dev - sync pull_request: branches: - master - dev - sync jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install -r extra-requirements.txt pip install -r requirements-dev.txt - name: Lint run: | scripts/lint - name: Tests run: | scripts/coverage yokadi-1.3.0/.gitignore000066400000000000000000000002061471234572000147660ustar00rootroot00000000000000*.pyc *.db .*.swp .py* *~ .pydevproject .project .settings dist build MANIFEST .idea/ __pycache__/ .coverage htmlcov yokadi.egg-info/ yokadi-1.3.0/.markdownlint.yml000066400000000000000000000004121471234572000163070ustar00rootroot00000000000000default: true ul-indent: indent: 4 fenced-code-language: false line_length: false first-line-h1: false no-trailing-punctuation: false no-inline-html: false commands-show-output: false # Changelog files are full of duplicate headers no-duplicate-header: false yokadi-1.3.0/.pre-commit-config.yaml000066400000000000000000000015731471234572000172670ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - repo: https://github.com/igorshubovych/markdownlint-cli rev: v0.32.2 hooks: - id: markdownlint-fix - repo: local hooks: - id: tests name: tests entry: bash -c ". .venv/bin/activate && scripts/tests" language: system files: \.py$ pass_filenames: false - repo: local hooks: - id: lint name: lint entry: bash -c ". .venv/bin/activate && scripts/lint" language: system files: \.py$ pass_filenames: false # Check GitHub workflows - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.23.1 hooks: - id: check-github-workflows yokadi-1.3.0/CHANGELOG.md000066400000000000000000000170371471234572000146210ustar00rootroot00000000000000# Changelog ## 1.3.0 - 2024-11-05 - Update SQLAlchemy to 2.0.32. - Use color to for keywords in tables. - Fix crash handler failing on Windows. ## 1.2.0 - 2019-02-10 ### New features - The new `p_merge` command lets you merge a project into another. - It is now possible to turn a task into a note with `t_to_note` and a note into a task with `n_to_task`. ### Bug fixes - The `k_remove` command no longer ignores unused keywords. - HTML output has been fixed to no longer output strings wrapped in `b""`. - `t_list` filtering has been fixed so that `t_list --urgency 0` filters out tasks with a negative urgency, as expected. ### Improvements - HTML output has been refreshed: - It looks more modern now. - Some fields have been removed (doneDate, creationDate). - The title, keywords and description fields have been merged. - An ID field has been added (handy to run a command on a task listed in the output). - Columns now use human-friendly titles. ### Misc - The `--db` option is now deprecated and replaced by the `--datadir` option. `--db` will be removed in the next version. - Similarly, the `YOKADI_DB` environment variable is now deprecated and will be removed in the next version. - Yokadi no longer supports cryptography: encrypted databases will be decrypted at update. ## 1.1.1 - 2016-11-11 ### Improvements - When listing multiple projects, order them alphabetically. ### Bug fixes - Fixed parse error if the user sets a time of "17m". - When the user edits a tasks with t_edit and removes a keyword, remove the keyword from the task. - Made recurrence code work with dateutil 2.6.0. ## 1.1.0 - 2016-09-03 ### New features & Improvements - A new command has been added: `t_medit`. `t_medit` lets you edit all tasks of a project in one go. - Aliases can now be modified. The name of the alias can be modified with `a_edit_name` and the command with `a_edit_command`. - Database format updates are now easier to run: just run `yokadi -u`, no more separate `update.py` command. Updates are also much faster. - Task lists have been improved: - Borders look nicer. - Some bugs in the rendering of the title column have been fixed (wrong width, badly cropped text). - Yokadi now uses standard paths by default: the database is stored in ~/.local/share/yokadi/yokadi.db and non-essential data is in ~/.cache/yokadi/. - Reviewed and improved documentation. Moved developer documentation to a separate dir (doc/dev). ### Bug fixes - The code handling recurrences has been made more robust. - Recurrences are now stored in a more future proof way. - Fixed `bug_edit` crash. - Fixed negative keyword filter: A task with two keywords k1 and k2 would not be excluded by a filter !k1. ## 1.0.2 - 2016-03-28 - Use a more portable way to get the terminal size. This makes it possible to use Yokadi inside Android terminal emulators like Termux - Sometimes the task lock used to prevent editing the same task description from multiple Yokadi instances were not correctly released - Deleting a keyword from the database caused a crash when a t_list returned tasks which previously contained this keyword ## 1.0.1 - 2015-12-03 ### User changes - Make sure installing via pip installs the required dependencies ### Developer changes - Improved release process ## 1.0.0 - 2015-11-29 ### User changes - Fixed an issue which caused t_list to fail when filtering by keywords on large lists - Removed the project keywords feature. It was not very useful and made the searching code more complicated - Fixed ical support: it now works with ical 3.6 or later - Improved documentation - Added Keywords field to yokadi.desktop ### Developer changes - Yokadi has been ported to Python 3 - The application now uses SQLAlchemy instead of SQLObject to access the SQLite database ## 0.14 - 2014-05-03 ### Command changes - t_add, n_add: - Allow creating two tasks with the same title (useful for recurrent tasks, like "buy bread"). - Allow using _ to select last project, making it possible to do multiple t_add on the same project with `t_add _ `. - Add --describe option to start describing the task right after adding it. - t_describe, n_describe: - Safer task description editing: task is updated each time the editor saves, a lock manager now prevents multiple edits. - Use .md suffix instead of .txt for the temporary filename to allow some smart things with editors that understand Markdown. - Use project and task name for the temporary filename. Useful when using graphical editors or when your terminal title shows the current running command. - t_due: - When called with a time argument which is before current time, set due date to the day after. - t_show: - Show the task ID. - t_list: - Use month and year for the task age if the task is older than 12 months. - Add support for arbitrary minimum date for --done. - Fixed broken help. - n_list: - Display creation date instead of age. - Notes are now grouped by date. - p_list: - Show task count per project. - p_remove: - Show the number of associated tasks in the prompt. - p_edit: - Handle case where user tries to rename a project using the name of an existing project. ### yokadid - Add --restart option and --log option. - Set process name with setproctitle. - Configuration keys can now be overridden using environment variables. ### Misc - Date/time commands now support `%d/%m/%y` date format. - Replaced xyokadi with a desktop file. - Updated README to match real output. ### Developer specific changes - Command parser has been ported from optparse to argparse. - Code is now PEP 8 compliant, with the exception of camelCase usage. - All imports have been changed to absolute imports (ie `import yokadi.`). - Code has been reorganized into different sub directories. - The scripts in bin/ are now smart enough to run the source tree version instead of the installed version if possible. - We now use Travis for continuous integration. ## 0.13 - 2011-04-09 - cryptographic support to encrypt tasks title and description. - t_apply now accept id range (x-y). - Special keyword `__` can used in t_apply to affect all tasks previously select by t_list. ## 0.12 - 2010-07-06 - Negative keyword support. Ex.: `t_list !@home` - Permanent filters on keyword or project. `t_filter @foo` will filter any further call to t_list on @foo keyword. ## 0.11.1 - 2009-11-02 - yokadi symlink (useful to run yokadi without installing it) was broken ## 0.11 - 2009-11-01 - dynamic display width according to user terminal - display keywords in t_list - bugs keywords are prefixed with a `_` to distinguish them from user keywords - YOKADI_DB environment variable can be defined to set default yokadi database path - tasks can be grouped by keyword instead of project - special character `_` can be used to represent last task id - custom aliases can be defined for all commands with a_add - switch from GPL 3 to GPL v3 or newer license ## 0.10 - 2009-07-08 - ability to assign keywords to a project - shortened some commands (old ones still available but deprecated): - `t_set_due` => `t_due` - `t_set_project` => `t_project` - `t_set_urgency` => `t_urgency` - changed keyword syntax: use `@foo` instead of `-k foo` - added t_recurs command to define task recursion (weekly, monthly, yearly...) - added full text search with `t_list -s foo` - enhanced t_list display - added purge command (t_purge) to remove old tasks - added Windows support - fixed install script to be more friendly to both users and packagers ## 0.9 - 2009-02-07 First public release. Fully usable for home and work. yokadi-1.3.0/LICENSE000066400000000000000000001045131471234572000140110ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . yokadi-1.3.0/MANIFEST.in000066400000000000000000000004041471234572000145340ustar00rootroot00000000000000include doc/*.md include doc/dev/*.md include man/*.1 include icon/* include scripts/* include editors/vim/*/*.vim include *py include README.md include LICENSE include MANIFEST.in include version include CHANGELOG.md include *requirements.txt include .github yokadi-1.3.0/README.md000066400000000000000000000242611471234572000142640ustar00rootroot00000000000000[![Build Status](https://travis-ci.org/agateau/yokadi.png?branch=master)](https://travis-ci.org/agateau/yokadi) [![Coverage Status](https://coveralls.io/repos/agateau/yokadi/badge.png)](https://coveralls.io/r/agateau/yokadi) # What is it? Yokadi is a command-line oriented, SQLite powered, TODO list tool. It helps you organize all the things you have to do and must not forget. It aims to be simple, intuitive and very efficient. In Yokadi you manage projects, which contain tasks. At the minimum, a task has a title, but it can also have a description, a due date, an urgency or keywords. Keywords can be any word that help you find and sort your tasks. # Dependencies Yokadi should run on any Unix-like systems. There is also some support for Windows but it is not as tested. Yokadi requires Python 3.4 or more and a few other modules, which you can install with: pip install -r requirements.txt It can also make use of other modules listed in extra-requirements.txt. You can install them with: pip install -r extra-requirements.txt These modules are needed for the Yokadi Daemon. # Quickstart Here is an example of a short Yokadi session: Start Yokadi: ./bin/yokadi Creating database Added keyword '_severity' Added keyword '_likelihood' Added keyword '_bug' Added keyword '_note' Create your first task: yokadi> t_add birthday Buy food and drinks Project 'birthday' does not exist, create it (y/n)? y Added project 'birthday' Added task 'Buy food and drinks' (id=1) Add two other tasks, you can use _ to refer to last project used: yokadi> t_add _ Invite Bob Added task 'Invite Bob' (id=2) yokadi> t_add _ Invite Wendy Added task 'Invite Wendy' (id=3) List tasks for project "birthday": yokadi> t_list birthday birthday ID |Title |U |S|Age |Due date -------------------------------------------------------------------------------------------------- 1 |Buy food and drinks |0 |N|1m | 2 |Invite Bob |0 |N|0m | 3 |Invite Wendy |0 |N|0m | Once you have called Bob, you can mark task 2 as done: yokadi> t_mark_done 2 Task 'Invite Bob' marked as done yokadi> t_list birthday birthday ID |Title |U |S|Age |Due date -------------------------------------------------------------------------------------------------- 1 |Buy food and drinks |0 |N|2m | 3 |Invite Wendy |0 |N|1m | Task 2 has not disappeared, but `t_list` skips done tasks by default. To list all tasks use: yokadi> t_list birthday --all birthday ID |Title |U |S|Age |Due date -------------------------------------------------------------------------------------------------- 1 |Buy food and drinks |0 |N|2m | 2 |Invite Bob |0 |D|1m | 3 |Invite Wendy |0 |N|1m | To list only tasks marked as done today: yokadi> t_list birthday --done today birthday ID |Title |U |S|Age |Due date -------------------------------------------------------------------------------------------------- 2 |Invite Bob |0 |D|1m | You may want to attach your grocery list to task 1. This can be done with `t_describe`. yokadi> t_describe 1 This will start the editor specified in $EDITOR (or `vi` if not set) to enter a longer text, attached to the task. You can now display details of task 1: yokadi> t_show 1 Project: birthday Title: Buy food and drinks ID: 1 Created: 2009-01-09 08:57:33 Due: None Status: new Urgency: 0 Recurrence: None Keywords: - Orange juice - Coke - Beer - Cookies - Pizzas Note: `t_show` is not mandatory, just entering the task number will display its details. `t_list` indicates tasks which have a longer description with a `*` character: yokadi> t_list birthday birthday ID |Title |U |S|Age |Due date -------------------------------------------------------------------------------------------------- 1 |Buy food and drinks *|0 |N|3m | 3 |Invite Wendy |0 |N|2m | There is much more, we only scratched the surface, but this should get you started. You can get a list of all commands by typing `help` and get the detailed documentation of a command with `help `. # Advanced stuff ## Quick access to last task When you execute multiple commands on the same task, you can use `_` as a shortcut to the last task id. Assuming you created a task like this: yokadi> t_add home Buy chocolate Added task 'Buy chocolate' (id=1069) Then the following commands are equivalents (until you work on another task): yokadi> t_edit 1069 yokadi> t_edit _ ## Due dates You can define due dates for your tasks with `t_due`. This can be done with a relative or absolute date: yokadi> t_due 21 +3d Due date for task 'Buy chocolate' set to Sat Jul 11 17:16:20 2009 yokadi> t_due 21 23/07 10:30 Due date for task 'Buy chocolate' set to Thu Jul 23 10:30:00 2009 Due dates are shown by `t_list`. Due date is colored according to time left. If you want to be reminded when a task is due, you can use the Yokadi Daemon for that. See below for details. ## Periodic tasks If you have periodic tasks, you can tell it to Yokadi with `t_recurs`: yokadi> t_recurs 1 weekly monday 21:30 yokadi> t_recurs 1 monthly 3 11:00 yokadi> t_recurs 1 monthly last saturday 11:00 yokadi> t_recurs 1 yearly 23/2 14:00 Type `help t_recurs` to see all possible syntaxes. ## Tasks range and magic __ keyword `t_apply` is a very powerful function but sometimes you have to use it on numerous tasks. First, you can use task range like this: yokadi> t_apply 1-3 t_urgency 10 Executing: t_urgency 1 10 Executing: t_urgency 2 10 Executing: t_urgency 3 10 yokadi> But sometimes tasks are not consecutive and you would like to use wonderful `t_list` options to select your tasks. Here's the trick: each time you display tasks with `t_list`, Yokadi stores the id list in the magic keyword `__` that you can give to `t_apply` like this: yokadi> t_list @keyword myProject (...) yokadi> t_apply __ t_urgency 35 Oh, by the way, one Yokadi dev uses the following alias which is quite self explanatory: yokadi> a_list procrastinate => t_apply __ t_due +1d ## Mass editing tasks `t_medit` lets you edit all tasks of a project at once by opening a text editor with all the tasks and let you editing them, applying the changes when you quit. If you are familiar with `git`, this is similar to the way the `git rebase --interactive` command works. For example to edit all the tasks of the "birthday" project do the following: yokadi> t_medit birthday Make adjustments to the task list (the syntax is documented as comments in the text opened in the editor), then save the file and quit to apply the changes. Yokadi provides Vim syntax highlighting files to make mass edit more convenient. You can find them in `editors/vim`. To install them, run the following: cd place/to/editors/vim mkdir -p ~/.vim/ftdetect mkdir -p ~/.vim/syntax cp ftdetect/medit.vim ~/.vim/ftdetect cp syntax/medit.vim ~/.vim/syntax If you use another editor and can provide support for highlighting files, your contribution is very welcome! Get in touch so that we can add your work to the next version of Yokadi. # Integration ## Database location By default, Yokadi creates a database in `$HOME/.local/share/yokadi/yokadi.db`, but you can specify an alternative directory with the `--datadir` option. A convenient way to start Yokadi is by creating an alias in your `.bashrc` file like this: alias y=yokadi The single letter `y` will start Yokadi with your favorite database from wherever you are. ## History location By default, Yokadi will store input history in `$HOME/.cache/yokadi/history`. This file stores commands used in Yokadi for future use and reference. If you do now want to use the default history file location, you can define the `YOKADI_HISTORY` environment variable to point to your history file: export YOKADI_HISTORY=$HOME/.hist/yokadi_history ## Yokadid, the Yokadid daemon If you want to be automatically reminded of due tasks, you can use the Yokadi daemon. The Yokadi daemon can be launched via desktop autostart services. In most desktop environments, you just need to create a symbolic link to yokadid (or a shell script that calls it) in `$HOME/.config/autostart/`: ln -s `which yokadid` $HOME/.config/autostart/ # Contact The project is hosted on . All discussions happen on Yokadi mailing-list, hosted by our friends from the Sequanux LUG. To join, visit . You can also find some of us on #yokadi, on the Freenode IRC network. # Authors Yokadi has been brought to you by: - Aurélien Gâteau : Developer, founder - Sébastien Renard : Developer - Benjamin Port : Developer Other people contributed to Yokadi: - Olivier Hervieu : first working setup.py release - Marc-Antoine Gouillart : Windows port - Kartik Mistry : man pages - Jonas Christian Drewsen : quarterly recurrence feature yokadi-1.3.0/bin/000077500000000000000000000000001471234572000135505ustar00rootroot00000000000000yokadi-1.3.0/bin/yokadi000077500000000000000000000005241471234572000147570ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- """This is just a wrapper to yokadi package that rely in standard python site-package This wrapper is intended to be placed in user PATH and to be executable @author: Sébastien Renard (sebastien.renard@digitalfox.org) @license:GPL v3 or later """ from yokadi.ycli import main main.main() yokadi-1.3.0/bin/yokadid000077500000000000000000000005251471234572000151240ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- """This is just a wrapper to yokadi package that rely in standard python site-package This wrapper is intended to be placed in user PATH and to be executable @author: Sébastien Renard (sebastien.renard@digitalfox.org) @license:GPL v3 or later """ from yokadi import yokadid yokadid.main() yokadi-1.3.0/doc/000077500000000000000000000000001471234572000135455ustar00rootroot00000000000000yokadi-1.3.0/doc/bugtracking.md000066400000000000000000000065341471234572000163770ustar00rootroot00000000000000# Bugtracking ## Introduction Yokadi comes with a set of commands tailored to help you track bugs. These commands are `bug_add` and `bug_edit`. They are similar to `t_add` and `t_edit` except they will ask you a few questions to help you decide which bug to fix next. ## Entering a bug Enter a new bug like you would enter a new task: bug_add fooplayer Fooplayer crashes when opening a .bar file Before adding the task to the project "fooplayer", `bug_add` will ask you the severity of the bug: 1: Documentation 2: Localization 3: Aesthetic issues 4: Balancing: Enables degenerate usage strategies that harm the experience 5: Minor usability: Impairs usability in secondary scenarios 6: Major usability: Impairs usability in key scenarios 7: Crash: Bug causes crash or data loss. Asserts in the Debug release Severity: _ Enter 7 here, this is a crash. Now `bug_add` wants to know about the likelihood of the bug: 1: Will affect almost no one 2: Will only affect a few users 3: Will affect average number of users 4: Will affect most users 5: Will affect all users Likelihood: _ .bar files are quite uncommon, enter 2 here. We reach the last question: bug: _ This last question is optional: `bug_add` wants to know the id of this bug. This is where you can enter the Bugzilla/Trac/Mantis/... id of the bug. If you just noticed this bug and have not yet entered it in a centralized bug tracker, just press Enter. Yokadi will now add a task for your bug: Added bug 'Fooplayer crashes when opening a .bar file' (id=12, urgency=40) If you edit the task with `t_edit 12` you will only be able to fix the task title. To be asked for severity, likelihood and bug id again, use `bug_edit 12`. ## What's next? Based on the severity and likelihood, Yokadi computes the urgency of the bug. The formula used is: likelihood * severity * 100 urgency = ----------------------------- max_likelihood * max_severity This is based on the concept of "User Pain", as described by Danc here: Now, when you list your tasks with `t_list`, the most urgent tasks will be listed first, making it easy to fix the most important bugs first. ## Behind the scenes Likelihood, severity and bug are stored as Yokadi keywords (Yokadi keywords can be associated with an integer value). The bug urgency is computed from likelihood and severity, then stored in the task urgency field. Yes, this means there is duplication and you may get likelihood/severity and urgency out of sync if you manually adjust urgency with `t_set_urgency`. In practice, I found it was not a problem. ## Tricks Here are a few tricks I came up with while using Yokadi to do bug tracking: - List all crashers: `t_list fooplayer -k severity=7` - Make use of Yokadi keywords. For example I often use: - backport: I should backport the fix when done - i18n: This bug requires translation changes, better fix it before i18n freeze - patch: This bug as an attached patch (You can paste the patch in the bug description with `t_describe`) - Find a bug by id: `t_list fooplayer -k bug=12` - I often keep two projects in Yokadi, one for the stable release, another for development. For example I have `yokadi_stable` and `yokadi_dev`. yokadi-1.3.0/doc/dev/000077500000000000000000000000001471234572000143235ustar00rootroot00000000000000yokadi-1.3.0/doc/dev/db-updates.md000066400000000000000000000030611471234572000166750ustar00rootroot00000000000000# Database updates ## How the update system works Lets assume current version is x and target version is x+n. The update process goes like this: - Copy yokadi.db to work.db - for each v between x and x + n - 1: - run `updateto.update()` - Create an empty database in recreated.db - Fill recreated.db with the content of work.db - If we are updating the database in place, rename yokadi.db to yokadi-$date.db and recreated.db to yokadi.db - If we are creating a new database (only possible by directly calling update/update.py), rename recreated.db to the destination name; The recreation steps ensure that: - All fields are created in the same order (when adding a new column, you can't specify its position) - All constraints are in place (when adding a new column, you can't mark it 'non null') - The updated database has the exact same structure as a brand new database. ## Database schema changes If you want to modify the database schema (adding, removing, changing tables or fields). You should: - Present the changes on the mailing-list - Implement your changes in db.py - Increase the database version number (`DB_VERSION` in db.py) - Write an update script in update/ - When the changes are merged in master, tag the merge commit using the tag name `db-v`, like this: # Note the -a! git tag -a db-v git push --tags Note: up to db-v4, `db-v*` have been created on the last commit before the update to a new version, so `db-v4` is on the last commit before `DB_VERSION` was bumped to 5. yokadi-1.3.0/doc/dev/debug.md000066400000000000000000000002451471234572000157340ustar00rootroot00000000000000# Debugging ## Show SQL commands If you set the `YOKADI_SQL_DEBUG` environment variable to a value different from "0", all SQL commands will be printed to stdout. yokadi-1.3.0/doc/dev/hacking.md000066400000000000000000000044471471234572000162620ustar00rootroot00000000000000# Coding style ## Naming Classes use CamelCase. Functions use mixedCase. Here is an example: class MyClass(object): def myMethod(self, arg1, arg2): pass def anotherMethod(self, arg1, *args, **kwargs): pass Exception: Classes which implement command methods should use underscores, since the name of the method is used to create the name of the command: class MyCmd(object): def do_t_cmd1(self, line): pass def parser_t_cmd1(self): return SomeParser def someMethod(self): pass Note: This naming convention is historic, we would like to switch to a more PEP-8 compliant coding style where words in function and variable names are separated with `_`. If you feel like doing the conversion, get in touch. Filenames are lowercase. If they contain a class they should match the name of the class they contain. Internal functions and methods should be prefixed with `_`. ## Spacing Indentation is 4 spaces. Try to keep two blank lines between functions. One space before and after operators, except in optional arguments. a = 12 if a > 14 or a == 15: print a myFunction(a, verbose=True) ## Import Use one import per line: import os import sys Avoid polluting the local namespace with `from module import function`. Good: import os os.listdir(x) Bad: from os import listdir listdir(x) You should however import classes like this: from module import SomeClass Keep import in blocks, in this order: 1. Standard Python modules 2. Third-party modules 3. Yokadi modules Keep import blocks sorted. It makes it easier to check if an import line is already there. ## Command docstrings All commands are documented either through their parser or using the command docstring. To ensure consistency all usage string should follow the same guidelines. For example assuming your command is named `t_my_command`, which accepts a few options, two mandatory arguments (a task id and a search text) and an optional filename argument. The usage string should look like this: t_my_command [options] [] No need to detail the options in the usage string, they will be listed by the parser below the usage string. yokadi-1.3.0/doc/dev/release.md000066400000000000000000000023551471234572000162720ustar00rootroot00000000000000# Release check list ## Introduction This doc assumes there is a checkout of yokadi.github.com next to the checkout of yokadi. ## In yokadi checkout - [ ] Define version ``` export version= ``` - [ ] Check dev is clean ``` git checkout dev git pull git status ``` - [ ] Update `CHANGELOG.md` file (add changes, check release date) - [ ] Ensure `yokadi/__init__.py` file contains $version - [ ] Build archives ``` ./scripts/mkdist.sh ../yokadi.github.com/download ``` - [ ] Push changes ``` git push ``` - [ ] When CI has checked the branch, merge changes in master ``` git checkout master git pull git merge dev git push ``` - [ ] Tag the release ``` git tag -a $version -m "Releasing $version" git push --tags ``` ## In yokadi.github.com checkout - [ ] Ensure checkout is up to date - [ ] Update documentation ``` ./updatedoc.py ../yokadi . ``` - [ ] Update version in download page (`download.md`) - [ ] Write a blog entry in `_posts/` - [ ] Test it: ``` jekyll serve ``` - [ ] Upload archives on PyPI ``` cd download/ twine upload yokadi-.* ``` - [ ] Publish blog post ``` git add . git commit -m "Releasing $version" git push ``` yokadi-1.3.0/doc/ical.md000066400000000000000000000046221471234572000150030ustar00rootroot00000000000000# Ical support ## Introduction This document presents how to use Yokadi with a third party calendar/todolist application that supports the ical format (RFC2445). To use ical Yokadi features, start the Yokadi daemon with the --icalserver switch. This daemon also manages alarms for due tasks. The ical server listens on TCP port 8000. You can choose another TCP port with the --port switch. For example, to start Yokadi daemon with the icalserver on TCP port 9000: yokadid --icalserver --port=9000 ## Read your Yokadi tasks in a third party tool If your third party tool supports ical format and is able to read it through HTTP, just set it up to read on localhost:8000 (or whatever port you setup) and enjoy. If your calendar/todo tool only supports local files: * complain to your software broker to include ical over HTTP ;-) * make a simple shell script that downloads the ical file and put it on your crontab. You can use wget for that: wget -O yokadi.ical Each Yokadi task is defined as an ical VTODO object. Yokadi projects are represented as special tasks to which included tasks are related. ## Create and update yokadi tasks from a third party tool On the same TCP socket, you can write tasks with the PUT HTTP method. Only new and updated tasks will be considered. ## Supported third party ical tool Yokadi should support any tool which implements RFC2345. But we are not in a perfect world. The following tools are known to work properly with Yokadi ical server: * Kontact/KOrganizer (4.4) from the KDE Software Compilation If you successfully plugged Yokadi with another calendar/todolist tool, please let us now in order to complete this list. ## Some security considerations By default, the ical server only listens on localhost (loopback). You can bypass this restriction with the --listen switch which makes the ical server listen on all interfaces. If you do this, you will be able to access to the ical HTTP stream from another computer. But this have some security issues if you don't setup a firewall to restrict who can access to your Yokadi daemon: * everybody could access to your task list * even worse, everybody could be able to modify you task list * the ical server has not been build with strong security as design goals. You have been warned. That's why listening only to localhost (which is the default) is strongly recommended. yokadi-1.3.0/doc/tips.md000066400000000000000000000044351471234572000150540ustar00rootroot00000000000000# Tips ## Introduction This document presents practical advices on how to get the best out of Yokadi. ## Completion Yokadi supports completion of command names, and in many commands it can complete project names. Do not hesitate to try the `[tab]` key! ## Setting up a project hierarchy You can set up a project hierarchy by adopting a name convention. For example if you want to track tasks related to a program which is made of many plugins, you could have the main project named `fooplayer`, all tasks for the .ogg plugin stored in `fooplayer_ogg` and all tasks about the .s3m plugin in `fooplayer_s3m`. This makes it easy to categorize your tasks and also to have a general overview. For example to list all `fooplayer` related tasks you can use: t_list fooplayer% ## Using keywords Keywords are great to group tasks in different ways. For example you can create a keyword named `phone`, and assign it to tasks which you must accomplish on the phone. Another useful keyword is `diy_store`: Every time you find that you need to buy some supply from a do-it-yourself store, add it with this keyword. Next time you are planning a trip to the store, get the list of what to buy with: t_list @diy_store Or even nicer, directly print your list (from the shell): yokadi "t_list @diy_store --format plain" | lp ## Keep track of your meetings To track my meetings, I like to use a `meeting` keyword together with an assigned due date. Yokadi ability to add long descriptions to tasks is also handy to associate address or contact information to a meeting task. ## Keep track of tasks you delegate to people When you delegate a task to someone, add a keyword with its name to the task. So you can check that people really do what they promise to do even if they are not as organized as you are. To list all tasks assigned to Bob: t_list @bob To check all task that Bob should have done: t_list --overdue @bob ## Some useful shortcuts Yokadi relies on readline library, so you can use very useful readline shortcuts such as: - up/down arrows to browse history - ctrl-r to search backward in Yokadi history - ctrl-l to clear the screen - ctrl-t to swap two letters - ctrl-a to go the begin of the line - ctrl-e to go the end of the line - ctrl-w delete last word yokadi-1.3.0/editors/000077500000000000000000000000001471234572000144515ustar00rootroot00000000000000yokadi-1.3.0/editors/example.medit000066400000000000000000000012751471234572000171350ustar00rootroot000000000000001 N New task @kw1 @kw2=12 2 S Started task 3 D Done task 4 n Lower case should work too 5 s Lower case should work too 6 d Lower case should work too - Just added this new task in this session - N Another freshly added task, this time with explicit "new" status - S Another freshly added task, this time with explicit "started" status - D Another freshly added task, this time with explicit "done" status - SSS This added task starts with an S, but the S is part of the first word of this task - NNN This added task starts with an N, but the N is part of the first word of this task - DDD This added task starts with an D, but the D is part of the first word of this task Not a valid line # Comments yokadi-1.3.0/editors/vim/000077500000000000000000000000001471234572000152445ustar00rootroot00000000000000yokadi-1.3.0/editors/vim/ftdetect/000077500000000000000000000000001471234572000170465ustar00rootroot00000000000000yokadi-1.3.0/editors/vim/ftdetect/yokadimedit.vim000066400000000000000000000001031471234572000220600ustar00rootroot00000000000000au BufRead,BufNewFile *.medit set filetype=yokadimedit textwidth=0 yokadi-1.3.0/editors/vim/syntax/000077500000000000000000000000001471234572000165725ustar00rootroot00000000000000yokadi-1.3.0/editors/vim/syntax/yokadimedit.vim000066400000000000000000000015111471234572000216100ustar00rootroot00000000000000" Vim syntax file " Language: Yokadi t_medit " Maintainer: Aurélien Gâteau " Filenames: *.medit if exists("b:current_syntax") finish endif syn case match syn match yokadimeditComment "^\s*#.*$" skipwhite syn match yokadimeditTaskId "\v^\s*(\d+|-)" nextgroup=yokadimeditStatus skipwhite syn match yokadimeditError "^\s*[^-0-9#].*" skipwhite syn match yokadimeditStatus "[NSDnsd] " nextgroup=yokadimeditTitle contained syn match yokadimeditTitle ".*" contains=yokadimeditKeyword contained syn match yokadimeditKeyword "@\w\+" contained syn match yokadimeditKeyword "@\w\+=\d\+" contained hi def link yokadimeditComment Comment hi def link yokadimeditTaskId Constant hi def link yokadimeditStatus Statement hi def link yokadimeditKeyword Type hi def link yokadimeditError Error let b:current_syntax = "yokadimedit" yokadi-1.3.0/extra-requirements.txt000066400000000000000000000000431471234572000174020ustar00rootroot00000000000000icalendar==3.7 setproctitle==1.1.8 yokadi-1.3.0/icon/000077500000000000000000000000001471234572000137305ustar00rootroot00000000000000yokadi-1.3.0/icon/128x128/000077500000000000000000000000001471234572000146655ustar00rootroot00000000000000yokadi-1.3.0/icon/128x128/yokadi.png000066400000000000000000000025331471234572000166560ustar00rootroot00000000000000PNG  IHDR>asBIT|d pHYsnnޱtEXtSoftwarewww.inkscape.org<IDATx?n[GQZ'ހ%/WK@"% !Qs ;R'sualnY/@آnnnrC$zpppppppNy:c20>DqBǜ[/-/\c W|-滯l1}`+[w_b W|-滯l 8x1>^w=8-~LvV1"z}˟?*rcgX,n1"("U{)F rL~E ^f :#內'tD*x^L&oE{տ }w~|>g>7y ʯN*"@K~EH 3Wt@d$.(L_6\+D`~w-1aۙ?߆_V:c-YDEk " `ݲX,њ>ɇB8LSRZ7P`Álz^J+( {nNߒvՊrb~KB}o/8} HD7At`(AfHAbhA4fA4bA2dN2tA>(O"<xã|P_\^oMjz|\s?7%gs?7rkz\$2`1SS|OE!Ss?6ʇ &i1D $ z7#Is$9'D|%!ɇC3C`!gC`!ᧁC`!}`!A}|tM`""2^ܗ<ɇ7`pgPxFW`x{x)x#. by?"|H@?RT.Hq E )6@):7}ߜ^#v&@ӫxߍ#%;g`aG/_D<ssssssյ=+;IENDB`yokadi-1.3.0/icon/16x16/000077500000000000000000000000001471234572000145155ustar00rootroot00000000000000yokadi-1.3.0/icon/16x16/yokadi.png000066400000000000000000000004651471234572000165100ustar00rootroot00000000000000PNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDAT8 EMH'cQP h [2"br=wAC5@u?{wo_XJaR)!a 0˲hZ }(}N~U=<|>DaPaqT*v-oM8lnb+~XzB>m|}vaQX:8xWϡP~?`>Ry,zcQX]P@<,7@؏Git]vzxLNԶXY~I۶sIENDB`yokadi-1.3.0/icon/32x32/000077500000000000000000000000001471234572000145115ustar00rootroot00000000000000yokadi-1.3.0/icon/32x32/yokadi.png000066400000000000000000000007021471234572000164760ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYs^tEXtSoftwarewww.inkscape.org<?IDATX͗ E/SΤm Pԁi@ a7AWdS|f8y(鍂J/ڶ)xGPGs|u+'PU]M,An8=ΙRJR||E8PJa>J8TDP7csIB1DB'/Ӑ,H3! 'ȁGmTp`؃Q@|̬  XK=ip@D|U`D.<*&A$ R)/~*IENDB`yokadi-1.3.0/icon/48x48/000077500000000000000000000000001471234572000145275ustar00rootroot00000000000000yokadi-1.3.0/icon/48x48/yokadi.png000066400000000000000000000011331471234572000165130ustar00rootroot00000000000000PNG  IHDR00WsBIT|d pHYs))"ߌtEXtSoftwarewww.inkscape.org<IDATh0El;c!ڀڎh9V6K}ftc AWq8 ΙpfC/n(Qg`sөxd\$ 9&W, fLdL/0|&/\Z5}s{`e㜗HkD\q dYrZ)|6 g yNiu*3CFH ½ܟnK1P}\._8FL ΟDr<m?0 .@:W=n׻nY.c0ưC=2ϙf!j%`}_EL&!  VUbn4nWۏ ,Au]s88NTU5Hi7`,Ȳ3ɞp IY_O>dLKM$x? +|[8$]{DKK 룳P%y:ܯĂ=* s=Ub< jz/n 曨  H E$A)xP @<WSlZxywM(x!w $h38`)w,!fpZ!Pw~,.w>ѕ`RKHwTRƒ: 0b%K<|-A <CIyix0+A<Jg4Cma*|KFx4oK  x$@?uU鈿8IENDB`yokadi-1.3.0/icon/generate-pngs000077500000000000000000000003741471234572000164210ustar00rootroot00000000000000#!/bin/sh set -e SUFFIX=y for size in 16 22 32 48 64 128 ; do dir=${size}x${size} mkdir -p $dir inkscape --export-png $dir/yokadi.png \ --export-id=icon-$SUFFIX --export-width=$size --export-height=$size \ yokadi.svg done yokadi-1.3.0/icon/yokadi.desktop000066400000000000000000000003771471234572000166120ustar00rootroot00000000000000[Desktop Entry] Name=Yokadi GenericName=TODO list manager Comment=Command-line oriented TODO list manager Exec=yokadi Terminal=true Icon=yokadi Type=Application Categories=Office;ProjectManagement;ConsoleOnly; Keywords=todo;projectmanagement;commandline; yokadi-1.3.0/icon/yokadi.svg000066400000000000000000000424071471234572000157400ustar00rootroot00000000000000 image/svg+xml yokadi-1.3.0/man/000077500000000000000000000000001471234572000135535ustar00rootroot00000000000000yokadi-1.3.0/man/yokadi.1000066400000000000000000000025751471234572000151260ustar00rootroot00000000000000.TH YOKADI 1 "July 10, 2009" .SH NAME yokadi \- commandline todo system .SH SYNOPSIS .B yokadi .RI [ options ]... .br .SH DESCRIPTION .B yokadi is a command-line oriented, SQLite powered, TODO list tool. It helps you organize all the things you have to do and you must not forget. It aims to be simple, intuitive and very efficient. In Yokadi you manage projects, which contains tasks. At the minimum, a task has a title, but it can also have a description, a due date, an urgency or keywords. Keywords can be any word that help you to find and sort your tasks. .PP .SH OPTIONS These programs follow the usual GNU command line syntax, with long options starting with two dashes (`-'). A summary of options is included below. .TP .B \-\-datadir= Database directory. .TP .B \-c, \-\-create-only Just create an empty database. .TP .B \-u, \-\-update Update database to the latest version. .TP .B \-h, \-\-help Show summary of options and exit. .TP .B \-v, \-\-version Show version of program and exit. .SH SEE ALSO .BR yokadid (1). .br .SH SEE ALSO Website: http://yokadi.github.io Mailing List: http://sequanux.org/cgi-bin/mailman/listinfo/ml-yokadi .SH AUTHOR yokadi was written by Aurélien Gâteau and Sébastien Renard . .PP This manual page was written by Kartik Mistry , for the Debian project (and may be used by others). yokadi-1.3.0/man/yokadid.1000066400000000000000000000025431471234572000152650ustar00rootroot00000000000000.TH YOKADID 1 "July 10, 2009" .SH NAME yokadid \- commandline todo system .SH SYNOPSIS .B yokadid .RI [ options ]... .br .SH DESCRIPTION .B yokadid is a Yokadi daemon that remind you due tasks. If you want to be automatically reminded of due tasks, you can use the Yokadi daemon. The Yokadi daemon can be launched via desktop autostart services. In most desktop environments, you just need to create a symbolic link to yokadid (or a shell script that calls it) in $HOME/.config/autostart/. ln \-s \`which yokadid\` $HOME/.config/autostart/ .PP .SH OPTIONS These programs follow the usual GNU command line syntax, with long options starting with two dashes (`-'). A summary of options is included below. .TP .B \-\-datadir= Database directory. .TP .B \-k, \-\-kill Kill Yokadi Daemon (you can specify database with \-db if you run multiple Yokadid). .TP .B \-f, \-\-foreground Don't fork background. Useful for debug. .TP .B \-h, \-\-help Show summary of options and exit. .SH SEE ALSO .BR yokadi (1). .br .SH SEE ALSO Website: http://yokadi.github.io Mailing List: http://sequanux.org/cgi-bin/mailman/listinfo/ml-yokadi .SH AUTHOR yokadi was written by Aurélien Gâteau and Sébastien Renard . .PP This manual page was written by Kartik Mistry , for the Debian project (and may be used by others). yokadi-1.3.0/requirements-dev.txt000066400000000000000000000001001471234572000170270ustar00rootroot00000000000000coverage==7.6.0 flake8==7.1.0 pytest==8.3.2 pytest-xdist==3.6.1 yokadi-1.3.0/requirements.txt000066400000000000000000000001531471234572000162630ustar00rootroot00000000000000sqlalchemy==2.0.32 python-dateutil==2.8.2 colorama==0.4.6 pyreadline3==3.4.1; platform_system == 'Windows' yokadi-1.3.0/scripts/000077500000000000000000000000001471234572000144675ustar00rootroot00000000000000yokadi-1.3.0/scripts/coverage000077500000000000000000000002571471234572000162140ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail cd $(dirname $0)/.. coverage run --source=yokadi --omit="yokadi/tests/*" -m pytest yokadi/tests/tests.py coverage report coverage html yokadi-1.3.0/scripts/diffinst000077500000000000000000000037301471234572000162260ustar00rootroot00000000000000#!/usr/bin/env python3 """ @author: Aurélien Gâteau @license: GPL v3 or newer """ import argparse import fnmatch import os import tarfile import subprocess import sys DESCRIPTION = """\ Compare a tarball and a git tree, list files unique on each side. """ GIT_IGNORE = ( '.gitignore', ) TARBALL_IGNORE = ( 'PKG-INFO', ) def list_git_dir(root): out = subprocess.check_output(['git', 'ls-files'], cwd=root) return [x.decode() for x in out.splitlines()] def remove_first_dir(path): lst = path.split(os.sep) return os.path.join(*lst[1:]) def list_tarball(tarball): with tarfile.open(tarball) as tf: for info in tf.getmembers(): if info.isfile(): yield remove_first_dir(info.name) def apply_blacklist(lst, blacklist): for item in lst: for pattern in blacklist: if fnmatch.fnmatch(item, pattern): break else: yield item def print_set(st): for item in sorted(list(st)): print(item) def main(): parser = argparse.ArgumentParser() parser.description = DESCRIPTION parser.add_argument('-q', '--quiet', action='store_true', help='Do not list changes') parser.add_argument('tarball') parser.add_argument('git_dir', nargs='?', default='.') args = parser.parse_args() dir_set = set(apply_blacklist(list_git_dir(args.git_dir), GIT_IGNORE)) tb_set = set(apply_blacklist(list_tarball(args.tarball), TARBALL_IGNORE)) only_in_dir = dir_set.difference(tb_set) only_in_tb = tb_set.difference(dir_set) if not args.quiet: if only_in_dir: print('# Only in {}'.format(args.git_dir)) print_set(only_in_dir) if only_in_tb: print('# Only in {}'.format(args.tarball)) print_set(only_in_tb) if only_in_dir or only_in_tb: return 1 else: return 0 if __name__ == '__main__': sys.exit(main()) # vi: ts=4 sw=4 et yokadi-1.3.0/scripts/lint000077500000000000000000000001211471234572000153550ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail cd $(dirname $0)/.. echo "flake8" flake8 yokadi-1.3.0/scripts/mkdist.sh000077500000000000000000000025761471234572000163330ustar00rootroot00000000000000#!/bin/sh set -e PROGNAME="$(basename "$0")" die() { echo "$PROGNAME: ERROR: $*" | fold -s -w "${COLUMNS:-80}" >&2 exit 1 } log() { echo "### $*" >&2 } [ $# = 1 ] || die "USAGE: $PROGNAME " SRC_DIR=$(cd "$(dirname $0)/.." ; pwd) DST_DIR=$(cd "$1" ; pwd) [ -d "$DST_DIR" ] || die "Destination dir '$DST_DIR' does not exist" WORK_DIR=$(mktemp -d "$DST_DIR/yokadi-dist.XXXXXX") log "Copying source" cp -a --no-target-directory "$SRC_DIR" "$WORK_DIR" log "Cleaning" cd "$WORK_DIR" git reset --hard HEAD git clean -q -dxf log "Building archives" python3 -m venv create "$WORK_DIR/venv" ( . "$WORK_DIR/venv/bin/activate" pip install build python -m build ) rm -rf "$WORK_DIR/venv" log "Installing archive" cd dist/ YOKADI_TARGZ=$(ls ./*.tar.gz) tar xf "$YOKADI_TARGZ" ARCHIVE_DIR="$PWD/${YOKADI_TARGZ%.tar.gz}" python3 -m venv create "$WORK_DIR/venv" ( . "$WORK_DIR/venv/bin/activate" # Install Yokadi in the virtualenv and make sure it can be started # That ensures dependencies got installed by pip log "Smoke test" pip install "$ARCHIVE_DIR" yokadi exit log "Installing extra requirements" pip install -r "$ARCHIVE_DIR/extra-requirements.txt" log "Running tests" "$ARCHIVE_DIR/yokadi/tests/tests.py" ) log "Moving archives out of work dir" cd "$WORK_DIR/dist" mv *.tar.gz *.whl "$DST_DIR" rm -rf "$WORK_DIR" log "Done" yokadi-1.3.0/scripts/tests000077500000000000000000000001411471234572000155530ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail cd $(dirname $0)/.. pytest -n auto yokadi/tests/tests.py yokadi-1.3.0/setup.cfg000066400000000000000000000001051471234572000146150ustar00rootroot00000000000000[flake8] exclude = build,w32_postinst.py,.venv max-line-length = 120 yokadi-1.3.0/setup.py000077500000000000000000000045101471234572000145150ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Setup script used to build and install Yokadi @author: Sébastien Renard (sebastien.renard@digitalfox.org) @license:GPL v3 or newer """ from setuptools import setup import sys import os from fnmatch import fnmatch from os.path import isdir, dirname, join sys.path.insert(0, dirname(__file__)) import yokadi # noqa: E402 def createFileList(sourceDir, *patterns): """ List files from sourceDir which match one of the pattern in patterns Returns the path including sourceDir """ for name in os.listdir(sourceDir): for pattern in patterns: if fnmatch(name, pattern): yield join(sourceDir, name) # Additional files data_files = [] data_files.append(["share/yokadi", ["README.md", "CHANGELOG.md", "LICENSE"]]) # Doc data_files.append(["share/yokadi/doc", createFileList("doc", "*.md")]) # Man data_files.append(["share/man/man1", createFileList("man", "*.1")]) # Editor scripts data_files.append(["share/yokadi/editors/vim/ftdetect", ["editors/vim/ftdetect/yokadimedit.vim"]]) data_files.append(["share/yokadi/editors/vim/syntax", ["editors/vim/syntax/yokadimedit.vim"]]) # Icon for size in os.listdir("icon"): if not isdir(join("icon", size)): continue data_files.append(["share/icons/hicolor/%s/apps" % size, ["icon/%s/yokadi.png" % size]]) data_files.append(["share/applications", ["icon/yokadi.desktop"]]) # Scripts scripts = ["bin/yokadi", "bin/yokadid"] # Windows post install script if "win" in " ".join(sys.argv[1:]): scripts.append("w32_postinst.py") # Go for setup setup( name="yokadi", version=yokadi.__version__, description="Command line oriented todo list system", author="The Yokadi Team", author_email="ml-yokadi@sequanux.org", url="http://yokadi.github.io/", packages=[ "yokadi", "yokadi.core", "yokadi.tests", "yokadi.update", "yokadi.ycli", "yokadi.yical", ], # distutils does not support install_requires, but pip needs it to be # able to automatically install dependencies install_requires=[ "sqlalchemy ~= 2.0.32", "python-dateutil ~= 2.8.2", "colorama ~= 0.4.6", "pyreadline3 ~= 3.4.1 ; platform_system == 'Windows'", ], scripts=scripts, data_files=data_files ) yokadi-1.3.0/tox.ini000066400000000000000000000004261471234572000143150ustar00rootroot00000000000000[flake8] max_line_length=120 # E402 module level import not at top of file # N802 function name should be lowercase # N803 argument name should be lowercase # N806 variable in function should be lowercase # W503 line break before binary operator ignore=E402,N802,N803,N806,W503 yokadi-1.3.0/w32_postinst.py000066400000000000000000000043431471234572000157340ustar00rootroot00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- """ Post installation script for win32 system Thanks to the coin coin projet for the inspiration for this postinstall script @author: Sébastien Renard (sebastien.renard@digitalfox.org) @license:GPL v3 or newer """ from os.path import abspath, join from os import mkdir import sys # pylint: disable-msg=E0602 # Description string desc = "Command line oriented todo list system" # Shortcut name lnk = "yokadi.lnk" # Only do things at install stage, not uninstall if sys.argv[1] == "-install": # Get python.exe path py_path = abspath(join(sys.prefix, "python.exe")) # Yokadi wrapper path yokadi_dir = abspath(join(sys.prefix, "scripts")) yokadi_path = join(yokadi_dir, "yokadi") # TODO: create a sexy yokadi .ico file to be put in share dir # Find desktop try: desktop_path = get_special_folder_path("CSIDL_COMMON_DESKTOPDIRECTORY") except OSError: desktop_path = get_special_folder_path("CSIDL_DESKTOPDIRECTORY") # Desktop shortcut creation create_shortcut(py_path, # program to launch desc, join(desktop_path, lnk), # shortcut file yokadi_path, # Argument (pythohn script) yokadi_dir, # Current work dir "" # Ico file (nothing for now) ) # Tel install process that we create a file so it can removed it during uninstallation file_created(join(desktop_path, lnk)) # Start menu shortcut creation try: start_path = get_special_folder_path("CSIDL_COMMON_PROGRAMS") except OSError: start_path = get_special_folder_path("CSIDL_PROGRAMS") # Menu folder creation programs_path = join(start_path, "Yokadi") try: mkdir(programs_path) except OSError: pass directory_created(programs_path) create_shortcut(py_path, # program to launch desc, join(programs_path, lnk), # Shortcut file yokadi_path, # Argument (python script) yokadi_dir, # Cuurent work dir "" # Icone ) file_created(join(programs_path, lnk)) # End of script sys.exit() yokadi-1.3.0/yokadi/000077500000000000000000000000001471234572000142605ustar00rootroot00000000000000yokadi-1.3.0/yokadi/__init__.py000066400000000000000000000003171471234572000163720ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Yokadi main package @author: Aurélien Gâteau @author: Sébastien Renard @license:GPL v3 or later """ __version__ = "1.3.0" yokadi-1.3.0/yokadi/core/000077500000000000000000000000001471234572000152105ustar00rootroot00000000000000yokadi-1.3.0/yokadi/core/__init__.py000066400000000000000000000002701471234572000173200ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Yokadi core package @author: Aurélien Gâteau @author: Sébastien Renard @license:GPL v3 or later """ yokadi-1.3.0/yokadi/core/basepaths.py000066400000000000000000000057231471234572000175430ustar00rootroot00000000000000# -*- coding: UTF-8 -*- """ Provide standard ways to get various dirs This is similar to th pyxdg module but it does not automatically creates the dirs. Not creating the dirs is important to be able to show default values in `yokadid --help` output without creating anything. @author: Aurélien Gâteau @license: GPL v3 or later """ import os import getpass import shutil import tempfile from yokadi.core import fileutils _WINDOWS = os.name == "nt" DB_NAME = "yokadi.db" class MigrationException(Exception): pass def _getAppDataDir(): assert _WINDOWS return os.environ["APPDATA"] def getRuntimeDir(): value = os.getenv("XDG_RUNTIME_DIR") if not value: # Running on a system where XDG_RUNTIME_DIR is not set, fallback to # $tempdir/yokadi-$user tmpdir = tempfile.gettempdir() value = os.path.join(tmpdir, "yokadi-" + getpass.getuser()) return value def getLogDir(): return getCacheDir() def getCacheDir(): if _WINDOWS: value = os.path.join(_getAppDataDir(), "yokadi", "cache") else: cacheBaseDir = os.getenv("XDG_CACHE_HOME") if not cacheBaseDir: cacheBaseDir = os.path.expandvars("$HOME/.cache") value = os.path.join(cacheBaseDir, "yokadi") return value def getDataDir(): xdgDataDir = os.environ.get("XDG_DATA_HOME") if xdgDataDir: return os.path.join(xdgDataDir, "yokadi") if _WINDOWS: return os.path.join(_getAppDataDir(), "yokadi", "data") return os.path.expandvars("$HOME/.local/share/yokadi") def getHistoryPath(): path = os.getenv("YOKADI_HISTORY") if path: return path return os.path.join(getCacheDir(), "history") def getDbPath(dataDir): path = os.getenv("YOKADI_DB") if path: return path return os.path.join(dataDir, "yokadi.db") def _getOldHistoryPath(): if _WINDOWS: return os.path.join(_getAppDataDir(), ".yokadi_history") else: return os.path.expandvars("$HOME/.yokadi_history") def migrateOldHistory(): oldHistoryPath = _getOldHistoryPath() if not os.path.exists(oldHistoryPath): return newHistoryPath = getHistoryPath() if os.path.exists(newHistoryPath): # History is not critical, just overwrite the new file os.unlink(newHistoryPath) fileutils.createParentDirs(newHistoryPath) shutil.move(oldHistoryPath, newHistoryPath) print("Moved %s to %s" % (oldHistoryPath, newHistoryPath)) def migrateOldDb(newDbPath): oldDbPath = os.path.normcase(os.path.expandvars("$HOME/.yokadi.db")) if not os.path.exists(oldDbPath): return if os.path.exists(newDbPath): raise MigrationException("Tried to move %s to %s, but %s already exists." " You must remove one of the two files." % (oldDbPath, newDbPath, newDbPath)) fileutils.createParentDirs(newDbPath) shutil.move(oldDbPath, newDbPath) print("Moved %s to %s" % (oldDbPath, newDbPath)) yokadi-1.3.0/yokadi/core/bugutils.py000066400000000000000000000035311471234572000174220ustar00rootroot00000000000000# -*- coding: UTF-8 -*- """ Bug related commands. @author: Aurélien Gâteau @license: GPL v3 or later """ from yokadi.ycli import tui SEVERITY_PROPERTY_NAME = "_severity" LIKELIHOOD_PROPERTY_NAME = "_likelihood" BUG_PROPERTY_NAME = "_bug" PROPERTY_NAMES = SEVERITY_PROPERTY_NAME, LIKELIHOOD_PROPERTY_NAME, BUG_PROPERTY_NAME SEVERITY_LIST = [ (1, "Documentation"), (2, "Localization"), (3, "Aesthetic issues"), (4, "Balancing: Enables degenerate usage strategies that harm the experience"), (5, "Minor usability: Impairs usability in secondary scenarios"), (6, "Major usability: Impairs usability in key scenarios"), (7, "Crash: Bug causes crash or data loss. Asserts in the Debug release"), ] LIKELIHOOD_LIST = [ (1, "Will affect almost no one"), (2, "Will only affect a few users"), (3, "Will affect average number of users"), (4, "Will affect most users"), (5, "Will affect all users"), ] def computeUrgency(keywordDict): likelihood = keywordDict[LIKELIHOOD_PROPERTY_NAME] severity = keywordDict[SEVERITY_PROPERTY_NAME] maxUrgency = LIKELIHOOD_LIST[-1][0] * SEVERITY_LIST[-1][0] return int(100 * likelihood * severity / maxUrgency) def editBugKeywords(keywordDict): severity = keywordDict.get(SEVERITY_PROPERTY_NAME, None) likelihood = keywordDict.get(LIKELIHOOD_PROPERTY_NAME, None) bug = keywordDict.get(BUG_PROPERTY_NAME, None) severity = tui.selectFromList(SEVERITY_LIST, prompt="Severity", default=severity) likelihood = tui.selectFromList(LIKELIHOOD_LIST, prompt="Likelihood", default=likelihood) bug = tui.enterInt(prompt="bug", default=bug) keywordDict[BUG_PROPERTY_NAME] = bug if severity: keywordDict[SEVERITY_PROPERTY_NAME] = severity if likelihood: keywordDict[LIKELIHOOD_PROPERTY_NAME] = likelihood # vi: ts=4 sw=4 et yokadi-1.3.0/yokadi/core/daemon.py000066400000000000000000000076551471234572000170420ustar00rootroot00000000000000#!/usr/bin/env python3 """ This class comes from: http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/ and is licensed as Public Domain (see comment in article) """ import sys import os import time import atexit import errno from signal import SIGTERM class Daemon: """ A generic daemon class. Usage: subclass the Daemon class and override the run() method """ def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): self.stdin = stdin self.stdout = stdout self.stderr = stderr self.pidfile = pidfile def daemonize(self): """ do the UNIX double-fork magic, see Stevens' "Advanced Programming in the UNIX Environment" for details (ISBN 0201563177) http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 """ try: pid = os.fork() if pid > 0: # exit first parent sys.exit(0) except OSError as e: sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) sys.exit(1) # decouple from parent environment os.chdir("/") os.setsid() os.umask(0) # do second fork try: pid = os.fork() if pid > 0: # exit from second parent sys.exit(0) except OSError as e: sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) sys.exit(1) # redirect standard file descriptors sys.stdout.flush() sys.stderr.flush() si = open(self.stdin, 'r', encoding='utf-8', buffering=1) so = open(self.stdout, 'a+', encoding='utf-8', buffering=1) se = open(self.stderr, 'a+', encoding='utf-8', buffering=1) os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) # write pidfile atexit.register(self.delpid) pid = str(os.getpid()) open(self.pidfile, 'w+', encoding='utf-8').write("%s\n" % pid) def delpid(self): os.remove(self.pidfile) def start(self): """ Start the daemon """ # Check for a pidfile to see if the daemon already runs try: pf = open(self.pidfile, 'r', encoding='utf-8') pid = int(pf.read().strip()) pf.close() except IOError: pid = None if pid: message = "pidfile %s already exist. Daemon already running?\n" sys.stderr.write(message % self.pidfile) sys.exit(1) # Start the daemon self.daemonize() self.run() def stop(self): """ Stop the daemon """ # Get the pid from the pidfile try: pf = open(self.pidfile, 'r', encoding='utf-8') pid = int(pf.read().strip()) pf.close() except IOError: pid = None if not pid: message = "pidfile %s does not exist. Daemon not running?\n" sys.stderr.write(message % self.pidfile) return # not an error in a restart # Try killing the daemon process try: while 1: os.kill(pid, SIGTERM) time.sleep(0.1) except OSError as err: if err.errno == errno.ESRCH: # No such process, meaning daemon has stopped if os.path.exists(self.pidfile): os.remove(self.pidfile) else: print(str(err)) sys.exit(1) def restart(self): """ Restart the daemon """ self.stop() self.start() def run(self): """ You should override this method when you subclass Daemon. It will be called after the process has been daemonized by start() or restart(). """ yokadi-1.3.0/yokadi/core/db.py000066400000000000000000000333721471234572000161570ustar00rootroot00000000000000# -*- coding: UTF-8 -*- """ Database access layer using SQL Alchemy @author: Sébastien Renard @license: GPL v3 or later """ import json import os from datetime import datetime from uuid import uuid1 from sqlalchemy import create_engine, inspect from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.orm import scoped_session, sessionmaker, relationship, declarative_base from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.exc import IntegrityError from sqlalchemy import Column, Integer, Boolean, Unicode, DateTime, Enum, ForeignKey, UniqueConstraint from sqlalchemy.types import TypeDecorator, VARCHAR from yokadi.core.recurrencerule import RecurrenceRule from yokadi.core.yokadiexception import YokadiException # Yokadi database version needed for this code # If database config key DB_VERSION differs from this one a database migration # is required DB_VERSION = 12 DB_VERSION_KEY = "DB_VERSION" class DbUserException(Exception): """ This exception is for errors which are not caused by a failure in our code and which must be fixed by the user. """ pass Base = declarative_base() NOTE_KEYWORD = "_note" def uuidGenerator(): return str(uuid1()) class Project(Base): __tablename__ = "project" id = Column(Integer, primary_key=True) uuid = Column(Unicode, unique=True, default=uuidGenerator, nullable=False) name = Column(Unicode, unique=True) active = Column(Boolean, default=True) tasks = relationship("Task", cascade="all", backref="project", cascade_backrefs=False) def __repr__(self): return self.name def merge(self, session, other): """Merge other into us This function calls session.commit() itself: we have to commit after moving the tasks but *before* deleting `other` otherwise when we delete `other` SQLAlchemy deletes its former tasks as well because it thinks they are still attached to `other`""" if self is other: raise YokadiException("Cannot merge a project into itself") for task in other.tasks: task.projectId = self.id session.commit() session.delete(other) session.commit() class Keyword(Base): __tablename__ = "keyword" id = Column(Integer, primary_key=True) name = Column(Unicode, unique=True) tasks = association_proxy("taskKeywords", "task") taskKeywords = relationship("TaskKeyword", cascade="all", backref="keyword", cascade_backrefs=False) def __repr__(self): return self.name class TaskKeyword(Base): __tablename__ = "task_keyword" __mapper_args__ = {"confirm_deleted_rows": False} id = Column(Integer, primary_key=True) taskId = Column("task_id", Integer, ForeignKey("task.id"), nullable=False) keywordId = Column("keyword_id", Integer, ForeignKey("keyword.id"), nullable=False) value = Column(Integer, default=None) __table_args__ = ( UniqueConstraint("task_id", "keyword_id", name="task_keyword_uc"), ) def __repr__(self): return "".format(self.task, self.keyword, self.value) class RecurrenceRuleColumnType(TypeDecorator): """Represents an ydateutils.RecurrenceRule column """ impl = VARCHAR def process_bind_param(self, value, dialect): if value: value = json.dumps(value.toDict()) else: value = "" return value def process_result_value(self, value, dialect): if value: dct = json.loads(value) value = RecurrenceRule.fromDict(dct) else: value = RecurrenceRule() return value class Task(Base): __tablename__ = "task" id = Column(Integer, primary_key=True) uuid = Column(Unicode, unique=True, default=uuidGenerator, nullable=False) title = Column(Unicode) creationDate = Column("creation_date", DateTime, nullable=False, default=datetime.now) dueDate = Column("due_date", DateTime, default=None) doneDate = Column("done_date", DateTime, default=None) description = Column(Unicode, default="", nullable=False) urgency = Column(Integer, default=0, nullable=False) status = Column(Enum("new", "started", "done"), default="new") recurrence = Column(RecurrenceRuleColumnType, nullable=False, default=RecurrenceRule()) projectId = Column("project_id", Integer, ForeignKey("project.id"), nullable=False) taskKeywords = relationship("TaskKeyword", cascade="all", backref="task", cascade_backrefs=False) lock = relationship("TaskLock", cascade="all", backref="task", cascade_backrefs=False) def setKeywordDict(self, dct): """ Defines keywords of a task. Dict is of the form: keywordName => value """ session = getSession() for taskKeyword in self.taskKeywords: session.delete(taskKeyword) for name, value in list(dct.items()): keyword = session.query(Keyword).filter_by(name=name).one() session.add(TaskKeyword(task=self, keyword=keyword, value=value)) def getKeywordDict(self): """ Returns all keywords of a task as a dict of the form: keywordName => value """ dct = {} for taskKeyword in self.taskKeywords: dct[taskKeyword.keyword.name] = taskKeyword.value return dct def getKeywordsAsString(self): """ Returns all keywords as a string like "key1=value1, key2=value2..." """ return ", ".join(list(("%s=%s" % k for k in list(self.getKeywordDict().items())))) def getUserKeywordsNameAsString(self): """ Returns all keywords keys as a string like "@key1 @key2 @key3...". Internal keywords (starting with _) are ignored. """ keywords = [k for k in list(self.getKeywordDict().keys()) if not k.startswith("_")] keywords.sort() if keywords: return " ".join(f"@{k}" for k in keywords) else: return "" def setStatus(self, status): """ Defines the status of the task, taking care of updating the done date and doing the right thing for recurrent tasks """ if self.recurrence and status == "done": self.dueDate = self.recurrence.getNext(self.dueDate) else: self.status = status if status == "done": self.doneDate = datetime.now().replace(second=0, microsecond=0) else: self.doneDate = None def setRecurrenceRule(self, rule): """Set recurrence and update the due date accordingly""" self.recurrence = rule self.dueDate = rule.getNext() @staticmethod def getNoteKeyword(session): return session.query(Keyword).filter_by(name=NOTE_KEYWORD).one() def toNote(self, session): session.add(TaskKeyword(task=self, keyword=Task.getNoteKeyword(session), value=None)) try: session.flush() except IntegrityError: # Already a note session.rollback() return def toTask(self, session): noteKeyword = Task.getNoteKeyword(session) try: taskKeyword = session.query(TaskKeyword).filter_by(task=self, keyword=noteKeyword).one() except NoResultFound: # Already a task return session.delete(taskKeyword) def isNote(self, session): noteKeyword = Task.getNoteKeyword(session) return any((x.keyword == noteKeyword for x in self.taskKeywords)) def __repr__(self): return "".format(self.id, self.title) class Config(Base): """yokadi config""" __tablename__ = "config" id = Column(Integer, primary_key=True) name = Column(Unicode, unique=True) value = Column(Unicode) system = Column(Boolean) desc = Column(Unicode) class TaskLock(Base): __tablename__ = "task_lock" id = Column(Integer, primary_key=True) taskId = Column("task_id", Integer, ForeignKey("task.id"), unique=True, nullable=False) pid = Column(Integer, default=None) updateDate = Column("update_date", DateTime, default=None) class Alias(Base): __tablename__ = "alias" uuid = Column(Unicode, unique=True, default=uuidGenerator, nullable=False, primary_key=True) name = Column(Unicode, unique=True, nullable=False) command = Column(Unicode, nullable=False) @staticmethod def getAsDict(session): dct = {} for alias in session.query(Alias).all(): dct[alias.name] = alias.command return dct @staticmethod def add(session, name, command): alias = Alias(name=name, command=command) session.add(alias) @staticmethod def rename(session, name, newName): alias = session.query(Alias).filter_by(name=name).one() alias.name = newName @staticmethod def setCommand(session, name, command): alias = session.query(Alias).filter_by(name=name).one() alias.command = command def getConfigKey(name, environ=True): session = getSession() if environ: return os.environ.get(name, session.query(Config).filter_by(name=name).one().value) else: return session.query(Config).filter_by(name=name).one().value _database = None def getSession(): global _database if not _database: raise YokadiException("Cannot get session. Not connected to database") return _database.session def connectDatabase(dbFileName, createIfNeeded=True, memoryDatabase=False): global _database _database = Database(dbFileName, createIfNeeded, memoryDatabase) class Database(object): def __init__(self, dbFileName, createIfNeeded=True, memoryDatabase=False, updateMode=False): """Connect to database and create it if needed @param dbFileName: path to database file @type dbFileName: str @param createIfNeeded: Indicate if database must be created if it does not exists (default True) @type createIfNeeded: bool @param memoryDatabase: create db in memory. Only usefull for unit test. Default is false. @type memoryDatabase: bool @param updateMode: allow to use it without checking version. Default is false. @type updateMode: bool """ dbFileName = os.path.abspath(dbFileName) if memoryDatabase: connectionString = "sqlite:///:memory:" else: connectionString = 'sqlite:///' + dbFileName echo = os.environ.get("YOKADI_SQL_DEBUG", "0") != "0" self.engine = create_engine(connectionString, echo=echo) self.session = scoped_session(sessionmaker(bind=self.engine)) if not os.path.exists(dbFileName) or memoryDatabase: if not createIfNeeded: raise DbUserException("Database file (%s) does not exist or is not readable." % dbFileName) if not memoryDatabase: print("Creating %s" % dbFileName) self.createTables() # Set database version according to current yokadi release # Don't do it in updateMode: the update script adds the version from the dump if not updateMode: self.session.add(Config(name=DB_VERSION_KEY, value=str(DB_VERSION), system=True, desc="Database schema release number")) self.session.commit() if not updateMode: self.checkVersion() def createTables(self): """Create all defined tables""" Base.metadata.create_all(self.engine) def getVersion(self): if not self._hasConfigTable(): # There was no Config table in v1 return 1 try: return int(self.session.query(Config).filter_by(name=DB_VERSION_KEY).one().value) except NoResultFound: raise YokadiException("Configuration key '%s' does not exist. This should not happen!" % DB_VERSION_KEY) def setVersion(self, version): assert self._hasConfigTable() instance = self.session.query(Config).filter_by(name=DB_VERSION_KEY).one() instance.value = str(version) self.session.add(instance) self.session.commit() def _hasConfigTable(self): inspector = inspect(self.engine) return inspector.has_table("config") def checkVersion(self): """Check version and exit if it is not suitable""" version = self.getVersion() if version != DB_VERSION: msg = "Your database version is {} but Yokadi wants version {}.\n".format(version, DB_VERSION) msg += "Please run Yokadi with the --update option to update your database." raise DbUserException(msg) def setDefaultConfig(): """Set default config parameter in database if they (still) do not exist""" defaultConfig = { "ALARM_DELAY_CMD": ('''kdialog --passivepopup "task {TITLE} ({ID}) is due for {DATE}" 180 --title "Yokadi: {PROJECT}"''', False, "Command executed by Yokadi Daemon when a tasks due date is reached soon (see ALARM_DELAY"), "ALARM_DUE_CMD": ('''kdialog --passivepopup "task {TITLE} ({ID}) should be done now" 1800 --title "Yokadi: {PROJECT}"''', False, "Command executed by Yokadi Daemon when a tasks due date is reached soon (see ALARM_DELAY"), "ALARM_DELAY": ("8", False, "Delay (in hours) before due date to launch the alarm (see ALARM_CMD)"), "ALARM_SUSPEND": ("1", False, "Delay (in hours) before an alarm trigger again"), "PURGE_DELAY": ("90", False, "Default delay (in days) for the t_purge command"), } session = getSession() for name, value in defaultConfig.items(): if session.query(Config).filter_by(name=name).count() == 0: session.add(Config(name=name, value=value[0], system=value[1], desc=value[2])) # vi: ts=4 sw=4 et yokadi-1.3.0/yokadi/core/dbutils.py000066400000000000000000000214551471234572000172370ustar00rootroot00000000000000# -*- coding: UTF-8 -*- """ Database utilities. @author: Aurélien Gâteau @author: Sébastien Renard @license: GPL v3 or later """ from datetime import datetime, timedelta import os from sqlalchemy import and_ from sqlalchemy.orm import aliased from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound from yokadi.ycli import tui from yokadi.core import db from yokadi.core.db import Keyword, Project, Task, TaskKeyword, TaskLock from yokadi.core.yokadiexception import YokadiException def addTask(projectName, title, keywordDict=None, interactive=True): """Adds a task based on title and keywordDict. @param projectName: name of project as a string @param title: task title as a string @param keywordDict: dictionary of keywords (name : value) @param interactive: Ask user before creating project (this is the default) @type interactive: Bool @returns : Task instance on success, None if cancelled.""" session = db.getSession() if keywordDict is None: keywordDict = {} # Create missing keywords if not createMissingKeywords(keywordDict.keys(), interactive=interactive): return None # Create missing project project = getOrCreateProject(projectName, interactive=interactive) if not project: return None # Create task task = Task(creationDate=datetime.now().replace(second=0, microsecond=0), project=project, title=title, description="", status="new") session.add(task) task.setKeywordDict(keywordDict) session.flush() return task def getTaskFromId(tid): """Returns a task given its id, or raise a YokadiException if it does not exist. @param tid: Task id or uuid @return: Task instance or None if existingTask is False""" session = db.getSession() if isinstance(tid, str) and '-' in tid: filters = dict(uuid=tid) else: try: # We do not use line.isdigit() because it returns True if line is '¹'! taskId = int(tid) except ValueError: raise YokadiException("task id should be a number") filters = dict(id=taskId) try: task = session.query(Task).filter_by(**filters).one() except NoResultFound: raise YokadiException("Task %s does not exist. Use t_list to see all tasks" % taskId) return task def getOrCreateKeyword(keywordName, interactive=True): """Get a keyword by its name. Create it if needed @param keywordName: keyword name as a string @param interactive: Ask user before creating keyword (this is the default) @type interactive: Bool @return: Keyword instance or None if user cancel creation""" session = db.getSession() try: return session.query(Keyword).filter_by(name=keywordName).one() except (NoResultFound, MultipleResultsFound): if interactive and not tui.confirm("Keyword '%s' does not exist, create it" % keywordName): return None keyword = Keyword(name=keywordName) session.add(keyword) print("Added keyword '%s'" % keywordName) return keyword def getOrCreateProject(projectName, interactive=True, createIfNeeded=True): """Get a project by its name. Create it if needed @param projectName: project name as a string @param interactive: Ask user before creating project (this is the default) @type interactive: Bool @param createIfNeeded: create project if it does not exist (this is the default) @type createIfNeeded: Bool @return: Project instance or None if user cancel creation or createIfNeeded is False""" session = db.getSession() try: return session.query(Project).filter_by(name=projectName).one() except (NoResultFound, MultipleResultsFound): if not createIfNeeded: return None if interactive and not tui.confirm("Project '%s' does not exist, create it" % projectName): return None project = Project(name=projectName) session.add(project) print("Added project '%s'" % projectName) return project def createMissingKeywords(lst, interactive=True): """Create all keywords from lst which does not exist @param lst: list of keyword @return: True, if ok, False if user canceled""" for keywordName in lst: if not getOrCreateKeyword(keywordName, interactive=interactive): return False return True def getKeywordFromName(name): """Returns a keyword from its name, which may start with "@" raises a YokadiException if not found @param name: the keyword name @return: The keyword""" session = db.getSession() if not name: raise YokadiException("No keyword supplied") if name.startswith("@"): name = name[1:] lst = session.query(Keyword).filter_by(name=name).all() if len(lst) == 0: raise YokadiException("No keyword named '%s' found" % name) return lst[0] def splitKeywordDict(dct): """Take a keyword dict and return a tuple of the form (userDict, reservedDict) """ userDict = {} reservedDict = {} for key, value in dct.items(): if key[0] == '_': reservedDict[key] = value else: userDict[key] = value return userDict, reservedDict class TaskLockManager: """Handle a lock to prevent concurrent editing of the same task""" def __init__(self, task): """ @param task: a Task instance @param session: sqlalchemy session""" self.task = task self.session = db.getSession() def _getLock(self): """Retrieve the task lock if it exists (else None)""" try: return db.getSession().query(TaskLock).filter(TaskLock.task == self.task).one() except NoResultFound: return None def acquire(self, pid=None, now=None): """Acquire a lock for that task and remove any previous stale lock""" if now is None: now = datetime.now() if pid is None: pid = os.getpid() lock = self._getLock() if lock: if lock.updateDate < now - 2 * timedelta(seconds=tui.MTIME_POLL_INTERVAL): # Stale lock, reusing it lock.pid = pid lock.updateDate = now else: raise YokadiException("Task %s is already locked by process %s" % (lock.task.id, lock.pid)) else: # Create a lock self.session.add(TaskLock(task=self.task, pid=pid, updateDate=now)) self.session.commit() def update(self, now=None): """Update lock timestamp to avoid it to expire""" if now is None: now = datetime.now() lock = self._getLock() lock.updateDate = now self.session.commit() def release(self): """Release the lock for that task""" # Only release our lock lock = self._getLock() if lock and lock.pid == os.getpid(): self.session.delete(lock) self.session.commit() class DbFilter(object): """ Light wrapper around SQL Alchemy filters. Makes it possible to have the same interface as KeywordFilter """ def __init__(self, condition): self.condition = condition def apply(self, lst): return lst.filter(self.condition) class KeywordFilter(object): """Represent a filter on a keyword""" def __init__(self, name, negative=False, value=None, valueOperator=None): self.name = name # Keyword name assert self.name, "Keyword name cannot be empty" self.negative = negative # Negative filter self.value = value # Keyword value self.valueOperator = valueOperator # Operator to compare value def __repr__(self): return "".format( self.name, self.negative, self.value, self.valueOperator) def apply(self, query): """Apply keyword filters to query @return: a new query""" if self.negative: session = db.getSession() excludedTaskIds = session.query(Task.id).join(TaskKeyword).join(Keyword) \ .filter(Keyword.name.like(self.name)) return query.filter(~Task.id.in_(excludedTaskIds)) else: keywordAlias = aliased(Keyword) taskKeywordAlias = aliased(TaskKeyword) query = query.outerjoin(taskKeywordAlias, Task.taskKeywords) query = query.outerjoin(keywordAlias, taskKeywordAlias.keyword) filter = keywordAlias.name.like(self.name) if self.valueOperator == "=": filter = and_(filter, taskKeywordAlias.value == self.value) elif self.valueOperator == "!=": filter = and_(filter, taskKeywordAlias.value != self.value) return query.filter(filter) # vi: ts=4 sw=4 et yokadi-1.3.0/yokadi/core/fileutils.py000066400000000000000000000004301471234572000175570ustar00rootroot00000000000000""" Various file utility functions @author: Aurélien Gâteau @license: GPL v3 or later """ import os def createParentDirs(path, mode=0o777): parent = os.path.dirname(path) if os.path.exists(parent): return os.makedirs(parent, mode=mode) yokadi-1.3.0/yokadi/core/recurrencerule.py000066400000000000000000000142241471234572000206120ustar00rootroot00000000000000""" Date utilities. @author: Aurélien Gâteau @license: GPL v3 or later """ from datetime import datetime from dateutil import rrule from yokadi.core.ydateutils import getHourAndMinute, getWeekDayNumberFromDay, parseHumaneDateTime from yokadi.core.yokadiexception import YokadiException FREQUENCIES = {0: "Yearly", 1: "Monthly", 2: "Weekly", 3: "Daily"} ALL_DAYS = (rrule.MO, rrule.TU, rrule.WE, rrule.TH, rrule.FR, rrule.SA, rrule.SU) class RecurrenceRule(object): """Thin wrapper around dateutil.rrule which brings: - Serialization to/from dict - Parsing methods - Sane defaults (byhour = byminute = bysecond = 0) - __eq__ operator - Readable name Dict format: freq: 0..3, see FREQUENCIES dict bymonth: tuple<1..12> bymonthday: tuple<1..31> byweekday: tuple<0..6> or {pos: -1;1..4, weekday: 0..6} byhour: tuple<0..23> byminute: tuple<0..59> Constructor arguments: same as dict format except tuples can be int or None for convenience """ def __init__(self, freq=None, bymonth=None, bymonthday=None, byweekday=None, byhour=0, byminute=0): def tuplify(value): if value is None: return () if isinstance(value, int): return (value,) else: return tuple(value) self._freq = freq self._bymonth = tuplify(bymonth) self._bymonthday = tuplify(bymonthday) if isinstance(byweekday, dict): self._byweekday = byweekday else: self._byweekday = tuplify(byweekday) self._byhour = tuplify(byhour) self._byminute = tuplify(byminute) @staticmethod def fromDict(dct): if not dct: return RecurrenceRule() return RecurrenceRule(**dct) @staticmethod def fromHumaneString(line): """Take a string following t_recurs format, returns a RecurrenceRule instance or None """ freq = byminute = byhour = byweekday = bymonthday = bymonth = None tokens = line.split() tokens[0] = tokens[0].lower() if tokens[0] == "none": return RecurrenceRule() if tokens[0] == "daily": if len(tokens) != 2: raise YokadiException("You should give time for daily task") freq = rrule.DAILY byhour, byminute = getHourAndMinute(tokens[1]) elif tokens[0] == "weekly": freq = rrule.WEEKLY if len(tokens) != 3: raise YokadiException("You should give day and time for weekly task") byweekday = getWeekDayNumberFromDay(tokens[1].lower()) byhour, byminute = getHourAndMinute(tokens[2]) elif tokens[0] in ("monthly", "quarterly"): if tokens[0] == "monthly": freq = rrule.MONTHLY else: # quarterly freq = rrule.YEARLY bymonth = (1, 4, 7, 10) if len(tokens) < 3: raise YokadiException("You should give day and time for %s task" % (tokens[0],)) try: bymonthday = int(tokens[1]) byhour, byminute = getHourAndMinute(tokens[2]) except ValueError: POSITION = {"first": 1, "second": 2, "third": 3, "fourth": 4, "last": -1} if tokens[1].lower() in POSITION and len(tokens) == 4: byweekday = RecurrenceRule.createWeekDay( weekday=getWeekDayNumberFromDay(tokens[2].lower()), pos=POSITION[tokens[1]]) byhour, byminute = getHourAndMinute(tokens[3]) bymonthday = None # Default to current day number - need to be blanked else: raise YokadiException("Unable to understand date. See help t_recurs for details") elif tokens[0] == "yearly": freq = rrule.YEARLY rDate = parseHumaneDateTime(" ".join(tokens[1:])) bymonth = rDate.month bymonthday = rDate.day byhour = rDate.hour byminute = rDate.minute else: raise YokadiException("Unknown frequency. Available: daily, weekly, monthly and yearly") return RecurrenceRule( freq, bymonth=bymonth, bymonthday=bymonthday, byweekday=byweekday, byhour=byhour, byminute=byminute, ) def toDict(self): if not self: return {} return dict( freq=self._freq, bymonth=self._bymonth, bymonthday=self._bymonthday, byweekday=self._byweekday, byhour=self._byhour, byminute=self._byminute ) def _rrule(self): if isinstance(self._byweekday, dict): day = ALL_DAYS[self._byweekday["weekday"]] byweekday = day(self._byweekday["pos"]) else: byweekday = self._byweekday return rrule.rrule( freq=self._freq, bymonth=self._bymonth, bymonthday=self._bymonthday, byweekday=byweekday, byhour=self._byhour, byminute=self._byminute, bysecond=0 ) def getNext(self, refDate=None): """Return next date of recurrence after given date @param refDate: reference date used to compute the next occurence of recurrence @type refDate: datetime @return: next occurence (datetime)""" if not self: return None if refDate is None: refDate = datetime.now() refDate.replace(second=0, microsecond=0) return self._rrule().after(refDate) def getFrequencyAsString(self): """Return a string for the frequency""" if not self: return "" return FREQUENCIES[self._freq] @staticmethod def createWeekDay(pos, weekday): return dict(pos=pos, weekday=weekday) def __eq__(self, other): return self.toDict() == other.toDict() def __bool__(self): return self._freq is not None def __repr__(self): return repr(self.toDict()) # vi: ts=4 sw=4 et yokadi-1.3.0/yokadi/core/ydateutils.py000066400000000000000000000206511471234572000177550ustar00rootroot00000000000000# -*- coding: UTF-8 -*- """ Date utilities. @author: Sébastien Renard @license: GPL v3 or later """ import operator from datetime import date, datetime, timedelta from yokadi.ycli import basicparseutils from yokadi.core.yokadiexception import YokadiException WEEKDAYS = {"monday": 0, "tuesday": 1, "wednesday": 2, "thursday": 3, "friday": 4, "saturday": 5, "sunday": 6} SHORT_WEEKDAYS = {"mo": 0, "tu": 1, "we": 2, "th": 3, "fr": 4, "sa": 5, "su": 6} TIME_HINT_BEGIN = "begin" TIME_HINT_END = "end" DATE_FORMATS = [ "%d/%m/%Y", "%d/%m/%y", "%d/%m", ] TIME_FORMATS = [ "%H:%M:%S", "%H:%M", "%H", ] def parseDateTimeDelta(line): # FIXME: Do we really want to support float deltas? try: delta = float(line[:-1]) except ValueError: raise YokadiException("Timeshift must be a float or an integer") suffix = line[-1].upper() if suffix == "W": return timedelta(days=delta * 7) elif suffix == "D": return timedelta(days=delta) elif suffix == "H": return timedelta(hours=delta) elif suffix == "M": return timedelta(minutes=delta) else: raise YokadiException("Unable to understand time shift. See help t_set_due") def testFormats(text, formats): for fmt in formats: try: return datetime.strptime(text, fmt), fmt except ValueError: pass return None, None def guessTime(text): afternoon = False # We do not use the "%p" format to handle AM/PM because its behavior is # locale-dependent suffix = text[-2:] if suffix == "am": text = text[:-2].strip() elif suffix == "pm": afternoon = True text = text[:-2].strip() out, fmt = testFormats(text, TIME_FORMATS) if out is None: return None if afternoon: out += timedelta(hours=12) return out.time() def parseHumaneDateTime(line, hint=None, today=None): """Parse human date and time and return structured datetime object Datetime can be absolute (23/10/2008 10:38) or relative (+5M, +3H, +1D, +6W) @param line: human date / time @param hint: optional hint to tell whether time should be set to the beginning or the end of the day when not specified. @param today: optional parameter to define a fake today date. Useful for unit testing. @type line: str @return: datetime object""" def guessDate(text): out, fmt = testFormats(text, DATE_FORMATS) if not out: return None if "%y" not in fmt and "%Y" not in fmt: out = out.replace(year=today.year) return out.date() def applyTimeHint(date, hint): if not hint: return date if hint == TIME_HINT_BEGIN: return date.replace(hour=0, minute=0, second=0) elif hint == TIME_HINT_END: return date.replace(hour=23, minute=59, second=59) else: raise Exception("Unknown hint %s" % hint) line = basicparseutils.simplifySpaces(line).lower() if not line: raise YokadiException("Date is empty") if today is None: today = datetime.today().replace(microsecond=0) if line == "now": return today if line == "today": return applyTimeHint(today, hint) # Check for "+" format if line.startswith("+"): return today + parseDateTimeDelta(line[1:]) if line.startswith("-"): return today - parseDateTimeDelta(line[1:]) # Check for " [