Skip to content
This repository was archived by the owner on Jan 30, 2023. It is now read-only.

Commit 412b143

Browse files
committed
Init
0 parents  commit 412b143

File tree

9 files changed

+307
-0
lines changed

9 files changed

+307
-0
lines changed

.gitignore

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.tox
2+
*.pyc
3+
*.pyo
4+
__pycache__
5+
*.egg-info
6+
docs/_build
7+
build
8+
dist
9+
.cache

.travis.yml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
language: python
2+
python:
3+
- "2.6"
4+
- "2.7"
5+
- "pypy"
6+
- "3.3"
7+
- "3.4"
8+
9+
install: pip install tox
10+
script: tox

LICENSE

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
This is free and unencumbered software released into the public domain.
2+
3+
Anyone is free to copy, modify, publish, use, compile, sell, or
4+
distribute this software, either in source code form or as a compiled
5+
binary, for any purpose, commercial or non-commercial, and by any
6+
means.
7+
8+
In jurisdictions that recognize copyright laws, the author or authors
9+
of this software dedicate any and all copyright interest in the
10+
software to the public domain. We make this dedication for the benefit
11+
of the public at large and to the detriment of our heirs and
12+
successors. We intend this dedication to be an overt act of
13+
relinquishment in perpetuity of all present and future rights to this
14+
software under copyright law.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
OTHER DEALINGS IN THE SOFTWARE.
23+
24+
For more information, please refer to <http://unlicense.org/>

MANIFEST.in

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
include LICENSE
2+
include README.rst

Makefile

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
release:
2+
python setup.py sdist bdist_wheel upload

README.rst

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
====
2+
vdir
3+
====
4+
5+
Tools to read and write from/to `vdirs
6+
<https://vdirsyncer.readthedocs.org/en/stable/vdir.html>`_.
7+
8+
Look at the source code for usage for now.
9+
10+
License
11+
=======
12+
13+
The ``vdir`` package is licensed under the public domain. See ``LICENSE``.

setup.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# -*- coding: utf-8 -*-
2+
import ast
3+
import re
4+
5+
from setuptools import setup
6+
7+
_version_re = re.compile(r'__version__\s+=\s+(.*)')
8+
9+
with open('vdir/__init__.py', 'rb') as f:
10+
version = str(ast.literal_eval(_version_re.search(
11+
f.read().decode('utf-8')).group(1)))
12+
13+
setup(
14+
name='vdir',
15+
version=version,
16+
description='Minimal interface for reading and writing from/to vdirs.',
17+
author='Markus Unterwaditzer',
18+
author_email='[email protected]',
19+
url='https://github.com/vdirsyncer/python-vdir',
20+
license='Public domain/Unlicense',
21+
packages=['vdir'],
22+
long_description=open('README.rst').read(),
23+
install_requires=['atomicwrites'],
24+
include_package_data=True,
25+
)

tox.ini

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[testenv]
2+
deps =
3+
flake8
4+
commands =
5+
flake8 .

vdir/__init__.py

