Skip to content

Commit

Permalink
Closes pylover#13, closes pylover#29, closes pylover#20, and closes p…
Browse files Browse the repository at this point in the history
  • Loading branch information
pylover committed Oct 1, 2016
1 parent 29c8d86 commit f7b461d
Show file tree
Hide file tree
Showing 26 changed files with 479 additions and 107 deletions.
8 changes: 4 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ python:
- 3.5-dev

before_install:
- if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then pip install -U pip setuptools wheel sphinx coverage coveralls; fi
- pip install -U pip setuptools wheel
- pip install -r requirements-dev.txt

install:
- pip install -ve .

script:
- if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then coverage run --source sqlalchemy_media $(which nosetests);
else nosetests; fi
- then coverage run --source sqlalchemy_media $(which nosetests)

after_success:
- if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then coveralls ; fi
- coveralls

after_deploy:
- if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then ./travis-gh-pages.sh ; fi
Expand Down
5 changes: 5 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-r requirements-optional.txt
nose
coverage
sphinx
coveralls
1 change: 1 addition & 0 deletions requirements-optional.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python-magic >= 0.4.12
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
package_version = re.compile(r".*__version__ = '(.*?)'", re.S).match(v_file.read()).group(1)

dependencies = [
'sqlalchemy >= 1.1.0b3'
'sqlalchemy >= 1.1.0b3',
]


Expand Down
83 changes: 42 additions & 41 deletions sqlalchemy_media/attachments/attachment.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from typing import Hashable
import copy
import mimetypes
import uuid
from os.path import splitext

from sqlalchemy.ext.mutable import MutableDict

from sqlalchemy_media.stores import StoreManager
from sqlalchemy_media.typing import Attachable
from sqlalchemy_media.descriptors import AttachableDescriptor


class Attachment(MutableDict):
Expand Down Expand Up @@ -119,53 +119,54 @@ def get_store(self):
store_manager = StoreManager.get_current_store_manager()
return store_manager.get(self.store_id)

def store(self, f):
length = self.get_store().put(self.path, f, max_length=self.max_length, min_length=self.min_length)
self['length'] = length

def delete(self):
self.get_store().delete(self.path)

def attach(self, f: Attachable, content_type: str=None, original_filename: str=None, extension: str=None,
store_id: str=None) -> None:
def attach(self, f: Attachable, content_type: str = None, original_filename: str = None, extension: str = None,
store_id: str = None) -> None:

# Backup the old key and filename if exists
old_attachment = None if self.empty else self.copy()

# Determining original filename
if original_filename is not None:
self.original_filename = original_filename
elif isinstance(f, str):
self.original_filename = f

# Determining the extension
if extension is not None:
self.extension = extension
elif isinstance(f, str):
self.extension = splitext(f)[1]
elif original_filename is not None:
self.extension = splitext(self.original_filename)[1]

# Determining the mimetype
if content_type is not None:
self.content_type = content_type
elif isinstance(f, str):
self.content_type = mimetypes.guess_type(f)
elif original_filename is not None:
self.content_type = mimetypes.guess_type(self.original_filename)
elif extension is not None:
self.content_type = mimetypes.guess_type('x%s' % extension)

self.key = str(uuid.uuid4())

if store_id is not None:
self.store_id = store_id

self.store(f)
store_manager = StoreManager.get_current_store_manager()
store_manager.register_to_delete_after_rollback(self)
if old_attachment:
store_manager.register_to_delete_after_commit(old_attachment)
# Wrap in AttachableDescriptor
with AttachableDescriptor(
f,
content_type=content_type,
original_filename=original_filename,
extension=extension
) as descriptor:

# Analyze

# Validate

# Store

# Determining the extension
if descriptor.original_filename:
self.original_filename = original_filename

if descriptor.extension:
self.extension = descriptor.extension

if descriptor.content_type:
self.content_type = descriptor.content_type
self.key = str(uuid.uuid4())

if store_id is not None:
self.store_id = store_id

self['length'] = self.get_store().put(
self.path,
descriptor,
max_length=self.max_length,
min_length=self.min_length
)

store_manager = StoreManager.get_current_store_manager()
store_manager.register_to_delete_after_rollback(self)
if old_attachment:
store_manager.register_to_delete_after_commit(old_attachment)

def locate(self):
store = self.get_store()
Expand Down
7 changes: 0 additions & 7 deletions sqlalchemy_media/attachments/image.py

This file was deleted.

12 changes: 12 additions & 0 deletions sqlalchemy_media/attachments/images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@


from sqlalchemy_media.attachments.file import File
from sqlalchemy_media.constants import MB, KB


class Image(File):
__directory__ = 'images'
__prefix__ = 'image'

max_length = 2*MB
min_length = 4*KB
1 change: 1 addition & 0 deletions sqlalchemy_media/descriptors/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .attachable import AttachableDescriptor
34 changes: 34 additions & 0 deletions sqlalchemy_media/descriptors/attachable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

import cgi

