Skip to content

Commit da7a403

Browse files
author
Jim Fulton
authored
feat: Add geography support (#228)
1 parent 62b2975 commit da7a403

26 files changed

+1439
-70
lines changed

docs/alembic.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Alembic support
2-
---------------
2+
^^^^^^^^^^^^^^^
33

44
`Alembic <https://alembic.sqlalchemy.org>`_ is a lightweight database
55
migration tool for usage with the SQLAlchemy Database Toolkit for

docs/geography.rst

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
Working with Geographic data
2+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3+
4+
BigQuery provides a `GEOGRAPHY data type
5+
<https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#geography_type>`_
6+
for `working with geographic data
7+
<https://cloud.google.com/bigquery/docs/gis-data>`_, including:
8+
9+
- Points,
10+
- Linestrings,
11+
- Polygons, and
12+
- Collections of points, linestrings, and polygons.
13+
14+
Geographic data uses the `WGS84
15+
<https://earth-info.nga.mil/#tab_wgs84-data>`_ coordinate system.
16+
17+
To define a geography column, use the `GEOGRAPHY` data type imported
18+
from the `sqlalchemy_bigquery` module:
19+
20+
.. literalinclude:: samples/snippets/geography.py
21+
:language: python
22+
:dedent: 4
23+
:start-after: [START bigquery_sqlalchemy_create_table_with_geography]
24+
:end-before: [END bigquery_sqlalchemy_create_table_with_geography]
25+
26+
BigQuery has a variety of `SQL geographic functions
27+
<https://cloud.google.com/bigquery/docs/reference/standard-sql/geography_functions>`_
28+
for working with geographic data. Among these are functions for
29+
converting between SQL geometry objects and `standard text (WKT) and
30+
binary (WKB) representations
31+
<https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry>`_.
32+
33+
Geography data is typically represented in Python as text strings in
34+
WKT format or as `WKB` objects, which contain binary data in WKB
35+
format. Querying geographic data returns `WKB` objects and `WKB`
36+
objects may be used in queries. When
37+
calling spatial functions that expect geographic arguments, text
38+
arguments are automatically coerced to geography.
39+
40+
Inserting data
41+
~~~~~~~~~~~~~~
42+
43+
When inserting geography data, you can pass WKT strings, `WKT` objects,
44+
or `WKB` objects:
45+
46+
.. literalinclude:: samples/snippets/geography.py
47+
:language: python
48+
:dedent: 4
49+
:start-after: [START bigquery_sqlalchemy_insert_geography]
50+
:end-before: [END bigquery_sqlalchemy_insert_geography]
51+
52+
Note that in the `lake3` example, we got a `WKB` object by creating a
53+
`WKT` object and getting its `wkb` property. Normally, we'd get `WKB`
54+
objects as results of previous queries.
55+
56+
Queries
57+
~~~~~~~
58+
59+
When performing spacial queries, and geography objects are expected,
60+
you can to pass `WKB` or `WKT` objects:
61+
62+
.. literalinclude:: samples/snippets/geography.py
63+
:language: python
64+
:dedent: 4
65+
:start-after: [START bigquery_sqlalchemy_query_geography_wkb]
66+
:end-before: [END bigquery_sqlalchemy_query_geography_wkb]
67+
68+
In this example, we passed the `geog` attribute of `lake2`, which is a WKB object.
69+
70+
Or you can pass strings in WKT format:
71+
72+
.. literalinclude:: samples/snippets/geography.py
73+
:language: python
74+
:dedent: 4
75+
:start-after: [START bigquery_sqlalchemy_query_geography_text]
76+
:end-before: [END bigquery_sqlalchemy_query_geography_text]
77+
78+
Installing geography support
79+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
80+
81+
To get geography support, you need to install `sqlalchemy-bigquery`
82+
with the `geography` extra, or separately install `GeoAlchemy2` and
83+
`shapely`.
84+
85+
.. code-block:: console
86+
87+
pip install 'sqlalchemy-bigquery[geography]'

docs/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
:maxdepth: 2
44

55
README
6+
geography
67
alembic
8+
reference
79

810
Changelog
911
---------

docs/reference.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
API Reference
2+
^^^^^^^^^^^^^
3+
4+
Geography
5+
~~~~~~~~~
6+
7+
.. autoclass:: sqlalchemy_bigquery.geography.GEOGRAPHY
8+
:exclude-members: bind_expression, ElementType, bind_processor
9+
10+
.. automodule:: sqlalchemy_bigquery.geography
11+
:members: WKB, WKT
12+
:exclude-members: GEOGRAPHY

docs/samples

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../samples

noxfile.py

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -82,22 +82,6 @@ def lint_setup_py(session):
8282
session.run("python", "setup.py", "check", "--restructuredtext", "--strict")
8383

8484

85-
def install_alembic_for_python_38(session, constraints_path):
86-
"""
87-
install alembic for Python 3.8 unit and system tests
88-
89-
We do not require alembic and most tests should run without it, however
90-
91-
- We run some unit tests (Python 3.8) to cover the alembic
92-
registration that happens when alembic is installed.
93-
94-
- We have a system test that demonstrates working with alembic and
95-
proves that the things we think should work do work. :)
96-
"""
97-
if session.python == "3.8":
98-
session.install("alembic", "-c", constraints_path)
99-
100-
10185
def default(session):
10286
# Install all test dependencies, then install this package in-place.
10387

@@ -114,8 +98,13 @@ def default(session):
11498
constraints_path,
11599
)
116100

