Skip to content

Commit 9928de4

Browse files
committed
Initial commit.
0 parents  commit 9928de4

File tree

6 files changed

+222
-0
lines changed

6 files changed

+222
-0
lines changed

.gitignore

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
*.py[cod]
2+
3+
# C extensions
4+
*.so
5+
6+
# Packages
7+
*.egg
8+
*.egg-info
9+
dist
10+
build
11+
eggs
12+
parts
13+
bin
14+
var
15+
sdist
16+
develop-eggs
17+
.installed.cfg
18+
lib
19+
lib64
20+
21+
# Installer logs
22+
pip-log.txt
23+
24+
# Unit test / coverage reports
25+
.coverage
26+
.tox
27+
nosetests.xml
28+
29+
# Translations
30+
*.mo
31+
32+
# Mr Developer
33+
.mr.developer.cfg
34+
.project
35+
.pydevproject
36+
37+
# Complexity
38+
output/*.html
39+
output/*/index.html
40+
41+
# Sphinx
42+
docs/_build
43+
README.html
44+
45+
_sandbox

LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright 2016 Joshua Carp
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in
11+
all copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

README.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
=================
2+
sqlalchemy-export
3+
=================

setup.cfg

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[wheel]
2+
# This flag says that the code is written to work on both Python 2 and Python
3+
# 3. If at all possible, it is good practice to do this. If you cannot, you
4+
# will need to generate wheels for each Python version that you support.
5+
universal=1
6+
7+
# E127: continuation line over-indented for visual indent
8+
# E128: continuation line under-indented for visual indent
9+
# E265: block comment should start with #
10+
# E301: expected 1 blank line, found 1
11+
# E302: expected 2 blank lines, found 0
12+
[flake8]
13+
ignore = E127,E128,E265,E301,E302
14+
max-line-length = 90

setup.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import re
4+
from setuptools import setup
5+
from setuptools import find_packages
6+
7+
REQUIRES = [
8+
'six',
9+
'sqlalchemy',
10+
]
11+
12+
def find_version(fname):
13+
"""Attempts to find the version number in the file names fname.
14+
Raises RuntimeError if not found.
15+
"""
16+
version = ''
17+
with open(fname, 'r') as fp:
18+
reg = re.compile(r'__version__ = [\'"]([^\'"]*)[\'"]')
19+
for line in fp:
20+
m = reg.match(line)
21+
if m:
22+
version = m.group(1)
23+
break
24+
if not version:
25+
raise RuntimeError('Cannot find version information')
26+
return version
27+
28+
def read(fname):
29+
with open(fname) as fp:
30+
content = fp.read()
31+
return content
32+
33+
setup(
34+
name='sqlalchemy-export',
35+
version=find_version('sqlalchemy_export/__init__.py'),
36+
description='Bulk export utilities for SQLAlchemy',
37+
long_description=read('README.rst'),
38+
author='Joshua Carp',
39+
author_email='[email protected]',
40+
url='https://github.com/jmcarp/sqlalchemy-export',
41+
packages=find_packages(exclude=('test*', )),
42+
package_dir={'sqlalchemy_export': 'sqlalchemy_export'},
43+
include_package_data=True,
44+
install_requires=REQUIRES,
45+
license=read('LICENSE'),
46+
zip_safe=False,
47+
keywords='sqlalchemy-export',
48+
classifiers=[
49+
'Development Status :: 2 - Pre-Alpha',
50+
'Intended Audience :: Developers',
51+
'License :: OSI Approved :: MIT License',
52+
'Natural Language :: English',
53+
'Programming Language :: Python :: 2',
54+
'Programming Language :: Python :: 2.7',
55+
'Programming Language :: Python :: 3',
56+
'Programming Language :: Python :: 3.3',
57+
'Programming Language :: Python :: 3.4',
58+
],
59+
test_suite='tests',
60+
)

sqlalchemy_export/__init__.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import six
2+
3+
from sqlalchemy.sql import ColumnElement
4+
from sqlalchemy.orm import Mapper, class_mapper
5+
from sqlalchemy.orm.exc import UnmappedClassError
6+
from sqlalchemy.dialects import postgresql
7+
8+
__version__ = '0.1.0'
9+
10+
def export(select, engine, fp, **flags):
11+
"""Export a query or select to a file. For flags, see the PostgreSQL
12+
documentation at http://www.postgresql.org/docs/9.5/static/sql-copy.html.
13+
14+
Examples: ::
15+
select = MyTable.select()
16+
with open('/path/to/file.tsv', 'w') as fp:
17+
export(select, engine, fp)
18+
19+
query = session.query(MyModel)
20+
with open('/path/to/file/csv', 'w') as fp:
21+
export(query, engine, fp, format='csv', null='.')
22+
23+
:param select: SQLAlchemy query or select
24+
:param engine: SQLAlchemy engine
25+
:param fp: File pointer, using write mode
26+
"""
27+
dialect = postgresql.dialect()
28+
statement = getattr(select, 'statement', select)
29+
compiled = statement.compile(dialect=dialect)
30+
conn = engine.raw_connection()
31+
cursor = conn.cursor()
32+
query = cursor.mogrify(compiled.string, compiled.params).decode()
33+
formatted_flags = '({})'.format(format_flags(flags)) if flags else ''
34+
copy = 'COPY ({}) TO STDOUT {}'.format(query, formatted_flags)
35+
cursor.copy_expert(copy, fp)
36+
conn.close()
37+
38+
def format_flags(flags):
39+
return ', '.join(
40+
'{} {}'.format(key.upper(), format_flag(value))
41+
for key, value in flags.items()
42+
)
43+
44+
def format_flag(value):
45+
return (
46+
six.text_type(value).upper()
47+
if isinstance(value, bool)
48+
else repr(value)
49+
)
50+
51+
def label_entities(query):
52+
entities = sum(
53+
[extract_entities(defn) for defn in query.column_definitions],
54+
[]
55+
)
56+
return query.with_entities(*entities)
57+
58+
def extract_entities(defn):
59+
expr = defn['expr']
60+
if isinstance(expr, Mapper):
61+
return extract_mapper_entities(expr)
62+
elif is_model(expr):
63+
return extract_mapper_entities(expr.__mapper__)
64+
elif isinstance(expr, ColumnElement):
65+
return expr
66+
else:
67+
raise ValueError()
68+
69+
def extract_mapper_entities(mapper):
70+
model = mapper.class_
71+
return [
72+
getattr(model, prop.key).label(prop.key)
73+
for prop in mapper.column_attrs
74+
]
75+
76+
def is_model(class_):
77+
try:
78+
class_mapper(class_)
79+
return True
80+
except UnmappedClassError:
81+
return False

0 commit comments

Comments
 (0)