Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3399581
add mypy tests
aaronsteers May 14, 2021
2238c74
add py.typed
aaronsteers May 14, 2021
e276848
zip_safe=False
aaronsteers May 14, 2021
793eadd
run CI (but not publish) on all branches
aaronsteers May 14, 2021
2d6ea91
fix missing comma
aaronsteers May 14, 2021
9395328
mypy ini config
aaronsteers May 14, 2021
2e88e0a
poetry lock file
aaronsteers May 14, 2021
01d62cb
prevent committing IDE settings
aaronsteers May 14, 2021
a8c7fa0
add typing for most files
aaronsteers May 15, 2021
90eaa5d
update comments
aaronsteers May 15, 2021
f61d780
remove typed.py ref
aaronsteers May 15, 2021
47f9ac3
remove zip_safe annotation
aaronsteers May 15, 2021
aef4f99
add comment on cast
aaronsteers May 15, 2021
cf45938
add types for utils
aaronsteers May 16, 2021
e5068a2
typing for transform.py and utils.py
aaronsteers May 17, 2021
75e310d
explicit mypy config path
aaronsteers May 20, 2021
920a28a
Update main.yml
aaronsteers May 20, 2021
12a588a
Merge remote-tracking branch 'origin/master' into feature/mypy-typing…
aaronsteers May 20, 2021
58f0ee6
reorder imports
aaronsteers May 20, 2021
8611dc7
fix unused import
aaronsteers May 20, 2021
ea77324
give missing types
aaronsteers May 20, 2021
b3951c7
more specific types
aaronsteers May 20, 2021
9c3f2e2
Merge branch 'master' into feature/mypy-typing-checks
aaronsteers Nov 19, 2021
c99812f
Delete poetry.lock
aaronsteers Nov 19, 2021
0825e01
Apply suggestions from code review
aaronsteers Nov 19, 2021
ebce5b5
Merge branch 'master' into feature/mypy-typing-checks
aaronsteers Nov 23, 2021
d67ad2e
Update main.yml
Samira-El Nov 30, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,9 @@ jobs:

- name: Runs tests with coverage
run: nosetests --with-doctest -v --nocapture

- name: Installs mypy types where available
run: mypy --install-types

Comment thread
aaronsteers marked this conversation as resolved.
- name: Runs mypy linter and type checks
run: mypy --config-file ./mypy.ini singer
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ __pycache__/
# C extensions
*.so

# Developer IDE settings
.vscode/settings.json

# Distribution / packaging
.Python
env/
Expand Down
17 changes: 17 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[mypy]
python_version = 3.8
warn_unused_configs = True
warn_return_any = True
exclude = tests

# No typing provided in current version of jsonschema, backoff, and cisco8601.
# This supresses the related mypy warnings:

[mypy-jsonschema.*]
ignore_missing_imports = True

[mypy-backoff.*]
ignore_missing_imports = True

[mypy-ciso8601.*]
ignore_missing_imports = True
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
'ipython',
'ipdb',
'nose',
'mypy',
'unify==0.5'
]
},
Expand Down
32 changes: 21 additions & 11 deletions singer/bookmarks.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
def ensure_bookmark_path(state, path):
from typing import Dict, List, Optional, Any, cast

StateDict = Dict[str, Any]

def ensure_bookmark_path(state: StateDict, path: List[str]) -> StateDict:
submap = state
for path_component in path:
if submap.get(path_component) is None:
Expand All @@ -7,40 +11,46 @@ def ensure_bookmark_path(state, path):
submap = submap[path_component]
return state

def write_bookmark(state, tap_stream_id, key, val):
def write_bookmark(
state: StateDict, tap_stream_id: str, key: str, val: Any
) -> StateDict:
state = ensure_bookmark_path(state, ['bookmarks', tap_stream_id])
state['bookmarks'][tap_stream_id][key] = val
return state

def clear_bookmark(state, tap_stream_id, key):
def clear_bookmark(state: StateDict, tap_stream_id: str, key: str) -> StateDict:
state = ensure_bookmark_path(state, ['bookmarks', tap_stream_id])
state['bookmarks'][tap_stream_id].pop(key, None)
return state

def reset_stream(state, tap_stream_id):
def reset_stream(state: StateDict, tap_stream_id: str) -> StateDict:
state = ensure_bookmark_path(state, ['bookmarks', tap_stream_id])
state['bookmarks'][tap_stream_id] = {}
return state

def get_bookmark(state, tap_stream_id, key, default=None):
def get_bookmark(
state: StateDict, tap_stream_id: str, key: str, default: Optional[Any] = None
) -> Optional[Any]:
return state.get('bookmarks', {}).get(tap_stream_id, {}).get(key, default)

def set_offset(state, tap_stream_id, offset_key, offset_value):
def set_offset(
state: StateDict, tap_stream_id: str, offset_key: Any, offset_value: Any
):
state = ensure_bookmark_path(state, ['bookmarks', tap_stream_id, 'offset', offset_key])
state['bookmarks'][tap_stream_id]['offset'][offset_key] = offset_value
return state