+217
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import os
2+
import sys
3+
import errno
4+
import uuid
5+
6+
from atomicwrites import atomic_write
7+
8+
__version__ = '0.1.0'
9+
10+
PY2 = sys.version_info[0] == 2
11+
12+
13+
def to_unicode(x, encoding='ascii'):
14+
if not isinstance(x, text_type):
15+
return x.decode(encoding)
16+
return x
17+
18+
19+
def to_bytes(x, encoding='ascii'):
20+
if not isinstance(x, bytes):
21+
return x.encode(encoding)
22+
return x
23+
24+
if PY2:
25+
text_type = unicode # noqa
26+
to_native = to_bytes
27+
28+
else:
29+
text_type = str # noqa
30+
to_native = to_unicode
31+
32+
33+
SAFE_UID_CHARS = ('abcdefghijklmnopqrstuvwxyz'
34+
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
35+
'0123456789_.-+')
36+
37+
38+
def _href_safe(ident, safe=SAFE_UID_CHARS):
39+
return not bool(set(ident) - set(safe))
40+
41+
42+
def _generate_href(ident=None, safe=SAFE_UID_CHARS):
43+
if not ident or not _href_safe(ident, safe):
44+
return to_unicode(uuid.uuid4().hex)
45+
else:
46+
return ident
47+
48+
49+
class VdirError(IOError):
50+
pass
51+
52+
53+
class NotFoundError(VdirError):
54+
pass
55+
56+
57+
class WrongEtagError(VdirError):
58+
pass
59+
60+
61+
class AlreadyExists(VdirError):
62+
pass
63+
64+
65+
class Item(object):
66+
def __init__(self, raw):
67+
assert isinstance(raw, text_type)
68+
self.raw = raw
69+
70+
71+
class Vdir(object):
72+
item_class = Item
73+
default_mode = 0o750
74+
75+
def __init__(self, path, fileext, encoding='utf-8'):
76+
self.path = path
77+
self.encoding = encoding
78+
self.fileext = fileext
79+
80+
@staticmethod
81+
def _get_etag_from_file(fpath):
82+
'''Get mtime-based etag from a filepath.'''
83+
stat = os.stat(fpath)
84+
mtime = getattr(stat, 'st_mtime_ns', None)
85+
if mtime is None:
86+
mtime = stat.st_mtime
87+
return '{:.9f}'.format(mtime)
88+
89+
@classmethod
90+
def discover(cls, path, **kwargs):
91+
try:
92+
collections = os.listdir(path)
93+
except OSError as e:
94+
if e.errno != errno.ENOENT:
95+
raise
96+
return
97+
98+
for collection in collections:
99+
collection_path = os.path.join(path, collection)
100+
if os.path.isdir(collection_path):
101+
yield cls(path=collection_path, **kwargs)
102+
103+
@classmethod
104+
def create(cls, collection_name, **kwargs):
105+
kwargs = dict(kwargs)
106+
path = kwargs['path']
107+
108+
path = os.path.join(path, collection_name)
109+
if not os.path.exists(path):
110+
os.makedirs(path, mode=cls.default_mode)
111+
elif not os.path.isdir(path):
112+
raise IOError('{} is not a directory.'.format(repr(path)))
113+
114+
kwargs['path'] = path
115+
return kwargs
116+
117+
def _get_filepath(self, href):
118+
return os.path.join(self.path, href)
119+
120+
def _get_href(self, ident):
121+
return _generate_href(ident) + self.fileext
122+
123+
def list(self):
124+
for fname in os.listdir(self.path):
125+
fpath = os.path.join(self.path, fname)
126+
if os.path.isfile(fpath) and fname.endswith(self.fileext):
127+
yield fname, self._get_etag_from_file(fpath)
128+
129+
def get(self, href):
130+
fpath = self._get_filepath(href)
131+
try:
132+
with open(fpath, 'rb') as f:
133+
return (Item(f.read().decode(self.encoding)),
134+
self._get_etag_from_file(fpath))
135+
except IOError as e:
136+
if e.errno == errno.ENOENT:
137+
raise NotFoundError(href)
138+
else:
139+
raise
140+
141+
def upload(self, item):
142+
if not isinstance(item.raw, text_type):
143+
raise TypeError('item.raw must be a unicode string.')
144+
145+
try:
146+
href = self._get_href(item.ident)
147+
fpath, etag = self._upload_impl(item, href)
148+
except OSError as e:
149+
if e.errno in (
150+
errno.ENAMETOOLONG, # Unix
151+
errno.ENOENT # Windows
152+
):
153+
# random href instead of UID-based
154+
href = self._get_href(None)
155+
fpath, etag = self._upload_impl(item, href)
156+
else:
157+
raise
158+
159+
if self.post_hook:
160+
self._run_post_hook(fpath)
161+
return href, etag
162+
163+
def _upload_impl(self, item, href):
164+
fpath = self._get_filepath(href)
165+
try:
166+
with atomic_write(fpath, mode='wb', overwrite=False) as f:
167+
f.write(item.raw.encode(self.encoding))
168+
return fpath, self._get_etag_from_file(f.name)
169+
except OSError as e:
170+
if e.errno == errno.EEXIST:
171+
raise AlreadyExists(existing_href=href)
172+
else:
173+
raise
174+
175+
def update(self, href, item, etag):
176+
fpath = self._get_filepath(href)
177+
if not os.path.exists(fpath):
178+
raise NotFoundError(item.uid)
179+
actual_etag = self._get_etag_from_file(fpath)
180+
if etag != actual_etag:
181+
raise WrongEtagError(etag, actual_etag)
182+
183+
if not isinstance(item.raw, text_type):
184+
raise TypeError('item.raw must be a unicode string.')
185+
186+
with atomic_write(fpath, mode='wb', overwrite=True) as f:
187+
f.write(item.raw.encode(self.encoding))
188+
etag = self._get_etag_from_fileobject(f)
189+
190+
return etag
191+
192+
def delete(self, href, etag):
193+
fpath = self._get_filepath(href)
194+
if not os.path.isfile(fpath):
195+
raise NotFoundError(href)
196+
actual_etag = self._get_etag_from_file(fpath)
197+
if etag != actual_etag:
198+
raise WrongEtagError(etag, actual_etag)
199+
os.remove(fpath)
200+
201+
def get_meta(self, key):
202+
fpath = os.path.join(self.path, key)
203+
try:
204+
with open(fpath, 'rb') as f:
205+
return f.read().decode(self.encoding) or None
206+
except IOError as e:
207+
if e.errno == errno.ENOENT:
208+
return None
209+
else:
210+
raise
211+
212+
def set_meta(self, key, value):
213+
value = value or u''
214+
assert isinstance(value, text_type)
215+
fpath = os.path.join(self.path, key)
216+
with atomic_write(fpath, mode='wb', overwrite=True) as f:
217+
f.write(value.encode(self.encoding))

0 commit comments

Comments
 (0)