Skip to content

Commit 5105742

Browse files
committed
Merge branch 'master' into a6seedregen
2 parents 43b1f91 + 943d0fd commit 5105742

30 files changed

+990
-349
lines changed

.github/workflows/ci.yaml

+21-6
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,40 @@ on: [push, pull_request, workflow_dispatch]
44

55
jobs:
66
tests:
7-
runs-on: ubuntu-20.04
7+
name: ${{ matrix.os }}-latest / ${{ matrix.python-version }}
8+
runs-on: ${{ matrix.os }}-latest
89
strategy:
9-
matrix:
10-
python-version: [3.6, 3.7, 3.8, 3.9, "3.10", "3.11"]
1110
fail-fast: false
11+
matrix:
12+
os:
13+
- ubuntu
14+
# - windows
15+
# - macos
16+
python-version:
17+
# - "3.6" # not supported in ubuntu-latest (22.04)
18+
- "3.7"
19+
- "3.8"
20+
- "3.9"
21+
- "3.10"
22+
- "3.11"
23+
- "3.12"
24+
1225
steps:
13-
- uses: actions/checkout@v2
26+
- uses: actions/checkout@v4
1427

1528
- name: Set up Python ${{ matrix.python-version }}
16-
uses: actions/setup-python@v2
29+
uses: actions/setup-python@v5
1730
with:
1831
python-version: ${{ matrix.python-version }}
32+
cache: pip
33+
allow-prereleases: true
1934

2035
- name: Install dependencies
2136
run: |
2237
sudo apt update
2338
sudo apt-get install -y libxml2-dev libxmlsec1-dev
2439
python -m pip install --upgrade pip
25-
pip install -r requirements.txt
40+
pip install .[test]
2641
2742
- name: Run tests
2843
run: |

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ dist/
44
*.egg-info/
55
build/
66
*.xml
7+
Pipfile
78
Pipfile.lock
89
*.kdbx
910
*.kdbx.out
11+
.idea
12+
.venv*
13+
TMPNOTES

CHANGELOG.rst

+17
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
4.1.0 - 2024-06-26
2+
------------------
3+
- merged #389 - add PyKeePass.database_name and database_description
4+
- merged #392, fixed #390 - fix pkg_resources dependency issue
5+
- fixed #391 - Entry.tags returns empty list instead of None
6+
- fixed #395 - set 'encoding' attribute when exporting as XML
7+
- fixed #383 - parse datetimes using isoformat instead of strptime
8+
9+
4.0.7 - 2024-02-29
10+
------------------
11+
- fixed #359 - PyKeePass has `decrypt` kwarg for accessing header info
12+
- merged #347 - added Entry.index and Entry.move for moving entries
13+
- merged #367 - added Entry.autotype_window setter
14+
- merged #364 - allow filename/keyfile to be file-like objects
15+
- merged #371 - drop dateutil dependency
16+
- merged #348 - switch to pyproject.toml
17+
118
4.0.6 - 2023-08-22
219
------------------
320
- fixed #350 - fixed all Python 2 deprecation FIXMEs (e.g. future, )

Makefile