117-
install_alembic_for_python_38(session, constraints_path)
118-
session.install("-e", ".", "-c", constraints_path)
101+
if session.python == "3.8":
102+
extras = "[alembic]"
103+
elif session.python == "3.9":
104+
extras = "[geography]"
105+
else:
106+
extras = ""
107+
session.install("-e", f".{extras}", "-c", constraints_path)
119108

120109
# Run py.test against the unit tests.
121110
session.run(
@@ -167,8 +156,13 @@ def system(session):
167156
# Install all test dependencies, then install this package into the
168157
# virtualenv's dist-packages.
169158
session.install("mock", "pytest", "google-cloud-testutils", "-c", constraints_path)
170-
install_alembic_for_python_38(session, constraints_path)
171-
session.install("-e", ".", "-c", constraints_path)
159+
if session.python == "3.8":
160+
extras = "[alembic]"
161+
elif session.python == "3.9":
162+
extras = "[geography]"
163+
else:
164+
extras = ""
165+
session.install("-e", f".{extras}", "-c", constraints_path)
172166

173167
# Run py.test against the system tests.
174168
if system_test_exists:
@@ -216,7 +210,13 @@ def compliance(session):
216210
"-c",
217211
constraints_path,
218212
)
219-
session.install("-e", ".", "-c", constraints_path)
213+
if session.python == "3.8":
214+
extras = "[alembic]"
215+
elif session.python == "3.9":
216+
extras = "[geography]"
217+
else:
218+
extras = ""
219+
session.install("-e", f".{extras}", "-c", constraints_path)
220220

221221
session.run(
222222
"py.test",
@@ -251,7 +251,9 @@ def docs(session):
251251
"""Build the docs for this library."""
252252

253253
session.install("-e", ".")
254-
session.install("sphinx==4.0.1", "alabaster", "recommonmark")
254+
session.install(
255+
"sphinx==4.0.1", "alabaster", "geoalchemy2", "shapely", "recommonmark"
256+
)
255257

256258
shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True)
257259
session.run(
@@ -274,7 +276,12 @@ def docfx(session):
274276

275277
session.install("-e", ".")
276278
session.install(
277-
"sphinx==4.0.1", "alabaster", "recommonmark", "gcp-sphinx-docfx-yaml"
279+
"sphinx==4.0.1",
280+
"alabaster",
281+
"geoalchemy2",
282+
"shapely",
283+
"recommonmark",
284+
"gcp-sphinx-docfx-yaml",
278285
)
279286

280287
shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True)

owlbot.py

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
import synthtool as s
2020
from synthtool import gcp
21-
21+
from synthtool.languages import python
2222

2323
REPO_ROOT = pathlib.Path(__file__).parent.absolute()
2424

