diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0894a19..4293468 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,17 @@ repos: - - repo: https://github.com/psf/black - rev: 22.6.0 - hooks: - - id: black - - repo: https://github.com/pycqa/flake8 - rev: 3.9.2 - hooks: - - id: flake8 - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.971 - hooks: - - id: mypy - additional_dependencies: - - types-python-dateutil - - types-redis + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.1 + hooks: + # Run the linter + - id: ruff + args: ["--fix"] + # Run the formatter + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.971 + hooks: + - id: mypy + additional_dependencies: + - types-python-dateutil + - types-redis diff --git a/bitmapist/__init__.py b/bitmapist/__init__.py index db6cf3c..677ffd9 100644 --- a/bitmapist/__init__.py +++ b/bitmapist/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ bitmapist ~~~~~~~~~ @@ -84,13 +83,14 @@ import calendar import threading -from builtins import bytes, range from collections import defaultdict from datetime import date, datetime, timedelta -from typing import Any, Optional, Union +from typing import TYPE_CHECKING, Any, Optional, Union import redis -from redis.client import Pipeline, Redis + +if TYPE_CHECKING: + from redis.client import Pipeline, Redis local_thread = threading.local() @@ -118,9 +118,9 @@ def setup_redis(name: str, host: str, port: int, **kw: Any) -> None: Example:: - setup_redis('stats_redis', 'localhost', 6380) + setup_redis("stats_redis", "localhost", 6380) - mark_event('active', 1, system='stats_redis') + mark_event("active", 1, system="stats_redis") """ redis_client = kw.pop("redis_client", redis.StrictRedis) SYSTEMS[name] = redis_client(host=host, port=port, **kw) @@ -135,8 +135,7 @@ def get_redis(system: str = "default") -> Union[Redis, Pipeline]: """ if isinstance(system, redis.StrictRedis): return system - else: - return SYSTEMS[system] + return SYSTEMS[system] # --- Events marking and deleting @@ -173,10 +172,10 @@ def mark_event( Examples:: # Mark id 1 as active - mark_event('active', 1) + mark_event("active", 1) # Mark task completed for id 252 - mark_event('tasks:completed', 252) + mark_event("tasks:completed", 252) """ _mark( event_name, uuid, system, now, track_hourly, track_unique, use_pipeline, value=1 @@ -247,7 +246,7 @@ def mark_unique(event_name: str, uuid: int, system: str = "default") -> None: Examples:: # Mark id 42 as premium - mark_unique('premium', 42) + mark_unique("premium", 42) """ _mark_unique(event_name, uuid, system, value=1) @@ -267,7 +266,7 @@ def unmark_unique(event_name: str, uuid: int, system: str = "default") -> None: Examples:: # Mark id 42 as not premium anymore - unmark_unique('premium', 42) + unmark_unique("premium", 42) """ _mark_unique(event_name, uuid, system, value=0) @@ -296,9 +295,7 @@ def get_event_names( def delete_all_events(system: str = "default") -> None: - """ - Delete all events from the database. - """ + """Delete all events from the database.""" cli = get_redis(system) keys = cli.keys("trackist_*") if keys: @@ -306,9 +303,7 @@ def delete_all_events(system: str = "default") -> None: def delete_temporary_bitop_keys(system: str = "default") -> None: - """ - Delete all temporary keys that are used when using bit operations. - """ + """Delete all temporary keys that are used when using bit operations.""" cli = get_redis(system) keys = cli.keys("trackist_bitop_*") if keys: @@ -316,9 +311,7 @@ def delete_temporary_bitop_keys(system: str = "default") -> None: def delete_runtime_bitop_keys() -> None: - """ - Delete all BitOp keys that were created. - """ + """Delete all BitOp keys that were created.""" bitop_keys = _bitop_keys() for system in bitop_keys: if len(bitop_keys[system]) > 0: @@ -351,14 +344,13 @@ def get_uuids(self): # find set bits, generate smth like [1, 0, ...] bits = [(char >> i) & 1 for i in range(7, -1, -1)] # list of positions with ones - set_bits = list(pos for pos, val in enumerate(bits) if val) + set_bits = [pos for pos, val in enumerate(bits) if val] # yield everything we need for bit in set_bits: yield char_num * 8 + bit def __iter__(self): - for item in self.get_uuids(): - yield item + yield from self.get_uuids() class MixinBitOperations: @@ -421,15 +413,14 @@ class MixinContains: Example:: - user_active_today = 123 in DayEvents('active', 2012, 10, 23) + user_active_today = 123 in DayEvents("active", 2012, 10, 23) """ def __contains__(self, uuid): cli = get_redis(self.system) if cli.getbit(self.redis_key, uuid): return True - else: - return False + return False class UniqueEvents( @@ -455,11 +446,11 @@ class GenericPeriodEvents( MixinIter, MixinCounts, MixinContains, MixinEventsMisc, MixinBitOperations ): def next(self): - """next object in a datetime line""" + """Next object in a datetime line""" return self.delta(value=1) def prev(self): - """prev object in a datetime line""" + """Prev object in a datetime line""" return self.delta(value=-1) @@ -469,7 +460,7 @@ class YearEvents(GenericPeriodEvents): Example:: - YearEvents('active', 2012) + YearEvents("active", 2012) """ @classmethod @@ -483,9 +474,7 @@ def __init__(self, event_name, year=None, system="default"): self.year = not_none(year, now.year) self.system = system - months = [] - for m in range(1, 13): - months.append(MonthEvents(event_name, self.year, m, system)) + months = [MonthEvents(event_name, self.year, m, system) for m in range(1, 13)] or_op = BitOpOr(system, *months) self.redis_key = or_op.redis_key @@ -505,7 +494,7 @@ class MonthEvents(GenericPeriodEvents): Example:: - MonthEvents('active', 2012, 10) + MonthEvents("active", 2012, 10) """ @classmethod @@ -519,7 +508,7 @@ def __init__(self, event_name, year=None, month=None, system="default"): self.year = not_none(year, now.year) self.month = not_none(month, now.month) self.system = system - self.redis_key = _prefix_key(event_name, "%s-%s" % (self.year, self.month)) + self.redis_key = _prefix_key(event_name, f"{self.year}-{self.month}") def delta(self, value): year, month = add_month(self.year, self.month, value) @@ -539,7 +528,7 @@ class WeekEvents(GenericPeriodEvents): Example:: - WeekEvents('active', 2012, 48) + WeekEvents("active", 2012, 48) """ @classmethod @@ -557,7 +546,7 @@ def __init__(self, event_name: str, year=None, week=None, system="default"): self.year = not_none(year, now_year) self.week = not_none(week, now_week) self.system = system - self.redis_key = _prefix_key(event_name, "W%s-%s" % (self.year, self.week)) + self.redis_key = _prefix_key(event_name, f"W{self.year}-{self.week}") def delta(self, value): dt = iso_to_gregorian(self.year, self.week + value, 1) @@ -579,7 +568,7 @@ class DayEvents(GenericPeriodEvents): Example:: - DayEvents('active', 2012, 10, 23) + DayEvents("active", 2012, 10, 23) """ @classmethod @@ -594,9 +583,7 @@ def __init__(self, event_name, year=None, month=None, day=None, system="default" self.month = not_none(month, now.month) self.day = not_none(day, now.day) self.system = system - self.redis_key = _prefix_key( - event_name, "%s-%s-%s" % (self.year, self.month, self.day) - ) + self.redis_key = _prefix_key(event_name, f"{self.year}-{self.month}-{self.day}") def delta(self, value): dt = date(self.year, self.month, self.day) + timedelta(days=value) @@ -615,7 +602,7 @@ class HourEvents(GenericPeriodEvents): Example:: - HourEvents('active', 2012, 10, 23, 13) + HourEvents("active", 2012, 10, 23, 13) """ @classmethod @@ -642,7 +629,7 @@ def __init__( self.hour = not_none(hour, now.hour) self.system = system self.redis_key = _prefix_key( - event_name, "%s-%s-%s-%s" % (self.year, self.month, self.day, self.hour) + event_name, f"{self.year}-{self.month}-{self.day}-{self.hour}" ) def delta(self, value): @@ -678,16 +665,16 @@ class BitOperation( Example:: active_2_months = BitOpAnd( - MonthEvents('active', last_month.year, last_month.month), - MonthEvents('active', now.year, now.month) + MonthEvents("active", last_month.year, last_month.month), + MonthEvents("active", now.year, now.month), ) active_2_months = BitOpAnd( BitOpAnd( - MonthEvents('active', last_month.year, last_month.month), - MonthEvents('active', now.year, now.month) + MonthEvents("active", last_month.year, last_month.month), + MonthEvents("active", now.year, now.month), ), - MonthEvents('active', now.year, now.month) + MonthEvents("active", now.year, now.month), ) """ @@ -702,7 +689,9 @@ def __init__(self, op_name: str, system_or_event, *events): event_redis_keys = [ev.redis_key for ev in events] - self.redis_key = "trackist_bitop_%s_%s" % (op_name, "-".join(event_redis_keys)) + self.redis_key = "trackist_bitop_{}_{}".format( + op_name, "-".join(event_redis_keys) + ) _bitop_keys()[system].add(self.redis_key) cli = get_redis(system) @@ -733,7 +722,7 @@ def __init__(self, system_or_event, *events): def _prefix_key(event_name: str, date: str) -> str: - return "trackist_%s_%s" % (event_name, date) + return f"trackist_{event_name}_{date}" # --- Helper functions @@ -752,23 +741,22 @@ def add_month(year: int, month: int, delta: int) -> tuple[int, int]: def not_none(*keys): - """ - Helper function returning first value which is not None - """ + """Helper function returning first value which is not None""" for key in keys: if key is not None: return key + return None def iso_year_start(iso_year: int) -> date: - "The gregorian calendar date of the first day of the given ISO year" + """The gregorian calendar date of the first day of the given ISO year""" fourth_jan = date(iso_year, 1, 4) delta = timedelta(fourth_jan.isoweekday() - 1) return fourth_jan - delta def iso_to_gregorian(iso_year: int, iso_week: int, iso_day: int) -> date: - "Gregorian calendar date for the given ISO year, week and day" + """Gregorian calendar date for the given ISO year, week and day""" year_start = iso_year_start(iso_year) return year_start + timedelta(days=iso_day - 1, weeks=iso_week - 1) diff --git a/bitmapist/cohort/__init__.py b/bitmapist/cohort/__init__.py index b6d550b..54687f8 100644 --- a/bitmapist/cohort/__init__.py +++ b/bitmapist/cohort/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ bitmapist.cohort ~~~~~~~~~~~~~~~~ @@ -62,6 +61,7 @@ :developer: Amir Salihefendic ( http://amix.dk ) :license: BSD """ + from datetime import date, datetime, timedelta from os import path from typing import Any, Callable, Optional @@ -182,9 +182,7 @@ def render_csv_data( num_of_rows: int = 12, start_date=None, ): - """ - Render's data as CSV. - """ + """Render's data as CSV.""" return ( get_lookup() .get_template("table_data_csv.mako") @@ -241,14 +239,18 @@ def get_dates_data( date_range = num_results now = now - timedelta(days=num_results - 1) - timedelta_inc = lambda d: timedelta(days=d) + + def timedelta_inc(d): + return timedelta(days=d) # Weeks elif time_group == "weeks": fn_get_events = _weeks_events_fn date_range = num_results now = now - relativedelta(weeks=num_results - 1) - timedelta_inc = lambda w: relativedelta(weeks=w) + + def timedelta_inc(w): + return relativedelta(weeks=w) # Months elif time_group == "months": fn_get_events = _month_events_fn @@ -256,7 +258,9 @@ def get_dates_data( date_range = num_results now = now - relativedelta(months=num_results - 1) now -= timedelta(days=now.day - 1) - timedelta_inc = lambda m: relativedelta(months=m) + + def timedelta_inc(m): + return relativedelta(months=m) # Years elif time_group == "years": fn_get_events = _year_events_fn @@ -265,11 +269,13 @@ def get_dates_data( date_range = num_results now = now - relativedelta(years=num_results - 1) - timedelta_inc = lambda m: relativedelta(years=m) + + def timedelta_inc(m): + return relativedelta(years=m) dates = [] - for i in range(0, date_range): + for _i in range(date_range): result = [now] # events for select1 (+select1b) @@ -282,7 +288,7 @@ def get_dates_data( result.append(select1_count) # Move in time - for t_delta in range(0, num_of_rows + 1): + for t_delta in range(num_of_rows + 1): if select1_count == 0: result.append("") continue @@ -304,12 +310,11 @@ def get_dates_data( # Append to result if both_count == 0: - result.append(float(0.0)) + result.append(0.0) + elif as_precent: + result.append((float(both_count) / float(select1_count)) * 100) else: - if as_precent: - result.append((float(both_count) / float(select1_count)) * 100) - else: - result.append(both_count) + result.append(both_count) dates.append(result) now = now + timedelta_inc(1) @@ -339,9 +344,12 @@ def set_custom_handler(event_name: str, callback) -> None: using web or ios, could look like:: def active_web_ios(key, cls, cls_args): - return cls('active', *cls_args) & (cls('web', *cls_args) | cls('ios', *cls_args)) + return cls("active", *cls_args) & ( + cls("web", *cls_args) | cls("ios", *cls_args) + ) - set_custom_handler('active_web_ios', active_web_ios) + + set_custom_handler("active_web_ios", active_web_ios) And then use something like:: @@ -362,8 +370,7 @@ def active_web_ios(key, cls, cls_args): def _dispatch(key: str, cls: type[GenericPeriodEvents], cls_args): if key in CUSTOM_HANDLERS: return CUSTOM_HANDLERS[key](key, cls, cls_args) - else: - return cls(key, *cls_args) + return cls(key, *cls_args) def _day_events_fn(key: str, date: date, system: str): diff --git a/poetry.lock b/poetry.lock index 2eb5861..85c934d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -209,6 +209,64 @@ files = [ {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, ] +[[package]] +name = "mypy" +version = "1.10.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, + {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, + {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, + {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, + {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, + {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, + {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, + {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, + {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, + {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, + {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, + {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, + {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, + {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, + {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, + {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, + {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, + {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + [[package]] name = "nodeenv" version = "1.8.0" @@ -377,6 +435,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -429,6 +488,32 @@ async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2 hiredis = ["hiredis (>=1.0.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] +[[package]] +name = "ruff" +version = "0.4.1" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.4.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2d9ef6231e3fbdc0b8c72404a1a0c46fd0dcea84efca83beb4681c318ea6a953"}, + {file = "ruff-0.4.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9485f54a7189e6f7433e0058cf8581bee45c31a25cd69009d2a040d1bd4bfaef"}, + {file = "ruff-0.4.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2921ac03ce1383e360e8a95442ffb0d757a6a7ddd9a5be68561a671e0e5807e"}, + {file = "ruff-0.4.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eec8d185fe193ad053eda3a6be23069e0c8ba8c5d20bc5ace6e3b9e37d246d3f"}, + {file = "ruff-0.4.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa27d9d72a94574d250f42b7640b3bd2edc4c58ac8ac2778a8c82374bb27984"}, + {file = "ruff-0.4.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f1ee41580bff1a651339eb3337c20c12f4037f6110a36ae4a2d864c52e5ef954"}, + {file = "ruff-0.4.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0926cefb57fc5fced629603fbd1a23d458b25418681d96823992ba975f050c2b"}, + {file = "ruff-0.4.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6e37f2e3cd74496a74af9a4fa67b547ab3ca137688c484749189bf3a686ceb"}, + {file = "ruff-0.4.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd703a5975ac1998c2cc5e9494e13b28f31e66c616b0a76e206de2562e0843c"}, + {file = "ruff-0.4.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b92f03b4aa9fa23e1799b40f15f8b95cdc418782a567d6c43def65e1bbb7f1cf"}, + {file = "ruff-0.4.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1c859f294f8633889e7d77de228b203eb0e9a03071b72b5989d89a0cf98ee262"}, + {file = "ruff-0.4.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b34510141e393519a47f2d7b8216fec747ea1f2c81e85f076e9f2910588d4b64"}, + {file = "ruff-0.4.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6e68d248ed688b9d69fd4d18737edcbb79c98b251bba5a2b031ce2470224bdf9"}, + {file = "ruff-0.4.1-py3-none-win32.whl", hash = "sha256:b90506f3d6d1f41f43f9b7b5ff845aeefabed6d2494307bc7b178360a8805252"}, + {file = "ruff-0.4.1-py3-none-win_amd64.whl", hash = "sha256:c7d391e5936af5c9e252743d767c564670dc3889aff460d35c518ee76e4b26d7"}, + {file = "ruff-0.4.1-py3-none-win_arm64.whl", hash = "sha256:a1eaf03d87e6a7cd5e661d36d8c6e874693cb9bc3049d110bc9a97b350680c43"}, + {file = "ruff-0.4.1.tar.gz", hash = "sha256:d592116cdbb65f8b1b7e2a2b48297eb865f6bdc20641879aa9d7b9c11d86db79"}, +] + [[package]] name = "setuptools" version = "69.0.3" @@ -494,6 +579,17 @@ virtualenv = ">=20.24.3" docs = ["furo (>=2023.8.19)", "sphinx (>=7.2.4)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.24)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=1)", "diff-cover (>=7.7)", "distlib (>=0.3.7)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.18)", "psutil (>=5.9.5)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "re-assert (>=1.1)", "time-machine (>=2.12)", "wheel (>=0.41.2)"] +[[package]] +name = "typing-extensions" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + [[package]] name = "virtualenv" version = "20.25.0" @@ -517,4 +613,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "bc83aa2957b9cc05c8d54cb2c4469675558d8723b77e61c7ca2f5880c1a9e580" +content-hash = "0fb66e80077fd3537c2230a17eb98fe3732680e71c8161203fe3ac9b6ea7fffe" diff --git a/pyproject.toml b/pyproject.toml index 64803d7..8b04d03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,192 @@ pytest-runner = "*" pytest = "*" pre-commit = "*" tox = "*" +mypy = "*" +ruff = "^0.4.1" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.ruff] +# By default, always show source code snippets. +output-format = 'full' + +extend-exclude = [ + "env", + "runtime", +] + +[tool.ruff.lint] +select = [ + "ASYNC", # flake8-async + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "D", # pydocstyle, + "E", "W", # pycodestyle + "F", # pyflakes + "I", # isort + "PL", # pylint + "RUF", # ruff + "S", # flake8-bandit + "T20", # flake8-print + "SIM", # flake8-simplify + "UP", # pyupgrade + "TCH", # flake8-type-checking + "TRY", # tryceratops + "BLE", # flake8-blind-except + "LOG", # flake8-logging + "G", # flake8-logging-format + "RET", # flake8-logging-return + "ISC", # flake8-implicit-str-concat + "INP", # flake8-no-pep420 + "PIE", # flake8-pie + "PT", # flake8-pytest-style + "PERF", # perflint +] + +ignore = [ + ## D - pydocstyle ## + # D1XX errors are OK. Don't force people into over-documenting. + "D100", "D101", "D102", "D103", "D104", "D105", "D107", + # These need to be fixed. + "D205", + "D400", + "D401", + + ## E / W - pycodestyle ## + "E501", # line too long + + ## PL - pylint ## + # Commented-out rules are rules that we disable in pylint but are not supported by ruff yet. + + "PLR6301", # no-self-use + "PLC2701", # import-private-name + + # Import order issues + # "PLC0411", # wrong-import-order + # "PLC0412", # wrong-import-position + "PLC0414", # ungrouped-imports + "PLC0415", # import-outside-top-level + + # flake8-implicit-str-concat + "ISC001", # May conflict with the formatter + + # Documentation issues + # "C0114", # missing-module-docstring + + # Complexity issues + "PLR0904", # too-many-public-methods + # "PLC0302", # too-many-lines + "PLR1702", # too-many-nested-blocks + # "PLR0902", # too-many-instance-attributes + "PLR0911", # too-many-return-statements + "PLR0915", # too-many-statements + "PLR0912", # too-many-branches + # "PLR0903", # too-few-public-methods + "PLR0914", # too-many-locals + # "PLC0301", # line-too-long + "PLR0913", # too-many-arguments + "PLR0917", # too-many-positional + "PLR2004", # magic-value-comparison + "PLW0603", # global-statement + "PLW2901", # redefined-loop-name + + ## RUF - ruff ## + "RUF001", # ambiguous-unicode-character-string + "RUF002", # ambiguous-unicode-character-docstring + "RUF003", # ambiguous-unicode-character-comment + "RUF012", # mutable-class-default + + # Enable when Poetry supports PEP 621 and we migrate our confguration to it. + # See: https://github.com/python-poetry/poetry-core/pull/567 + "RUF200", + + "S101", # assert + "S104", # hardcoded-bind-all-interfaces + "S105", # hardcoded-password-string + "S106", # hardcoded-password-func-arg + "S303", # suspicious-insecure-hash-usage + "S310", # suspicious-url-open-usage + "S311", # suspicious-non-cryptographic-random-usage + "S324", # hashlib-insecure-hash-function + "S603", # subprocess-without-shell-equals-true + "S607", # start-process-with-partial-path + "S608", # hardcoded-sql-expression + + ## SIM - flake8-simplify ## + "SIM102", # collapsible-if + "SIM114", # if-with-same-arms + "SIM117", # multiple-with-statements + + # Enable when the rule is out of preview and false-positives are handled. + # See: https://docs.astral.sh/ruff/rules/in-dict-keys/ + "SIM118", # in-dict-keys + + ## TRY - tryceratops ## + "TRY003", # raise-vanilla-args + "TRY004", # type-check-without-type-error + "TRY301", # raise-within-try + + ## BLE - flake8-blind-except ## + "BLE001", # blind-except + + ## RET - flake8-return ## + "RET504", # unnecessary-assign + + ## PT - flake8-pytest-style ## + "PT004", # pytest-missing-fixture-name-underscore + "PT012", # pytest-raises-with-multiple-statements + + ## UP - pyupgrade ## + "UP038", # non-pep604-isinstance + + ## B - flake8-bugbear ## + "B008", # function-call-in-default-argument + "B009", # get-attr-with-constant + "B010", # set-attr-with-constant + "B018", # useless-expression +] + +flake8-pytest-style.fixture-parentheses = false +flake8-pytest-style.mark-parentheses = false + +pylint.allow-dunder-method-names = [ + "__json__", + "__get_pydantic_core_schema__" +] + +[tool.ruff.lint.flake8-type-checking] +runtime-evaluated-base-classes = [ + "pydantic.BaseModel", + "typing_extensions.TypedDict", + "sqlalchemy.orm.DeclarativeBase", +] +runtime-evaluated-decorators = [ + "pydantic.dataclasses.dataclass", + "pydantic.validate_call", +] + +[tool.ruff.lint.per-file-ignores] +# Open devnull without a context manager + "conftest.py" = ["SIM115"] + +[tool.ruff.lint.isort] +section-order = [ + "future", + "standard-library", + "third-party", + "first-party", + "local-folder", +] + +[tool.ruff.lint.pydocstyle] +convention = "pep257" + +[tool.ruff.lint.pyupgrade] +# Required by tools like Pydantic that use type information at runtime. +# https://github.com/asottile/pyupgrade/issues/622#issuecomment-1088766572 +keep-runtime-typing = true + +[tool.ruff.format] +docstring-code-format = true diff --git a/test/conftest.py b/test/conftest.py index 21c8306..9614948 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,19 +1,17 @@ -from builtins import range - -import pytest -import os -import subprocess import atexit +import os import socket +import subprocess import time -from bitmapist import setup_redis, delete_all_events + +import pytest + +from bitmapist import delete_all_events, setup_redis @pytest.yield_fixture(scope="session", autouse=True) def redis_server(): - """ - Fixture starting the Redis server - """ + """Fixture starting the Redis server""" redis_host = "127.0.0.1" redis_port = 6399 if is_socket_open(redis_host, redis_port): @@ -38,10 +36,8 @@ def clean_redis(): def start_redis_server(port): - """ - Helper function starting Redis server - """ - devzero = open(os.devnull, "r") + """Helper function starting Redis server""" + devzero = open(os.devnull) devnull = open(os.devnull, "w") proc = subprocess.Popen( ["/usr/bin/redis-server", "--port", str(port)], @@ -55,18 +51,14 @@ def start_redis_server(port): def is_socket_open(host, port): - """ - Helper function which tests is the socket open - """ + """Helper function which tests is the socket open""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(0.1) return sock.connect_ex((host, port)) == 0 def wait_for_socket(host, port, seconds=3): - """ - Check if socket is up for :param:`seconds` sec, raise an error otherwise - """ + """Check if socket is up for :param:`seconds` sec, raise an error otherwise""" polling_interval = 0.1 iterations = int(seconds / polling_interval) diff --git a/test/test_bitmapist.py b/test/test_bitmapist.py index 981859c..be69709 100644 --- a/test/test_bitmapist.py +++ b/test/test_bitmapist.py @@ -1,17 +1,16 @@ -# -*- coding: utf-8 -*- from datetime import datetime, timedelta from bitmapist import ( - mark_event, - unmark_event, - YearEvents, - MonthEvents, - WeekEvents, - DayEvents, - HourEvents, BitOpAnd, BitOpOr, + DayEvents, + HourEvents, + MonthEvents, + WeekEvents, + YearEvents, get_event_names, + mark_event, + unmark_event, ) @@ -202,9 +201,9 @@ def test_bitop_key_sharing(): ev2_both = BitOpAnd(ev2_task1, ev2_task2) assert ev1_both.redis_key == ev2_both.redis_key - assert len(ev1_both) == len(ev1_both) == 2 + assert len(ev1_both) == len(ev2_both) == 2 ev1_both.delete() - assert len(ev1_both) == len(ev1_both) == 0 + assert len(ev1_both) == len(ev2_both) == 0 def test_events_marked(): diff --git a/test/test_cohort.py b/test/test_cohort.py index e146086..a46117d 100644 --- a/test/test_cohort.py +++ b/test/test_cohort.py @@ -1,6 +1,7 @@ -import pytest from datetime import datetime, timedelta +import pytest + from bitmapist import mark_event from bitmapist.cohort import get_dates_data @@ -28,7 +29,7 @@ def events(): @pytest.mark.parametrize( - "select1,select1b,select2,select2b, expected", + ("select1", "select1b", "select2", "select2b", "expected"), [ ("active", None, "active", None, [2, 100, 50]), ("active", None, "unknown", None, [2, "", ""]), diff --git a/test/test_from_date.py b/test/test_from_date.py index 082b89c..f181a5a 100644 --- a/test/test_from_date.py +++ b/test/test_from_date.py @@ -1,6 +1,7 @@ -import bitmapist import datetime +import bitmapist + def test_from_date_year(): ev1 = bitmapist.YearEvents.from_date("foo", datetime.datetime(2014, 1, 1)) diff --git a/test/test_period_start_end.py b/test/test_period_start_end.py index 5f19e13..2c86396 100644 --- a/test/test_period_start_end.py +++ b/test/test_period_start_end.py @@ -1,6 +1,7 @@ -# -*- coding: utf-8 -*- -import pytest import datetime + +import pytest + import bitmapist