def clear_offset(state, tap_stream_id):
def clear_offset(state: StateDict, tap_stream_id: str):
state = ensure_bookmark_path(state, ['bookmarks', tap_stream_id, 'offset'])
state['bookmarks'][tap_stream_id]['offset'] = {}
return state

def get_offset(state, tap_stream_id, default=None):
def get_offset(state: StateDict, tap_stream_id: str, default: Optional[Any] = None):
return state.get('bookmarks', {}).get(tap_stream_id, {}).get('offset', default)

def set_currently_syncing(state, tap_stream_id):
def set_currently_syncing(state: StateDict, tap_stream_id: str):
state['currently_syncing'] = tap_stream_id
return state

def get_currently_syncing(state, default=None):
return state.get('currently_syncing', default)
def get_currently_syncing(state: StateDict, default: str = None) -> Optional[str]:
return cast(str, state.get('currently_syncing', default))
44 changes: 28 additions & 16 deletions singer/catalog.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'''Provides an object model for a Singer Catalog.'''
import orjson
import sys
from typing import Iterable, Optional, List, Any, cast

from . import metadata as metadata_module
from .bookmarks import get_currently_syncing
Expand All @@ -10,7 +11,7 @@
LOGGER = get_logger()


def write_catalog(catalog):
def write_catalog(catalog: "Catalog") -> None:
# If the catalog has no streams, log a warning
if not catalog.streams:
LOGGER.warning('Catalog being written with no streams.')
Expand All @@ -22,10 +23,21 @@ def write_catalog(catalog):
# pylint: disable=too-many-instance-attributes
class CatalogEntry():

def __init__(self, tap_stream_id=None, stream=None,
key_properties=None, schema=None, replication_key=None,
is_view=None, database=None, table=None, row_count=None,
stream_alias=None, metadata=None, replication_method=None):
def __init__(
self,
tap_stream_id: Optional[str] = None,
stream: Optional[str] = None,
key_properties: Optional[List[str]] = None,
schema: Optional[Schema] = None,
replication_key: Optional[str] = None,
is_view: Optional[bool] = None,
database: Optional[str] = None,
table: Optional[str] = None,
row_count: Optional[int] = None,
stream_alias: Optional[str] = None,
metadata: Optional[dict] = None,
replication_method: Optional[str] = None
) -> None:

self.tap_stream_id = tap_stream_id
self.stream = stream
Expand Down Expand Up @@ -83,22 +95,22 @@ def to_dict(self):

class Catalog():

def __init__(self, streams):
def __init__(self, streams: List[CatalogEntry]) -> None:
self.streams = streams

def __str__(self):
def __str__(self) -> str:
return str(self.__dict__)

def __eq__(self, other):
return self.__dict__ == other.__dict__
def __eq__(self, other: Any) -> bool:
return cast(bool, self.__dict__ == other.__dict__)

@classmethod
def load(cls, filename):
def load(cls, filename: str) -> "Catalog":
with open(filename, encoding='utf-8') as fp: # pylint: disable=invalid-name
return Catalog.from_dict(orjson.loads(fp.read()))

@classmethod
def from_dict(cls, data):
def from_dict(cls, data: dict) -> "Catalog":
# TODO: We may want to store streams as a dict where the key is a
# tap_stream_id and the value is a CatalogEntry. This will allow
# faster lookup based on tap_stream_id. This would be a breaking
Expand All @@ -121,19 +133,19 @@ def from_dict(cls, data):
streams.append(entry)
return Catalog(streams)

def to_dict(self):
def to_dict(self) -> dict:
return {'streams': [stream.to_dict() for stream in self.streams]}

def dump(self):
def dump(self) -> None:
write_catalog(self)

def get_stream(self, tap_stream_id):
def get_stream(self, tap_stream_id: str) -> Optional[CatalogEntry]:
for stream in self.streams:
if stream.tap_stream_id == tap_stream_id:
return stream
return None

def _shuffle_streams(self, state):
def _shuffle_streams(self, state: dict) -> List[CatalogEntry]:
currently_syncing = get_currently_syncing(state)

if currently_syncing is None:
Expand All @@ -149,7 +161,7 @@ def _shuffle_streams(self, state):
return top_half + bottom_half


def get_selected_streams(self, state):
def get_selected_streams(self, state: dict) -> Iterable[CatalogEntry]:
for stream in self._shuffle_streams(state):
if not stream.is_selected():
LOGGER.info('Skipping stream: %s', stream.tap_stream_id)
Expand Down
2 changes: 1 addition & 1 deletion singer/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import os


def get_logger(name='singer'):
def get_logger(name: str = 'singer') -> logging.Logger:
"""Return a Logger instance to use in singer."""
# Use custom logging config provided by environment variable
if 'LOGGING_CONF_FILE' in os.environ and os.environ['LOGGING_CONF_FILE']:
Expand Down
Loading