@@ -27,10 +27,19 @@
2727
# ----------------------------------------------------------------------------
2828
# Add templated files
2929
# ----------------------------------------------------------------------------
30+
extras = []
31+
extras_by_python = {
32+
"3.8": ["alembic"],
33+
"3.9": ["geography"],
34+
}
3035
templated_files = common.py_library(
3136
unit_test_python_versions=["3.6", "3.7", "3.8", "3.9"],
3237
system_test_python_versions=["3.8", "3.9"],
33-
cov_level=100
38+
cov_level=100,
39+
unit_test_extras=extras,
40+
unit_test_extras_by_python=extras_by_python,
41+
system_test_extras=extras,
42+
system_test_extras_by_python=extras_by_python,
3443
)
3544
s.move(templated_files, excludes=[
3645
# sqlalchemy-bigquery was originally licensed MIT
@@ -77,37 +86,6 @@ def place_before(path, text, *before_text, escape=None):
7786
"nox.options.stop_on_first_error = True",
7887
)
7988

80-
install_alembic_for_python_38 = '''
81-
def install_alembic_for_python_38(session, constraints_path):
82-
"""
83-
install alembic for Python 3.8 unit and system tests
84-
85-
We do not require alembic and most tests should run without it, however
86-
87-
- We run some unit tests (Python 3.8) to cover the alembic
88-
registration that happens when alembic is installed.
89-
90-
- We have a system test that demonstrates working with alembic and
91-
proves that the things we think should work do work. :)
92-
"""
93-
if session.python == "3.8":
94-
session.install("alembic", "-c", constraints_path)
95-
96-
97-
'''
98-
99-
place_before(
100-
"noxfile.py",
101-
"def default",
102-
install_alembic_for_python_38,
103-
)
104-
105-
place_before(
106-
"noxfile.py",
107-
' session.install("-e", ".", ',
108-
" install_alembic_for_python_38(session, constraints_path)",
109-
escape='(')
110-
11189
old_sessions = '''
11290
"unit",
11391
"system",
@@ -125,6 +103,9 @@ def install_alembic_for_python_38(session, constraints_path):
125103

126104
s.replace( ["noxfile.py"], old_sessions, new_sessions)
127105

106+
# Maybe we can get rid of this when we don't need pytest-rerunfailures,
107+
# which we won't need when BQ retries itself:
108+
# https://github.com/googleapis/python-bigquery/pull/837
128109
compliance = '''
129110
@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS)
130111
def compliance(session):
@@ -153,7 +134,13 @@ def compliance(session):
153134
"-c",
154135
constraints_path,
155136
)
156-
session.install("-e", ".", "-c", constraints_path)
137+
if session.python == "3.8":
138+
extras = "[alembic]"
139+
elif session.python == "3.9":
140+
extras = "[geography]"
141+
else:
142+
extras = ""
143+
session.install("-e", f".{extras}", "-c", constraints_path)
157144
158145
session.run(
159146
"py.test",
@@ -180,6 +167,7 @@ def compliance(session):
180167
escape="()",
181168
)
182169

170+
s.replace(["noxfile.py"], '"alabaster"', '"alabaster", "geoalchemy2", "shapely"')
183171

184172

185173

@@ -201,6 +189,12 @@ def compliance(session):
201189
"""
202190
)
203191

192+
# ----------------------------------------------------------------------------
193+
# Samples templates
194+
# ----------------------------------------------------------------------------
195+
196+
python.py_samples(skip_readmes=True)
197+
204198
# ----------------------------------------------------------------------------
205199
# Final cleanup
206200
# ----------------------------------------------------------------------------

samples/__init__.py

Whitespace-only changes.

samples/pytest.ini

Whitespace-only changes.

samples/snippets/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright (c) 2021 The sqlalchemy-bigquery Authors
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
4+
# this software and associated documentation files (the "Software"), to deal in
5+
# the Software without restriction, including without limitation the rights to
6+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7+
# the Software, and to permit persons to whom the Software is furnished to do so,
8+
# subject to the following conditions:
9+
#
10+
# The above copyright notice and this permission notice shall be included in all
11+
# 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, FITNESS
15+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19+
20+
__version__ = "1.0.0-a1"

0 commit comments

Comments
 (0)