+41-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,48 @@
1-
version := $(shell python -c "exec(open('pykeepass/version.py').read());print(__version__)")
1+
.ONESHELL:
2+
.SHELLFLAGS = -ec
3+
.SILENT:
4+
version := $(shell python -c "import tomllib;print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
25

36
.PHONY: dist
47
dist:
5-
python setup.py sdist bdist_wheel
8+
python -m build
69

7-
.PHONY: pypi
8-
pypi: dist
9-
twine upload dist/pykeepass-$(version).tar.gz
10+
.PHONY: release
11+
release: lock dist
12+
# check that changelog is updated. only look at first 3 parts of semver
13+
version=$(version)
14+
stripped=$$(echo $${version} | cut -d . -f -3 | cut -d '-' -f 1)
15+
if ! grep $${stripped} CHANGELOG.rst
16+
then
17+
echo "Changelog doesn't seem to be updated! Quitting..."
18+
exit 1
19+
fi
20+
# generate release notes from changelog
21+
awk "BEGIN{p=0}; /^$${stripped}/{next}; /---/{p=1;next}; /^$$/{exit}; p {print}" CHANGELOG.rst > TMPNOTES
22+
# make github and pypi release
23+
gh release create --latest --verify-tag v$(version) dist/pykeepass-$(version)* -F TMPNOTES
24+
twine upload -u __token__ dist/pykeepass-$(version)*
25+
26+
.PHONY: lock
27+
lock:
28+
# run tests then make a requirements.txt lockfile
29+
rm -rf .venv_lock
30+
virtualenv .venv_lock
31+
. .venv_lock/bin/activate
32+
pip install .[test]
33+
python tests/tests.py
34+
pip freeze > requirements.txt
35+
36+
.PHONY: tag
37+
tag:
38+
# tag git commit
39+
git add requirements.txt
40+
git add pyproject.toml
41+
git add CHANGELOG.rst
42+
git commit -m "bump version" --allow-empty
43+
git tag -a v$(version) -m "version $(version)"
44+
git push --tags
45+
git push
1046

1147
.PHONY: docs
1248
docs:

README.rst

+72-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
pykeepass
22
============
33

4-
.. image:: https://github.com/libkeepass/pykeepass/workflows/CI/badge.svg
5-
:target: https://github.com/libkeepass/pykeepass/actions?query=workflow%3ACI
4+
.. image:: https://github.com/libkeepass/pykeepass/actions/workflows/ci.yaml/badge.svg
5+
:target: https://github.com/libkeepass/pykeepass/actions/workflows/ci.yaml
66

77
.. image:: https://readthedocs.org/projects/pykeepass/badge/?version=latest
88
:target: https://pykeepass.readthedocs.io/en/latest/?badge=latest
@@ -16,10 +16,17 @@ pykeepass
1616

1717
This library allows you to write entries to a KeePass database.
1818

19-
Come chat at `#pykeepass`_ on Freenode or `#pykeepass:matrix.org`_ on Matrix.
19+
Come chat at `#pykeepass:matrix.org`_ on Matrix.
2020

21-
.. _#pykeepass: irc://irc.freenode.net
22-
.. _#pykeepass\:matrix.org: https://matrix.to/#/%23pykeepass:matrix.org
21+
.. _#pykeepass\:matrix.org: https://matrix.to/#/%23pykeepass:matrix.org
22+
23+
Installation
24+
------------
25+
26+
.. code::
27+
28+
sudo apt install python3-lxml
29+
pip install pykeepass
2330
2431
Example
2532
-------
@@ -66,7 +73,7 @@ Finding Entries
6673

6774
**find_entries** (title=None, username=None, password=None, url=None, notes=None, otp=None, path=None, uuid=None, tags=None, string=None, group=None, recursive=True, regex=False, flags=None, history=False, first=False)
6875

69-
Returns entries which match all provided parameters, where ``title``, ``username``, ``password``, ``url``, ``notes``, ``otp``, and ``autotype_sequence`` are strings, ``path`` is a list, ``string`` is a dict, ``autotype_enabled`` is a boolean, ``uuid`` is a ``uuid.UUID`` and ``tags`` is a list of strings. This function has optional ``regex`` boolean and ``flags`` string arguments, which means to interpret search strings as `XSLT style`_ regular expressions with `flags`_.
76+
Returns entries which match all provided parameters, where ``title``, ``username``, ``password``, ``url``, ``notes``, ``otp``, ``autotype_window`` and ``autotype_sequence`` are strings, ``path`` is a list, ``string`` is a dict, ``autotype_enabled`` is a boolean, ``uuid`` is a ``uuid.UUID`` and ``tags`` is a list of strings. This function has optional ``regex`` boolean and ``flags`` string arguments, which means to interpret search strings as `XSLT style`_ regular expressions with `flags`_.
7077

7178
.. _XSLT style: https://www.xml.com/pub/a/2003/06/04/tr.html
7279
.. _flags: https://www.w3.org/TR/xpath-functions/#flags
@@ -163,8 +170,8 @@ a flattened list of all groups in the database
163170
Group: "/"
164171
165172
166-
Entry Functions
167-
---------------
173+
Entry Functions and Properties
174+
------------------------------
168175
**add_entry** (destination_group, title, username, password, url=None, notes=None, tags=None, expiry_time=None, icon=None, force_creation=False)
169176

170177
**delete_entry** (entry)
@@ -175,6 +182,18 @@ move a group to the recycle bin. The recycle bin is created if it does not exit
175182

176183
**move_entry** (entry, destination_group)
177184

185+
**atime**
186+
187+
access time
188+
189+
**ctime**
190+
191+
creation time
192+
193+
**mtime**
194+
195+
modification time
196+
178197
where ``destination_group`` is a ``Group`` instance. ``entry`` is an ``Entry`` instance. ``title``, ``username``, ``password``, ``url``, ``notes``, ``tags``, ``icon`` are strings. ``expiry_time`` is a ``datetime`` instance.
179198

180199
If ``expiry_time`` is a naive datetime object (i.e. ``expiry_time.tzinfo`` is not set), the timezone is retrieved from ``dateutil.tz.gettz()``.
@@ -202,8 +221,15 @@ If ``expiry_time`` is a naive datetime object (i.e. ``expiry_time.tzinfo`` is no
202221
# save the database
203222
>>> kp.save()
204223
205-
Group Functions
206-
---------------
224+
# change creation time
225+
>>> from datetime import datetime, timezone
226+
>>> entry.ctime = datetime(2023, 1, 1, tzinfo=timezone.utc)
227+
228+
# update modification or access time
229+
>>> entry.touch(modify=True)
230+
231+
Group Functions and Properties
232+
------------------------------
207233
**add_group** (destination_group, group_name, icon=None, notes=None)
208234

209235
**delete_group** (group)
@@ -218,6 +244,18 @@ delete all entries and subgroups of a group. ``group`` is an instance of ``Grou
218244

219245
**move_group** (group, destination_group)
220246

247+
**atime**
248+
249+
access time
250+
251+
**ctime**
252+
253+
creation time
254+
255+
**mtime**
256+
257+
modification time
258+
221259
``destination_group`` and ``group`` are instances of ``Group``. ``group_name`` is a string
222260

223261
.. code:: python
@@ -241,6 +279,13 @@ delete all entries and subgroups of a group. ``group`` is an instance of ``Grou
241279
# save the database
242280
>>> kp.save()
243281
282+
# change creation time
283+
>>> from datetime import datetime, timezone
284+
>>> group.ctime = datetime(2023, 1, 1, tzinfo=timezone.utc)
285+
286+
# update modification or access time
287+
>>> group.touch(modify=True)
288+
244289
Attachments
245290
-----------
246291

@@ -366,7 +411,7 @@ Miscellaneous
366411
-------------
367412
**read** (filename=None, password=None, keyfile=None, transformed_key=None, decrypt=False)
368413
369-
where ``filename``, ``password``, and ``keyfile`` are strings. ``filename`` is the path to the database, ``password`` is the master password string, and ``keyfile`` is the path to the database keyfile. At least one of ``password`` and ``keyfile`` is required. Alternatively, the derived key can be supplied directly through ``transformed_key``. ``decrypt`` tells whether the file should be decrypted or not.
414+
where ``filename``, ``password``, and ``keyfile`` are strings ( ``filename`` and ``keyfile`` may also be file-like objects). ``filename`` is the path to the database, ``password`` is the master password string, and ``keyfile`` is the path to the database keyfile. At least one of ``password`` and ``keyfile`` is required. Alternatively, the derived key can be supplied directly through ``transformed_key``. ``decrypt`` tells whether the file should be decrypted or not.
370415
371416
Can raise ``CredentialsError``, ``HeaderChecksumError``, or ``PayloadChecksumError``.
372417
@@ -376,7 +421,7 @@ reload database from disk using previous credentials
376421
377422
**save** (filename=None)
378423
379-
where ``filename`` is the path of the file to save to. If ``filename`` is not given, the path given in ``read`` will be used.
424+
where ``filename`` is the path of the file to save to (``filename`` may also be file-like object). If ``filename`` is not given, the path given in ``read`` will be used.
380425
381426
**password**
382427
@@ -414,6 +459,21 @@ get database XML data as string
414459
415460
pretty print database XML to file
416461
462+
TOTP
463+
-------
464+
465+
**Entry.otp**
466+
467+
TOTP URI which can be passed to an OTP library to generate codes
468+
469+
.. code:: python
470+
471+
# find an entry which has otp attribute
472+
>>> e = kp.find_entries(otp='.*', regex=True, first=True)
473+
>>> import pyotp
474+
>>> pyotp.parse_uri(e.otp).now()
475+
799270
476+
417477
418478
Tests and Debugging
419479
-------------------

pykeepass/__init__.py

-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
from __future__ import absolute_import
21
from .pykeepass import PyKeePass, create_database
3-
42
from .version import __version__
53

64
__all__ = ["PyKeePass", "create_database", "__version__"]

pykeepass/attachment.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from . import entry
22
from .exceptions import BinaryError
33

4-
class Attachment(object):
4+
5+
class Attachment:
56
def __init__(self, element=None, kp=None, id=None, filename=None):
67
self._element = element
78
self._kp = kp

pykeepass/baseelement.py

+12-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import base64
22
import uuid
3+
from datetime import datetime, timezone
4+
5+
36
from lxml import etree
47
from lxml.builder import E
5-
from datetime import datetime
68

79

8-
class BaseElement():
10+
class BaseElement:
911
"""Entry and Group inherit from this class"""
1012

1113
def __init__(self, element, kp=None, icon=None, expires=False,
@@ -17,9 +19,9 @@ def __init__(self, element, kp=None, icon=None, expires=False,
1719
)
1820
if icon:
1921
self._element.append(E.IconID(icon))
20-
current_time_str = self._kp._encode_time(datetime.now())
22+
current_time_str = self._kp._encode_time(datetime.now(timezone.utc))
2123
if expiry_time:
22-
expiry_time_str = self._kp._encode_time(expiry_time)
24+
expiry_time_str = self._kp._encode_time(expiry_time.astimezone(timezone.utc))
2325
else:
2426
expiry_time_str = current_time_str
2527

@@ -116,8 +118,8 @@ def expires(self, value):
116118
def expired(self):
117119
if self.expires:
118120
return (
119-
self._kp._datetime_to_utc(datetime.utcnow()) >
120-
self._kp._datetime_to_utc(self.expiry_time)
121+
datetime.now(timezone.utc) >
122+
self.expiry_time
121123
)
122124

123125
return False
@@ -132,6 +134,7 @@ def expiry_time(self, value):
132134

133135
@property
134136
def ctime(self):
137+
"""(datetime.datetime): Creation time."""
135138
return self._get_times_property('CreationTime')
136139

137140
@ctime.setter
@@ -140,6 +143,7 @@ def ctime(self, value):
140143

141144
@property
142145
def atime(self):
146+
"""(datetime.datetime): Access time. Update with touch()"""
143147
return self._get_times_property('LastAccessTime')
144148

145149
@atime.setter
@@ -148,6 +152,7 @@ def atime(self, value):
148152

149153
@property
150154
def mtime(self):
155+
"""(datetime.datetime): Access time. Update with touch(modify=True)"""
151156
return self._get_times_property('LastModificationTime')
152157

153158
@mtime.setter
@@ -178,7 +183,7 @@ def touch(self, modify=False):
178183
Args:
179184
modify (bool): update access time as well a modification time
180185
"""
181-
now = datetime.now()
186+
now = datetime.now(timezone.utc)
182187
self.atime = now
183188
if modify:
184189
self.mtime = now

pykeepass/deprecated.py

-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
#!/usr/bin/env python3
2-
3-
41
# ---------- Find functions ---------------
52
# Use find_entries()/find_groups() instead
63

0 commit comments

Comments
 (0)