from sqlalchemy_media.typing import Attachable
from sqlalchemy_media.helpers import is_uri
from sqlalchemy_media.descriptors.base import BaseDescriptor
from sqlalchemy_media.descriptors.localfs import LocalFileSystemDescriptor
from sqlalchemy_media.descriptors.cgi_fieldstorage import CgiFieldStorageDescriptor
from sqlalchemy_media.descriptors.url import UrlDescriptor
from sqlalchemy_media.descriptors.stream import StreamDescriptor


# noinspection PyAbstractClass
class AttachableDescriptor(BaseDescriptor):

# noinspection PyInitNewSignature
def __new__(cls, attachable: Attachable, *args, **kwargs):
"""
Should determine the appropriate descriptor and return an instance of it.
:param attachable:
:param args:
:param kwargs:
:return:
"""

if isinstance(attachable, cgi.FieldStorage):
return_type = CgiFieldStorageDescriptor
elif isinstance(attachable, str):
return_type = UrlDescriptor if is_uri(attachable) else LocalFileSystemDescriptor
else:
return_type = StreamDescriptor

return return_type(attachable, *args, **kwargs)

75 changes: 75 additions & 0 deletions sqlalchemy_media/descriptors/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@

import io
import mimetypes

from sqlalchemy_media.exceptions import MaximumLengthIsReachedError


class BaseDescriptor(object):
__header_buffer_size__ = 1024
header = None
original_filename = None
extension = None

def __init__(self, max_length: int=None, content_type: str=None, content_length: int=None, extension: str=None,
**kwargs):
self.max_length = max_length
self.content_type = content_type
self.content_length = content_length
self.header = io.BytesIO(self._read_source(self.__header_buffer_size__))

for k, v in kwargs.items():
setattr(self, k, v)

if extension:
self.extension = extension
elif self.original_filename:
self.extension = mimetypes.guess_extension(self.original_filename)

def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self.close()

def read(self, size):
if not self.header:
return self._read_source(size)

current_cursor = self.header.tell()
cursor_after_read = current_cursor + size
source_cursor = self._tell_source()

if self.max_length is not None and source_cursor > self.max_length:
raise MaximumLengthIsReachedError(self.max_length)

if source_cursor > self.__header_buffer_size__ or current_cursor == self.__header_buffer_size__:
return self._read_source(size)

if cursor_after_read > self.__header_buffer_size__:
# split the read, half from header & half from source
part1 = self.header.read()
part2 = self._read_source(size - len(part1))
return part1 + part2
return self.header.read(size)

def tell(self):
source_cursor = self._tell_source()
if not self.header:
return source_cursor

if source_cursor > self.header.tell():
return self.header.tell()
return source_cursor

def _tell_source(self):
raise NotImplementedError()

def _read_source(self, size):
raise NotImplementedError()

def seek(self, position):
raise NotImplementedError('Seek operation is not supported by this object: %r' % self)

def close(self):
raise NotImplementedError()
14 changes: 14 additions & 0 deletions sqlalchemy_media/descriptors/cgi_fieldstorage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

from cgi import FieldStorage

from sqlalchemy_media.descriptors.stream import CloserStreamDescriptor


class CgiFieldStorageDescriptor(CloserStreamDescriptor):

def __init__(self, storage: FieldStorage, content_type: str=None, **kwargs):
self.original_filename = storage.filename
if content_type is None:
content_type = storage.headers['Content-Type']

super().__init__(storage.file, content_type=content_type, **kwargs)
15 changes: 15 additions & 0 deletions sqlalchemy_media/descriptors/localfs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

import mimetypes

from sqlalchemy_media.descriptors.stream import CloserStreamDescriptor


class LocalFileSystemDescriptor(CloserStreamDescriptor):

def __init__(self, filename: str, content_type: str=None, **kwargs):
self.original_filename = filename
if content_type is None:
content_type = mimetypes.guess_type(filename)[0]

super().__init__(open(filename, 'rb'), content_type=content_type, **kwargs)

33 changes: 33 additions & 0 deletions sqlalchemy_media/descriptors/stream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@


from sqlalchemy_media.typing import Stream
from sqlalchemy_media.descriptors.base import BaseDescriptor


class StreamDescriptor(BaseDescriptor):

def __init__(self, stream: Stream, **kwargs):
self._file = stream
super().__init__(**kwargs)

def _tell_source(self) -> int:
return self._file.tell()

def _read_source(self, size: int) -> bytes:
return self._file.read(size)

def seek(self, position: int):
self._file.seek(position)

def close(self) -> None:
"""
Do not closing the stream here, because we'r not upened it.
:return:
"""
pass


class CloserStreamDescriptor(StreamDescriptor):

def close(self) -> None:
self._file.close()
19 changes: 19 additions & 0 deletions sqlalchemy_media/descriptors/url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

from urllib.request import urlopen

from sqlalchemy_media.descriptors.stream import CloserStreamDescriptor


class UrlDescriptor(CloserStreamDescriptor):
def __init__(self, uri: str, content_type: str=None, **kwargs):
self.original_filename = uri
response = urlopen(uri)

if content_type is None and 'Content-Type' in response.headers:
content_type = response.headers.get('Content-Type')

if 'Content-Length' in response.headers:
kwargs['content_length'] = int(response.headers.get('Content-Length'))

super().__init__(response, content_type = content_type, ** kwargs)

Loading

0 comments on commit f7b461d

Please sign in to comment.