Skip to content

Commit c8b7298

Browse files
authored
Develop (#8)
1 parent de600ad commit c8b7298

16 files changed

Lines changed: 2092 additions & 152 deletions

File tree

.github/workflows/ci.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
tags:
8+
- '**'
9+
pull_request: {}
10+
11+
jobs:
12+
test:
13+
name: Test
14+
runs-on: ubuntu-20.04
15+
steps:
16+
- uses: actions/checkout@v2
17+
- uses: actions/setup-python@v2
18+
with:
19+
python-version: 3.8
20+
- name: Get Poetry
21+
uses: abatilo/actions-poetry@v2.1.4
22+
with:
23+
poetry-version: 1.1.12
24+
- name: Install
25+
run: poetry install
26+
- name: Run tests
27+
run: poetry run pytest tests.py

.github/workflows/test.yml

Lines changed: 0 additions & 41 deletions
This file was deleted.

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,5 @@ dmypy.json
137137
# Cython debug symbols
138138
cython_debug/
139139

140-
.idea
140+
.idea
141+
.DS_Store

.pre-commit-config.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Remember to update the version in the pyproject.toml.
2+
repos:
3+
- repo: https://github.com/timothycrosley/isort
4+
rev: 5.10.1
5+
hooks:
6+
- id: isort
7+
- repo: https://github.com/psf/black
8+
rev: 21.11b1
9+
hooks:
10+
- id: black
11+
args:
12+
- --line-length=88
13+
- --include='\.pyi?$'
14+
15+
- --exclude="""\.git |
16+
\.__pycache__|
17+
\.hg|
18+
\.mypy_cache|
19+
\.tox|
20+
\.venv|
21+
_build|
22+
buck-out|
23+
build|
24+
dist"""
25+
# flake8
26+
- repo: https://github.com/pre-commit/pre-commit-hooks
27+
rev: v2.3.0
28+
hooks:
29+
- id: flake8
30+
args:
31+
- "--max-line-length=88"
32+
- "--max-complexity=18"
33+
- "--select=B,C,E,F,W,T4,B9"
34+
- "--ignore=E203,E266,E501,W503,F403,F401,E402"

Makefile

Lines changed: 0 additions & 10 deletions
This file was deleted.

esridumpgdf/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
# flake8: noqa
12
from .layer import Layer
23
from .service import Service
4+
from .site import Site

esridumpgdf/_base.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import json
2+
from copy import deepcopy
3+
4+
from pandas import Series
5+
from requests import Session
6+
7+
8+
class Base(object):
9+
current_version: float = None
10+
_supported_services = ["MapServer", "FeatureServer"]
11+
_supported_types = ["Feature Layer", "Table"]
12+
13+
def __init__(self, url, session: Session = None):
14+
super(Base, self).__init__()
15+
self.url = url
16+
self._session = session or Session()
17+
self._session.params.update(dict(f="json"))
18+
19+
self.meta = self._session.get(self.url).json()
20+
self.meta["url"] = self.url
21+
self.current_version = self.meta["currentVersion"]
22+
23+
def __repr__(self) -> str:
24+
return Series(self.meta).to_string()
25+
26+
def _repr_html_(self) -> str:
27+
session = deepcopy(self._session)
28+
session.params.pop("f")
29+
text = session.get(self.url).text
30+
disabled = "Services Directory has been disabled"
31+
if disabled in text:
32+
return f"""
33+
<b>{disabled}</b>\n
34+
<pre>{json.dumps(self.meta, indent=2)}</pre>
35+
"""
36+
return text

esridumpgdf/base.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

esridumpgdf/layer.py

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,38 @@
1-
from typing import Union, Dict
2-
from collections import OrderedDict
1+
from typing import Dict, Union
32

4-
from pandas import to_datetime
5-
from geopandas import GeoDataFrame
63
from esridump.dumper import EsriDumper
4+
from geopandas import GeoDataFrame
5+
from pandas import to_datetime
76

8-
from .base import Base
7+
from ._base import Base
98

109

1110
class Layer(Base):
1211
def __init__(self, url: str, **kwargs):
13-
super(Layer, self).__init__(url, **kwargs)
12+
super(Layer, self).__init__(url)
13+
self.crs = kwargs.get("crs") or 4326
14+
self.layer = EsriDumper(self.url, outSR=str(self.crs), **kwargs)
1415

15-
@property
16-
def type(self) -> str:
17-
return self.meta['type']
18-
19-
def to_gdf(self, **kwargs) -> Union[GeoDataFrame, Dict[str, GeoDataFrame]]:
16+
def to_gdf(
17+
self, columns: list = None
18+
) -> Union[GeoDataFrame, Dict[str, GeoDataFrame]]:
2019
"""
21-
Export layer to GeoDataFrame
20+
Export an ArcGIS Server layer to GeoDataFrame
2221
23-
:param kwargs: extra keyword arguments provided to pyesridump's EsriDumper class
22+
:param columns: list of column names, optional
23+
Optionally specify the column names to include in the output frame.
24+
This does not overwrite the property names of the input, but can
25+
ensure a consistent output format.
2426
:return:
2527
"""
26-
crs = kwargs.get('crs') or 4326
27-
layer = EsriDumper(self.url, outSR=crs, **kwargs)
28-
29-
if self.type == 'Group Layer':
30-
gdfs = OrderedDict(
31-
{layer['name']: Layer(f'{"/".join(self.url.split("/")[:-1])}/{layer["id"]}', **kwargs).to_gdf()
32-
for layer in self.meta['subLayers']}
33-
)
34-
return gdfs
35-
36-
gdf = GeoDataFrame.from_features(features=layer, crs=crs)
37-
for field in self.meta['fields']:
38-
if field['type'] == 'esriFieldTypeOID':
39-
gdf.set_index(field['name'], inplace=True)
40-
if field['type'] == 'esriFieldTypeDate':
41-
gdf[field['name']] = to_datetime(gdf[field['name']], unit='ms')
42-
if field['type'] == 'esriFieldTypeInteger':
43-
gdf[field['name']] = gdf[field['name']].astype('Int64')
28+
gdf = GeoDataFrame.from_features(
29+
features=self.layer, crs=self.crs, columns=columns
30+
)
31+
for field in self.meta["fields"]:
32+
if field["type"] == "esriFieldTypeOID":
33+
gdf.set_index(field["name"], inplace=True)
34+
if field["type"] == "esriFieldTypeDate":
35+
gdf[field["name"]] = to_datetime(gdf[field["name"]], unit="ms")
36+
if field["type"] == "esriFieldTypeInteger":
37+
gdf[field["name"]] = gdf[field["name"]].astype("Int64")
4438
return gdf

esridumpgdf/service.py

Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,96 @@
1-
from collections import OrderedDict
2-
from typing import List, Dict
1+
from typing import Dict, Iterable, List
32

43
from geopandas import GeoDataFrame
4+
from pandas import DataFrame, concat
55

6-
from .base import Base
6+
from ._base import Base
77
from .layer import Layer
88

99

1010
class Service(Base):
11-
def __init__(self, url, **kwargs):
12-
super(Service, self).__init__(url, **kwargs)
11+
def __init__(self, url):
12+
self.url = url
13+
super(Service, self).__init__(self.url)
1314

14-
@property
15-
def layers(self) -> List[dict]:
16-
return self.meta['layers']
17-
18-
@property
19-
def tables(self) -> List[dict]:
20-
return self.meta['tables']
21-
22-
def to_gdfs(self, include_tables: bool = True, **kwargs) -> Dict[str, GeoDataFrame]:
15+
def layers(self, include_tables: bool = True) -> DataFrame:
2316
"""
24-
Export a complete ArcGIS Server Map or Feature service to GeoDataFrames
17+
Get Service layers.
2518
26-
:param include_tables: whether to include attribute-only tables
27-
:param kwargs: extra keyword arguments provided to pyesridump's EsriDumper class
19+
:param include_tables: include Service tables
2820
:return:
2921
"""
30-
layers = [layer for layer in self.layers if layer['type'] != 'Group Layer']
31-
gdfs = OrderedDict(
32-
{layer['name']: Layer(f'{self.url}/{layer["id"]}', **kwargs).to_gdf()
33-
for layer in sorted(layers, key=lambda _: _['name'])}
22+
layers = DataFrame(
23+
data=[
24+
{**layer, "url": f'{self.url}/{layer["id"]}'}
25+
for layer in self.meta["layers"]
26+
]
3427
)
35-
if self.tables and include_tables:
36-
gdfs.update({table['name']: Layer(f'{self.url}/{table["id"]}').to_gdf() for table in self.tables})
28+
layers.set_index("id", inplace=True)
29+
30+
if include_tables and self.meta["tables"]:
31+
tables = DataFrame(
32+
data=[
33+
{**table, "url": f'{self.url}/{table["id"]}'}
34+
for table in self.meta["tables"]
35+
]
36+
)
37+
tables.set_index("id", inplace=True)
38+
layers = concat([layers, tables])
39+
40+
return layers
41+
42+
def to_gpkg(
43+
self,
44+
filename: str,
45+
index: bool = True,
46+
schema: dict = None,
47+
include_tables: bool = True,
48+
**kwargs,
49+
) -> str:
50+
"""
51+
Export an ArcGIS Server Map or Feature service to geopackage
52+
53+
:param filename: File path or file handle to write to.
54+
:param index: If True, write index into one or more columns (for MultiIndex).
55+
Default None writes the index into one or more columns only if
56+
the index is named, is a MultiIndex, or has a non-integer data
57+
type. If False, no index is written.
58+
:param schema: If specified, the schema dictionary is passed to Fiona to
59+
better control how the file is written.
60+
:param include_tables: include Service tables
61+
:param kwargs: extra keyword arguments provided to the EsriDumper class
62+
:return: provided filename
63+
"""
64+
layers = self.layers(include_tables).to_dict(orient="records")
65+
for layer in layers:
66+
if layer["type"] in self._supported_types:
67+
Layer(layer["url"], **kwargs).to_gdf().to_file(
68+
filename,
69+
driver="GPKG",
70+
index=index,
71+
schema=schema,
72+
layer=layer["name"],
73+
)
74+
return filename
75+
76+
def to_gdfs(
77+
self, include_tables: bool = True, **kwargs
78+
) -> List[Dict[str, GeoDataFrame]]:
79+
"""
80+
Export an ArcGIS Server Map or Feature service to GeoDataFrames
81+
82+
:param include_tables: include Service tables
83+
:param kwargs: extra keyword arguments provided to the EsriDumper class
84+
:return: list of dicts with layer names and layer GeoDataFrames
85+
"""
86+
layers = self.layers(include_tables).to_dict(orient="records")
87+
gdfs = []
88+
for layer in layers:
89+
if layer["type"] in self._supported_types:
90+
gdfs.append(
91+
{
92+
"name": layer["name"],
93+
"gdf": Layer(layer["url"], **kwargs).to_gdf(),
94+
}
95+
)
3796
return gdfs

0 commit comments

Comments
 (0)