diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..dd5a140 --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,49 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python package + +on: + push: + branches: + - master + - develop + - dev/v** + - release-** + pull_request: + branches: + - master + - develop + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements_dev.txt ]; then pip install -r requirements_dev.txt; fi + - name: Lint with flake8 + run: | + flake8 borax tests + - name: Test with pytest + run: | + nose2 --with-coverage --coverage borax --coverage-report xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + file: ./coverage.xml + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true + verbose: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6aa1236..0000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: python -python: - - "3.6" - - "3.7" - - "3.8" -branches: - only: - - master - - develop - - /^release-.*$/ - - /^beta-.*$/ -install: - - pip install -r requirements_dev.txt -script: - - nose2 --with-coverage --coverage borax - - flake8 borax tests \ No newline at end of file diff --git a/README-en.md b/README-en.md deleted file mode 100644 index 19b20bd..0000000 --- a/README-en.md +++ /dev/null @@ -1,166 +0,0 @@ -# Borax- python3 development util collections - - -[![PyPI](https://img.shields.io/pypi/v/borax.svg)](https://pypi.org/project/borax) -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/borax.svg)](https://pypi.org/project/borax) -[![PyPI - Status](https://img.shields.io/pypi/status/borax.svg)](https://github.com/kinegratii/borax) - - - - -## Overview & Installation - -Borax is a utils collections for python3 development, which contains some common data structures and the implementation of design patterns. - -Use *pip* to install the package: - -```shell -$ pip install borax - -``` - -Or checkout source code: - -```shell -git clone https://github.com/kinegratii/borax.git -cd borax -python setup.py install -``` - -## Modules Usage - -### lunardate - -> The dataset and algorithm is referenced from [jjonline/calendar.js](https://github.com/jjonline/calendar.js). - -```python -from borax.calendars.lunardate import LunarDate - -# Get the date instance of today. -print(LunarDate.today()) # LunarDate(2018, 7, 1, 0) - -# Convert a solar date to the lunar date. -ld = LunarDate.from_solar_date(2018, 8, 11) -print(ld) # LunarDate(2018, 7, 1, 0) - -# Return the lunar date after 10 days. - -print(ld.after(10)) # LunarDate(2018, 7, 11, 0) -``` - -Return the lunar date after 10 days. - -``` ->>>ld.after(10) -LunarDate(2018, 7, 11, 0) -``` - -### Festivals - -How many days away from spring festival,my birth day,Chinese New Year's Eve. - -```python -from borax.calendars.festivals import get_festival, LunarSchema, DayLunarSchema - -festival = get_festival('春节') -print(festival.countdown()) # 7 - -ls = LunarSchema(month=11, day=1) -print(ls.countdown()) # 285 - -dls = DayLunarSchema(month=12, day=1, reverse=1) -print(dls.countdown()) # 344 -``` - -### Financial Capital Numbers - -Convert amount to financial capital numbers. - -``` ->>> from borax.finance import financial_amount_capital ->>> financial_amount_capital(100000000) -'壹亿元整' ->>>financial_amount_capital(4578442.23) -'肆佰伍拾柒万捌仟肆佰肆拾贰元贰角叁分' ->>>financial_amount_capital(107000.53) -壹拾万柒仟元伍角叁分 -``` - -### Singleton - -``` ->>>from borax.patterns.singleton import MetaSingleton ->>>class SingletonM(metaclass=MetaSingleton):pass ->>>a = SingletonM() ->>>b = SingletonM() ->>>id(a) == id(b) -True -``` - -### Fetch - -A function sets for fetch the values of some axises. - - -Get list values from dict list. - -```python -from borax.datasets.fetch import fetch - -objects = [ - {'id': 282, 'name': 'Alice', 'age': 30}, - {'id': 217, 'name': 'Bob', 'age': 56}, - {'id': 328, 'name': 'Charlie', 'age': 56}, -] - -names = fetch(objects, 'name') -print(names) -``` - -Output - -``` -['Alice', 'Bob', 'Charlie'] -``` - -## Document - -The document site is powered by [docsify](https://docsify.js.org/), and hosted on the folowing site: - -| Source | Page Link | -| ---- | ---- | -| github | [https://kinegratii.github.io/borax](https://kinegratii.github.io/borax) | -| gitee | [https://kinegratii.gitee.io/borax](https://kinegratii.gitee.io/borax) | - -## Development Features - -- [x] [Typing Hints](https://www.python.org/dev/peps/pep-0484/) -- [x] [Flake8 Code Style](http://flake8.pycqa.org/en/latest/) -- [x] [nose2](https://pypi.org/project/nose2/) -- [x] [Travis CI](https://travis-ci.org) -- [x] [Docsify](https://docsify.js.org) - -## License - -``` -The MIT License (MIT) - -Copyright (c) 2015-2020 kinegratii - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` \ No newline at end of file diff --git a/README.md b/README.md index 657bf20..c0421c3 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,48 @@ -# Borax - python开发工具集合 +# Borax - python3工具集合库 [![PyPI](https://img.shields.io/pypi/v/borax.svg)](https://pypi.org/project/borax) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/borax.svg)](https://pypi.org/project/borax) [![PyPI - Status](https://img.shields.io/pypi/status/borax.svg)](https://github.com/kinegratii/borax) -[![Build Status](https://travis-ci.org/kinegratii/borax.svg?branch=master)](https://travis-ci.org/kinegratii/borax) +![Python package](https://github.com/kinegratii/borax/workflows/Python%20package/badge.svg) +![Codecov](https://codecov.io/github/kinegratii/borax/coverage.svg) -## 概述 & 安装 +## 概述 (Overview) -Borax 是一个 Python3 开发工具集合库,涉及到: +Borax 是一个Python3工具集合库。包括了以下几个话题: - - 设计模式 - - 数据结构及其实现 - - 一些简单函数的封装 +| 话题(Topics) | 内容 | +| ------------------- | ----------------------------------------------------- | +| Borax.Calendars | 1900-2100年的中国农历日期库 | +| Borax.Choices | 声明式的选项类。适用于Django.models.choices 定义。 | +| Borax.Datasets | 记录型数据操作库,包括连结(Join)、列选择(fetch)等 | +| Borax.DataStuctures | 树形结构,json数据 | +| Borax.Numbers | 数字库。包括中文数字、百分数等。 | +| Borax.Patterns | 设计模式。包括单例模式、代理对象、延迟对象。 | -使用 *pip* : +## 安装 (Installation) + +Borax 要求 Python3.5+ 。 + +可以通过以下两种方式安装 : + +1) 使用 *pip* : ```shell $ pip install borax ``` -## 使用示例 +2) 使用 [poetry](https://poetry.eustace.io/) 工具: + +```shell +$ poetry add borax +``` + +## 使用示例 (Usage) -### 中国农历日期 +### Borax.LunarDate: 中国农历日期 一个支持1900-2100年的农历日期工具库。 @@ -33,7 +51,7 @@ $ pip install borax 创建日期,日期推算 ```python -from borax.calendars.lunardate import LunarDate +from borax.calendars import LunarDate # 获取今天的农历日期(农历2018年七月初一) print(LunarDate.today()) # LunarDate(2018, 7, 1, 0) @@ -51,13 +69,13 @@ print(ld.after(10)) # LunarDate(2018, 7, 11, 0) ```python today = LunarDate.today() -print(today.strftime('%Y-%M-%D')) # '二〇一八-六-廿六' +print(today.strftime('%Y年%L%M月%D')) # '二〇一八年六月廿六' print(today.strftime('今天的干支表示法为:%G')) # '今天的干支表示法为:戊戌年庚申月辛未日' ``` -### 国内外节日 +### Borax.Festival: 国内外节日 -分别计算距离 “春节”、生日(十一月初一)、“除夕(农历十二月的最后一天)” 还有多少天 +分别计算距离 “春节”、“除夕(农历十二月的最后一天)” 还有多少天 ```python from borax.calendars.festivals import get_festival, LunarSchema, DayLunarSchema @@ -65,28 +83,40 @@ from borax.calendars.festivals import get_festival, LunarSchema, DayLunarSchema festival = get_festival('春节') print(festival.countdown()) # 7 -ls = LunarSchema(month=11, day=1) -print(ls.countdown()) # 285 - dls = DayLunarSchema(month=12, day=1, reverse=1) print(dls.countdown()) # 344 ``` -### 大写金额 +### Borax.Numbers: 中文数字处理 -将金额转化为符合标准的大写数字。 +不同形式的中文数字 + +```python +from borax.numbers import ChineseNumbers + +# 小写、计量 +print(ChineseNumbers.to_chinese_number(204)) # '二百零四' +# 小写、编号 +print(ChineseNumbers.order_number(204)) # '二百〇四' +# 大写、计量 +print(ChineseNumbers.to_chinese_number(204, upper=True)) # '贰佰零肆' +# 大写、编号 +print(ChineseNumbers.to_chinese_number(204, upper=True, order=True)) # '贰佰〇肆' ``` ->>> from borax.numbers import FinanceNumbers ->>> FinanceNumbers.to_capital_str(100000000) -'壹亿元整' ->>>FinanceNumbers.to_capital_str(4578442.23) -'肆佰伍拾柒万捌仟肆佰肆拾贰元贰角叁分' ->>>FinanceNumbers.to_capital_str(107000.53) -壹拾万柒仟元伍角叁分 + +财务金额 + +```python +from borax.numbers import FinanceNumbers +print(FinanceNumbers.to_capital_str(100000000)) # '壹亿元整' +print(FinanceNumbers.to_capital_str(4578442.23)) # '肆佰伍拾柒万捌仟肆佰肆拾贰元贰角叁分' + +print(FinanceNumbers.to_capital_str(107000.53)) # '壹拾万柒仟元伍角叁分' + ``` -### 数据拾取 +### Borax.Datasets: 数据列选择 从数据序列中选择一个或多个字段的数据。 @@ -103,24 +133,24 @@ names = fetch(objects, 'name') print(names) # ['Alice', 'Bob', 'Charlie'] ``` -## 文档 +## 文档 (Document) 文档由 [docsify](https://docsify.js.org/) 构建。 | 源 | 网址 | | ---- | ---- | -| github | [https://kinegratii.github.io/borax](https://kinegratii.github.io/borax) | +| github | [https://kinegratii.github.io/borax](https://kinegratii.github.io/borax) | | gitee | [https://kinegratii.gitee.io/borax](https://kinegratii.gitee.io/borax) | -## 开发特性和规范 +## 开发特性和规范 (Development Features) - [x] [Typing Hints](https://www.python.org/dev/peps/pep-0484/) - [x] [Flake8 Code Style](http://flake8.pycqa.org/en/latest/) -- [x] [nose2](https://pypi.org/project/nose2/) -- [x] [Travis CI](https://travis-ci.org) -- [x] [Docsify](https://docsify.js.org) +- [x] [nose2](https://pypi.org/project/nose2/) | [pytest](https://docs.pytest.org/en/latest/) +- [x] [Github Action](https://github.com/kinegratii/borax/actions) +- [x] [Code Coverage](https://codecov.io/) -## 开源协议 +## 开源协议 (License) ``` The MIT License (MIT) @@ -145,7 +175,7 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -## 捐赠 +## 捐赠 (Donate) 如果你觉得这个项目帮助到了你,你可以帮作者们买一杯咖啡表示感谢! diff --git a/borax/__init__.py b/borax/__init__.py index 29a6d57..b3bb84e 100644 --- a/borax/__init__.py +++ b/borax/__init__.py @@ -1,4 +1,4 @@ # coding=utf8 -__version__ = '3.3.1' +__version__ = '3.4.0' __author__ = 'kinegratii' diff --git a/borax/calendars/__init__.py b/borax/calendars/__init__.py index 9cccbef..d9a40ba 100644 --- a/borax/calendars/__init__.py +++ b/borax/calendars/__init__.py @@ -1,5 +1,6 @@ # coding=utf8 -from .lunardate import LunarDate +from .lunardate import LunarDate, InvalidLunarDateError, LCalendars +from .utils import SCalendars -__all__ = ['LunarDate'] +__all__ = ['LunarDate', 'InvalidLunarDateError', 'LCalendars', 'SCalendars'] diff --git a/borax/calendars/lunardate.py b/borax/calendars/lunardate.py index 3963afc..1ff9b59 100644 --- a/borax/calendars/lunardate.py +++ b/borax/calendars/lunardate.py @@ -9,7 +9,15 @@ from typing import Optional, Iterator, Tuple, Union -__all__ = ['LunarDate', 'LCalendars'] +__all__ = ['LunarDate', 'LCalendars', 'InvalidLunarDateError'] + + +# Exception + +class InvalidLunarDateError(ValueError): + # The InvalidLunarDateError will not be the subclass of ValueError in v4.0. + pass + # Typing @@ -28,7 +36,7 @@ def _check_year_range(year): if year < MIN_LUNAR_YEAR or year > MAX_LUNAR_YEAR: - raise ValueError('year out of range [1900, 2100]') + raise InvalidLunarDateError('[year={}]: Year must be in the range [1900, 2100]'.format(year)) # lunar year 1900~2100 @@ -129,7 +137,16 @@ def ndays(year: int, month: Optional[int] = None, leap: Leap = False) -> int: if (_month, _leap) == (month, leap): return _days else: - raise ValueError('Invalid month for the year {}'.format(year)) + raise InvalidLunarDateError('[year={},month={},leap={}]: Invalid month.'.format(year, month, leap)) + + @staticmethod + def get_leap_years(month: int = 0) -> tuple: + res = [] + for yoffset, yinfo in enumerate(YEAR_INFOS): + leap_month = yinfo % 16 + if leap_month > 0 and (month == 0 or leap_month == month): + res.append(MIN_LUNAR_YEAR + yoffset) + return tuple(res) @staticmethod def create_solar_date(year: int, term_index: Optional[int] = None, @@ -164,7 +181,7 @@ def delta(date1, date2): # offset <----> year, day_offset <----> year, month, day, leap -def offset2ymdl(offset): +def offset2ymdl(offset: int) -> Tuple[int, int, int, Leap]: def _o2mdl(_year_info, _offset): for _month, _days, _leap in _iter_year_month(_year_info): if _offset < _days: @@ -199,10 +216,11 @@ def _mdl2o(_year_info, _month, _day, _leap): res += _day - 1 return res else: - raise ValueError("day out of range") + raise InvalidLunarDateError( + "[year={},month={},day={},leap={}]:Invalid day".format(year, month, day, leap)) res += _days_ - raise ValueError("month out of range") + raise InvalidLunarDateError('[year={},month={},leap={}]: Invalid month.'.format(year, month, leap)) offset = 0 _check_year_range(year) @@ -416,16 +434,25 @@ def _get_gz_ymd(self): @property def cn_year(self) -> str: - return '{}年'.format(TextUtils.year_cn(self.year)) + return '{}'.format(TextUtils.year_cn(self.year)) @property def cn_month(self) -> str: - return '{}{}月'.format('闰' if self.leap else '', TextUtils.month_cn(self.month)) + return '{}'.format(TextUtils.month_cn(self.month)) @property def cn_day(self) -> str: return '{}'.format(TextUtils.day_cn(self.day)) + @property + def cn_leap(self) -> str: + return '闰' if self.leap else '' + + @property + def cn_month_num(self) -> str: + mstr = self.cn_month + return {'冬': '十一', '腊': '十二'}.get(mstr, mstr) + @property def cn_day_calendar(self) -> str: if self.day == 1: @@ -433,14 +460,14 @@ def cn_day_calendar(self) -> str: else: return self.cn_day - def weekday(self): + def weekday(self) -> int: return (self.offset + 2) % 7 - def isoweekday(self): + def isoweekday(self) -> int: return (self.offset + 3) % 7 or 7 def cn_str(self) -> str: - return '{}{}{}'.format(self.cn_year, self.cn_month, self.cn_day) + return '{}年{}{}月{}'.format(self.cn_year, self.cn_leap, self.cn_month, self.cn_day) def gz_str(self) -> str: return '{}年{}月{}日'.format(self.gz_year, self.gz_month, self.gz_day) @@ -593,6 +620,7 @@ class Formatter: '%q': 'gz_day', '%C': 'cn_str', '%G': 'gz_str', + '%N': 'cn_month_num', '%%': '%' } @@ -629,26 +657,20 @@ def resolve(self, obj, field): # Custom values - def get_leap(self, obj): - return int(obj.leap) + def get_term(self, obj: LunarDate) -> str: + return obj.term or '-' + + def get_leap(self, obj: LunarDate) -> str: + return str(int(obj.leap)) - def get_cn_leap(self, obj): + def get_cn_leap(self, obj: LunarDate) -> str: if obj.leap: return '闰' else: return '' - def get_cn_year(self, obj): - return TextUtils.year_cn(obj.year) - - def get_cn_month(self, obj): - return TextUtils.month_cn(obj.month) - - def get_cn_day(self, obj): - return TextUtils.day_cn(obj.day) - - def get_padding_month(self, obj): + def get_padding_month(self, obj: LunarDate) -> str: return '{0:02d}'.format(obj.month) - def get_padding_day(self, obj): + def get_padding_day(self, obj: LunarDate) -> str: return '{0:02d}'.format(obj.day) diff --git a/borax/calendars/utils.py b/borax/calendars/utils.py index 673178c..1a687e3 100644 --- a/borax/calendars/utils.py +++ b/borax/calendars/utils.py @@ -3,10 +3,12 @@ from datetime import date, datetime -def get_last_day_of_this_month(year: int, month: int) -> date: - return date(year, month, calendar.monthrange(year, month)[-1]) - - -def get_fist_day_of_year_week(year: int, week: int) -> date: - fmt = '{}-W{}-1'.format(year, week) - return datetime.strptime(fmt, "%Y-W%W-%w").date() +class SCalendars: + @staticmethod + def get_last_day_of_this_month(year: int, month: int) -> date: + return date(year, month, calendar.monthrange(year, month)[-1]) + + @staticmethod + def get_fist_day_of_year_week(year: int, week: int) -> date: + fmt = '{}-W{}-1'.format(year, week) + return datetime.strptime(fmt, "%Y-W%W-%w").date() diff --git a/borax/choices.py b/borax/choices.py index 2bfe097..187dd37 100644 --- a/borax/choices.py +++ b/borax/choices.py @@ -1,8 +1,8 @@ # coding=utf8 - - from collections import OrderedDict +from typing import Dict + __all__ = ['Item', 'ConstChoices'] @@ -10,30 +10,44 @@ class Item: _order = 0 def __init__(self, value, display=None, *, order=None): - self.value = value + self._value = value if display is None: - self.display = str(value) + self._display = str(value) else: - self.display = str(display) + self._display = str(display) if order is None: Item._order += 1 self.order = Item._order else: self.order = order + @property + def value(self): + return self._value + + @property + def display(self): + return self._display + + @property + def label(self): + return self._display + + def __str__(self): + return '<{0} value={1!r} label={2!r}>'.format(self.__class__.__name__, self.value, self.label) + + __repr__ = __str__ + class ChoicesMetaclass(type): def __new__(cls, name, bases, attrs): - choices = [] - display_lookup = {} - - fields = {} + fields = {} # {:} parents = [b for b in bases if isinstance(b, ChoicesMetaclass)] for kls in parents: - for field_name in kls._fields: - fields[field_name] = kls._fields[field_name] + for field_name in kls.fields: + fields[field_name] = kls.fields[field_name] for k, v in attrs.items(): if k.startswith('_'): @@ -41,34 +55,48 @@ def __new__(cls, name, bases, attrs): if isinstance(v, Item): fields[k] = v elif isinstance(v, (tuple, list)) and len(v) == 2: - fields[k] = Item(value=v[0], display=v[1]) + fields[k] = Item(v[0], v[1]) elif isinstance(v, (int, float, str, bytes)): - fields[k] = Item(value=v, display=v) + fields[k] = Item(v) fields = OrderedDict(sorted(fields.items(), key=lambda x: x[1].order)) + for field_name, item in fields.items(): + attrs[field_name] = item.value # override the exists attrs __dict__ + + new_cls = super().__new__(cls, name, bases, attrs) + new_cls._fields = fields + return new_cls - for field_name in fields: - val_item = fields[field_name] + @property + def fields(cls) -> Dict[str, Item]: + return dict(cls._fields) + + @property + def choices(cls) -> list: + return [(item.value, item.label) for _, item in cls.fields.items()] - if isinstance(val_item, Item): - choices.append((val_item.value, val_item.display)) - display_lookup[val_item.value] = val_item.display - attrs[field_name] = val_item.value - else: - choices.append((field_name, val_item.choices)) + @property + def names(cls) -> tuple: + return tuple(cls.fields.keys()) - attrs['_fields'] = fields - attrs['_choices'] = choices - attrs['_display_lookup'] = display_lookup - return type.__new__(cls, name, bases, attrs) + @property + def values(cls) -> tuple: + return tuple([value for value, _ in cls.choices]) + + @property + def displays(cls) -> tuple: + return tuple([display for _, display in cls.choices]) @property - def choices(self): - return self._choices + def labels(cls) -> tuple: + return cls.displays @property - def display_lookup(self): - return self._display_lookup + def display_lookup(cls) -> dict: + return {value: label for value, label in cls.choices} + + def __contains__(self, item): + return item in self.values def __iter__(self): for item in self.choices: @@ -77,12 +105,13 @@ def __iter__(self): def __len__(self): return len(self.choices) - -class ConstChoices(metaclass=ChoicesMetaclass): - @classmethod - def is_valid(cls, value): + # API + def is_valid(cls, value) -> bool: return value in cls.display_lookup - @classmethod def get_value_display(cls, value, default=None): return cls.display_lookup.get(value, default) + + +class ConstChoices(metaclass=ChoicesMetaclass): + pass diff --git a/borax/choices3.py b/borax/choices3.py deleted file mode 100644 index ac80365..0000000 --- a/borax/choices3.py +++ /dev/null @@ -1,100 +0,0 @@ -# coding=utf8 - -from collections import OrderedDict - - -class BItem: - _order = 0 - - def __init__(self, value, label=None, *, order=-1): - self._value = value - self._label = label - if order is None: - BItem._order += 1 - self.order = BItem._order - else: - self.order = order - - @property - def value(self): - return self._value - - @property - def label(self): - return self._label - - def __eq__(self, other): - return self._value == other - - -class BChoicesMeta(type): - def __new__(cls, name, bases, attrs): - - fields = {} # {:} - - parents = [b for b in bases if isinstance(b, BChoicesMeta)] - for kls in parents: - for field_name in kls._fields: - fields[field_name] = kls._fields[field_name] - - for k, v in attrs.items(): - if k.startswith('_'): - continue - if isinstance(v, BItem): - fields[k] = v - elif isinstance(v, (tuple, list)) and len(v) == 2: - fields[k] = BItem(v[0], v[1]) - elif isinstance(v, (int, float, str, bytes)): - fields[k] = BItem(v, k.lower()) - - # FIXME unordered dict for python3.5 - - fields = OrderedDict(sorted(fields.items(), key=lambda x: x[1].order)) - for field_name, item in fields.items(): - attrs[field_name] = item - - new_cls = super().__new__(cls, name, bases, attrs) - new_cls._fields = fields - return new_cls - - @property - def fields(cls): - return cls._fields - - @property - def choices(cls): - return [(item.value, item.label) for _, item in cls.fields.items()] - - @property - def values(cls): - return [value for value, _ in cls.choices] - - @property - def labels(cls): - return [label for _, label in cls.choices] - - @property - def display_lookup(cls): - return {value: label for value, label in cls.choices} - - def get_value_display(cls, item): - return cls.display_lookup.get(item) - - def __getattr__(self, item): - if item in self.fields: - return self.fields[item] - return super().__getattr__(item) - - def __contains__(self, item): - return item in self.values - - def __iter__(self): - for item in self.choices: - yield item - - def __len__(self): - return len(self.choices) - - -class BChoices(metaclass=BChoicesMeta): - pass diff --git a/borax/counters/serial_pool.py b/borax/counters/serial_pool.py new file mode 100644 index 0000000..e2678f1 --- /dev/null +++ b/borax/counters/serial_pool.py @@ -0,0 +1,193 @@ +# coding=utf8 + +import re +from itertools import chain + +from typing import List, Union, Iterable, Generator, Optional, Callable + +# ---------- Custom typing ---------- +ElementsType = List[Union[int, str]] + + +def serial_no_generator(lower: int = 0, upper: int = 10, reused: bool = True, values: Iterable[int] = None) -> \ + Generator[int, None, None]: + values = values or [] + eset = set(filter(lambda x: lower <= x < upper, values)) + if eset: + max_val = max(eset) + if reused: + gen = chain(range(max_val + 1, upper), range(lower, max_val)) + else: + gen = range(max_val + 1, upper) + else: + gen = range(lower, upper) + for ele in gen: + if ele in eset: + continue + yield ele + + +_fmt_re = re.compile(r'\{no(:0(\d+)([bodxX]))?\}') + +b2p_dict = {'b': 2, 'o': 8, 'd': 10, 'x': 16, 'X': 16} +p2b_dict = {2: 'b', 8: 'o', 10: 'd', 16: 'x'} + + +class LabelFormatOpts: + def __init__(self, fmt_str, base=10, digits=2): + + base_char = p2b_dict[base] + data = _fmt_re.findall(fmt_str) + ft = [item[0] for item in data if item[0] != ''] + if ft: + if all(el == ft[0] for el in ft): + base_char = ft[0][-1] + base, digits = b2p_dict.get(base_char), int(ft[0][2:-1]) + else: + raise ValueError('{} Define different formatter for no variable.'.format(fmt_str)) + new_field_fmt = '{{no:0{0}{1}}}'.format(digits, base_char) + + self.origin_fmt = fmt_str + self.normalized_fmt = _fmt_re.sub(new_field_fmt, fmt_str) + + fr_dict = { + 'b': '(?P[01]{{{0}}})'.format(digits), + 'o': '(?P[0-7]{{{0}}})'.format(digits), + 'd': '(?P[0-9]{{{0}}})'.format(digits), + 'x': '(?P[0-9a-f]{{{0}}})'.format(digits), + 'X': '(?P[0-9A-Z]{{{0}}})'.format(digits), + } + + self.parse_re = re.compile(self.normalized_fmt.replace(new_field_fmt, fr_dict[base_char])) + self.base = base + self.digits = digits + self.base_char = base_char + + def value2label(self, value: int) -> str: + return self.normalized_fmt.format(no=value) + + def label2value(self, label: str) -> int: + m = self.parse_re.match(label) + if m: + return int(m.group('no'), base=self.base) + + +class SerialElement: + __slots__ = ['value', 'label'] + + def __init__(self, value, label): + self.value = value + self.label = label + + +class SerialNoPool: + def __init__(self, lower: int = None, upper: int = None, base: int = 0, digits: int = 0, + label_fmt: Optional[str] = None): + + if label_fmt is None: + self._opts = None + else: + base = base or 10 + digits = digits or 2 + self._opts = LabelFormatOpts(label_fmt, base, digits) + base = self._opts.base + digits = self._opts.digits + + if lower is not None and lower < 0: + raise ValueError('lower(={}) must be >= 0.'.format(lower)) + if upper is not None and upper <= 0: + raise ValueError('upper(={}) must be >= 0.'.format(upper)) + s_set = base and digits + t_set = lower is not None and upper is not None + + if t_set: + self._lower, self._upper = lower, upper + if s_set: + cl, cu = 0, base ** digits + if not (lower >= cl and upper <= cu): + raise ValueError('The lower-upper [{},{}) is not in [{},{})'.format(lower, upper, cl, cu)) + else: + if s_set: + self._lower, self._upper = 0, base ** digits + else: + self._lower, self._upper = 0, 100 + + self._values = set() + self._source = None + + # ---------- Pool Attributes ---------- + + @property + def lower(self): + return self._lower + + @property + def upper(self): + return self._upper + + # ---------- Data API ---------- + + def set_elements(self, elements: ElementsType) -> 'SerialNoPool': + self._values = set() + self.add_elements(elements) + return self + + def set_source(self, source: Callable[[], ElementsType]) -> 'SerialNoPool': + self._source = source + return self + + def add_elements(self, elements: ElementsType) -> 'SerialNoPool': + values = self._elements2values(elements) + for v in values: + self._values.add(v) + return self + + def remove_elements(self, elements: ElementsType) -> 'SerialNoPool': + values = self._elements2values(elements) + for v in values: + self._values.remove(v) + return self + + def _elements2values(self, elements: ElementsType) -> List[int]: + values = [] # type: List[int] + for ele in elements: + if isinstance(ele, int): + value = ele + elif isinstance(ele, str): + value = self._opts.label2value(ele) + else: + raise TypeError('Invalid element {}:unsupported type.'.format(ele)) + if self._lower <= value < self._upper: + values.append(value) + else: + raise ValueError('Invalid element {}: range error'.format(ele)) + return values + + # ---------- Generate API ---------- + + def get_next_generator(self) -> Generator[SerialElement, None, None]: + """ + This is the low-level method. + :return: + """ + if self._source is not None: + elements = self._source() + self.set_elements(elements) + value_gen = serial_no_generator(lower=self._lower, upper=self._upper, values=self._values) + for value in value_gen: + if self._opts: + label = self._opts.value2label(value) + else: + label = None + yield SerialElement(value, label) + + def generate_values(self, num=1) -> List[int]: + return [se.value for se in self.get_next_generator()][:num] + + def generate_labels(self, num=1) -> List[str]: + if self._opts is None: + raise TypeError('The operation generate_labels is not allowed when label_fmt is not set.') + return [se.label for se in self.get_next_generator()][:num] + + def generate(self, num=1) -> List[str]: + return self.generate_labels(num) diff --git a/borax/counters/serials.py b/borax/counters/serials.py index 82075d3..6163bca 100644 --- a/borax/counters/serials.py +++ b/borax/counters/serials.py @@ -64,7 +64,7 @@ def __init__(self, prefix: str, digits: int = 2, base: int = 10): raise ValueError('Invalid base value {}.Choices are: 2, 8, 10, 16'.format(base)) self._num_fmt = '{{0:0{0}{1}}}'.format(digits, num_fmt[base]) self._base = base - super().__init__(lower=0, upper=self._base ** digits - 1) + super().__init__(lower=0, upper=self._base ** digits) def generate(self, num: int) -> List[str]: res = super().generate(num) diff --git a/borax/datasets/__init__.py b/borax/datasets/__init__.py index 43c46f6..735b79b 100644 --- a/borax/datasets/__init__.py +++ b/borax/datasets/__init__.py @@ -1,5 +1 @@ # coding=utf8 - -from borax.datasets.dict_datasets import DictDataset - -__all__ = ['DictDataset'] diff --git a/borax/datasets/dict_datasets.py b/borax/datasets/dict_datasets.py deleted file mode 100644 index fcabb79..0000000 --- a/borax/datasets/dict_datasets.py +++ /dev/null @@ -1,17 +0,0 @@ -# coding=utf8 - - -class DictDataset: - def __init__(self, data, primary_field=None): - self._data = [] - if data: - self._data = list(data) - self._primary_field = primary_field - - @property - def data(self): - return self._data - - def __iter__(self): - for item in self.data: - yield item diff --git a/borax/datasets/join_.py b/borax/datasets/join_.py index 877ecca..8ed2dae 100644 --- a/borax/datasets/join_.py +++ b/borax/datasets/join_.py @@ -1,6 +1,7 @@ # coding=utf8 import operator +import copy __all__ = ['join_one', 'join', 'old_join_one', 'old_join'] @@ -103,6 +104,16 @@ def _pick_data(_item, _sfs): return ldata +def deep_join_one(ldata, rdata, on, select_as, default=None): + ldata = copy.deepcopy(ldata) + return join_one(ldata, rdata, on, select_as, default=default) + + +def deep_join(ldata, rdata, on, select_as): + ldata = copy.deepcopy(ldata) + return join(ldata, rdata, on, select_as) + + def old_join_one(data_list, values, from_, as_, default=None): if isinstance(values, (list, tuple)): values = dict(values) diff --git a/borax/devtools.py b/borax/devtools.py new file mode 100644 index 0000000..18ddf7a --- /dev/null +++ b/borax/devtools.py @@ -0,0 +1,45 @@ +# coding=utf8 + + +import time +from collections import defaultdict, namedtuple +from contextlib import contextmanager + +from typing import List, Dict + +TagMeasureResult = namedtuple('TagMeasureResult', 'name total count avg') + + +class RuntimeMeasurer: + def __init__(self): + self._data = defaultdict(list) + self._start_time_dict = {} + + def start(self, *tags: List[str]) -> 'RuntimeMeasurer': + st = time.time() + for _tag in tags: + self._start_time_dict[_tag] = st + return self + + def end(self, *tags: List[str]) -> 'RuntimeMeasurer': + et = time.time() + for _tag in tags: + if _tag in self._start_time_dict: + self._data[_tag].append(et - self._start_time_dict[_tag]) + return self + + @contextmanager + def measure(self, *tags: List[str]): + try: + self.start(*tags) + yield + finally: + self.end(*tags) + + def get_measure_result(self) -> Dict[str, TagMeasureResult]: + result = {} + for tag, values in self._data.items(): + tv = sum(values) + cv = len(values) + result[tag] = TagMeasureResult(tag, tv, cv, tv / cv) + return result diff --git a/borax/finance.py b/borax/finance.py index 641bafc..0275c16 100644 --- a/borax/finance.py +++ b/borax/finance.py @@ -13,7 +13,7 @@ def financial_amount_capital(num: Union[int, float, Decimal, str]) -> str: warnings.warn( - 'This method is deprecated and will be removed in V3.5.Use borax.numbers.FinanceNumbers instead.', + 'This method is deprecated and will be removed in v4.0.Use borax.numbers.FinanceNumbers instead.', category=PendingDeprecationWarning, stacklevel=2 ) diff --git a/borax/htmls.py b/borax/htmls.py index e185e79..aba45b9 100644 --- a/borax/htmls.py +++ b/borax/htmls.py @@ -31,10 +31,10 @@ def escape(cls, s): return rv -def html_params(**kwargs): +def html_params(**kwargs) -> str: params = [] - for k, v in sorted(kwargs.items()): - if k in ('class_', 'class__', 'for_'): + for k, v in kwargs.items(): + if k in ('class_', 'class__', 'for_', 'id_'): k = k[:-1] elif k.startswith('data_'): k = k.replace('_', '-') @@ -49,6 +49,6 @@ def html_params(**kwargs): def html_tag(tag_name, content=None, **kwargs): if content: - return HTMLString('<{0} {1}>{2}'.format(tag_name, html_params(**kwargs), content)) + return HTMLString('<{0} {1}>{2}'.format(tag_name, html_params(**kwargs), content)) else: - return HTMLString('<0 {1}/>'.format(tag_name, html_params(**kwargs))) + return HTMLString('<{0} {1} />'.format(tag_name, html_params(**kwargs))) diff --git a/borax/numbers.py b/borax/numbers.py index b02ded7..4431301 100644 --- a/borax/numbers.py +++ b/borax/numbers.py @@ -6,7 +6,13 @@ from typing import Union -MAX_VALUE_LIMIT = 1000000000000 +MAX_VALUE_LIMIT = 1000000000000 # 10^12 + +LOWER_UNITS = '千百十亿千百十万千百十_' +LOWER_DIGITS = '零一二三四五六七八九' + +UPPER_UNITS = '仟佰拾亿仟佰拾万仟佰拾_' +UPPER_DIGITS = '零壹贰叁肆伍陆柒捌玖' class ChineseNumbers: @@ -16,13 +22,11 @@ class ChineseNumbers: (r'零{2,}', '零'), (r'零([亿|万])', r'\g<1>'), (r'亿零{0,3}万', '亿'), - (r'零 ', ''), + (r'零?_', ''), ] @staticmethod - def to_chinese_number(num: Union[int, str]) -> str: - units = '千百十亿千百十万千百十 ' - digits = '零一二三四五六七八九' + def measure_number(num: Union[int, str]) -> str: if isinstance(num, str): _n = int(num) else: @@ -30,17 +34,32 @@ def to_chinese_number(num: Union[int, str]) -> str: if _n < 0 or _n >= MAX_VALUE_LIMIT: raise ValueError('Out of range') num_str = str(num) - capital_str = ''.join([digits[int(i)] for i in num_str]) - s_units = units[len(units) - len(num_str):] + capital_str = ''.join([LOWER_DIGITS[int(i)] for i in num_str]) + s_units = LOWER_UNITS[len(LOWER_UNITS) - len(num_str):] o = ''.join('{}{}'.format(u, d) for u, d in zip(capital_str, s_units)) for p, d in ChineseNumbers.RULES: o = re.sub(p, d, o) if 10 <= _n < 20: o.replace('一十', '十') - return o + @staticmethod + def order_number(num: Union[int, str]) -> str: + val = ChineseNumbers.measure_number(num) + return val.replace('零', '〇') + + @staticmethod + def to_chinese_number(num: Union[int, str], upper: bool = False, order: bool = False) -> str: + if order: + lower_string = ChineseNumbers.order_number(num) + else: + lower_string = ChineseNumbers.measure_number(num) + if upper: + for _ld, _ud in zip(LOWER_DIGITS + LOWER_UNITS[:3], UPPER_DIGITS + UPPER_UNITS[:3]): + lower_string = lower_string.replace(_ld, _ud) + return lower_string + class FinanceNumbers: RULES = [ @@ -55,8 +74,7 @@ class FinanceNumbers: @staticmethod def to_capital_str(num: Union[int, float, Decimal, str]) -> str: - units = '仟佰拾亿仟佰拾万仟佰拾元角分' - digits = '零壹贰叁肆伍陆柒捌玖' + units = UPPER_UNITS[:-1] + '元角分' if isinstance(num, str): _n = Decimal(num) else: @@ -68,7 +86,7 @@ def to_capital_str(num: Union[int, float, Decimal, str]) -> str: dot_pos = num_str.find('.') if dot_pos > -1: num_str = num_str[:dot_pos] + num_str[dot_pos + 1:dot_pos + 3] - capital_str = ''.join([digits[int(i)] for i in num_str]) + capital_str = ''.join([UPPER_DIGITS[int(i)] for i in num_str]) s_units = units[len(units) - len(num_str):] o = ''.join('{}{}'.format(u, d) for u, d in zip(capital_str, s_units)) diff --git a/borax/runtime.py b/borax/runtime.py deleted file mode 100644 index 217a8e3..0000000 --- a/borax/runtime.py +++ /dev/null @@ -1,43 +0,0 @@ -# coding=utf8 - - -import time -from collections import defaultdict -from contextlib import contextmanager - - -class RuntimeMeasurer: - def __init__(self): - self._data = defaultdict(list) - self._start_time_dict = {} - - def start(self, tag, *tags): - tags = (tag,) + tags - st = time.time() - for _tag in tags: - self._start_time_dict[_tag] = st - - def end(self, tag, *tags): - tags = (tag,) + tags - et = time.time() - for _tag in tags: - if _tag in self._start_time_dict: - self._data[_tag].append(et - self._start_time_dict[_tag]) - - @contextmanager - def measure(self, tag, *tags): - try: - self.start(tag, *tags) - yield - finally: - self.end(tag, *tags) - - def get_measure_data(self): - data = [] - for tag, values in self._data.items(): - data.append({ - 'tag': tag, - 'total': len(values), - 'avg': sum(values) / len(values) - }) - return data diff --git a/borax/serialize/bjson.py b/borax/serialize/bjson.py index 3d2004f..f01a6d0 100644 --- a/borax/serialize/bjson.py +++ b/borax/serialize/bjson.py @@ -1,9 +1,12 @@ # coding=utf8 import json +import warnings __all__ = ['EncoderMixin', 'BJSONEncoder'] +warnings.warn('This module is deprecated, use borax.serialize.cjson instead.', DeprecationWarning, stacklevel=2) + class EncoderMixin: def __json__(self): diff --git a/borax/serialize/cjson.py b/borax/serialize/cjson.py index 3b355bf..46fe17b 100644 --- a/borax/serialize/cjson.py +++ b/borax/serialize/cjson.py @@ -1,18 +1,36 @@ # coding=utf8 + + import json +from datetime import datetime, date from functools import singledispatch -__all__ = ['to_serializable', 'dumps', 'dump'] +__all__ = ['encode_object', 'encoder', 'dumps', 'dump', 'to_serializable'] -@singledispatch -def to_serializable(obj): +def encode_object(obj): + if hasattr(obj, '__json__'): + return obj.__json__() raise TypeError('Type {} is not JSON serializable'.format(obj.__class__.__name__)) +def _unregister(self, cls): + self.register(cls, encode_object) + + +encoder = singledispatch(encode_object) +encoder.unregister = _unregister.__get__(encoder) # see more detail on https://stackoverflow.com/a/28060251 + + def dumps(obj, **kwargs): - return json.dumps(obj, default=to_serializable, **kwargs) + return json.dumps(obj, default=encoder, **kwargs) def dump(obj, fp, **kwargs): - return json.dump(obj, fp, default=to_serializable, **kwargs) + return json.dump(obj, fp, default=encoder, **kwargs) + + +to_serializable = encoder + +encoder.register(datetime, lambda obj: obj.strftime('%Y-%m-%d %H:%M:%S')) +encoder.register(date, lambda obj: obj.strftime('%Y-%m-%d')) diff --git a/borax/strings.py b/borax/strings.py index dfa56e7..b911694 100644 --- a/borax/strings.py +++ b/borax/strings.py @@ -2,12 +2,12 @@ import re -def camel2snake(s): +def camel2snake(s: str) -> str: camel_to_snake_regex = r'((?<=[a-z0-9])[A-Z]|(?!^)(? str: snake_to_camel_regex = r"(?:^|_)(.)" return re.sub(snake_to_camel_regex, lambda m: m.group(1).upper(), s) diff --git a/borax/structures/dictionary.py b/borax/structures/dictionary.py index 5500156..dce9d1b 100644 --- a/borax/structures/dictionary.py +++ b/borax/structures/dictionary.py @@ -1,60 +1,5 @@ # coding=utf8 - -from collections import namedtuple - -__all__ = ['EMPTY', 'AliasItem', "AliasDictionary"] - -EMPTY = object() - -AliasItem = namedtuple('AliasItem', 'alias key value') - - -class AliasDictionary: - """ - A dictionary in support of access value with alias name. - """ - - def __init__(self, data, aliases=None): - self._aliases = aliases or {} - self._data = data - - def add_aliases(self, **kwargs): - self._aliases.update(**kwargs) - - def get(self, name, default=None): - key = self._aliases.get(name, name) - return self._data.get(key, default) - - def get_item(self, name, default=EMPTY, raise_exception=False): - if default is EMPTY: - raise_exception = True - - if name in self._data: - key, value = name, self._data[name] - else: - if name in self._aliases: - _f_key = self._aliases[name] - if _f_key in self._data: - key, value = _f_key, self._data[_f_key] - else: - if raise_exception: - raise KeyError('{}'.format(name)) - else: - key, value = None, default - else: - if raise_exception: - raise KeyError('{}'.format(name)) - else: - key, value = None, default - return AliasItem(name, key, value) - - def get_available_items(self): - for key, value in self._data.items(): - aliases = [k for k, v in self._aliases.items() if v == key] - yield key, value, aliases - - class AttributeDict(dict): def __getattr__(self, key): try: @@ -66,11 +11,5 @@ def __getattr__(self, key): def __setattr__(self, key, value): self[key] = value - def first(self, *names): - for name in names: - value = self.get(name) - if value: - return value - AD = AttributeDict diff --git a/borax/structures/lookup.py b/borax/structures/lookup.py deleted file mode 100644 index 9dbbead..0000000 --- a/borax/structures/lookup.py +++ /dev/null @@ -1,33 +0,0 @@ -# coding=utf8 - -import collections - - -class TableLookup: - def __init__(self, fields, primary=None): - self._fields = fields - self._item_class = collections.namedtuple('Item', fields) - self._dataset = collections.OrderedDict() - if primary: - self.primary = primary - self._primary_index = fields.index(primary) - else: - self.primary = fields[0] - self._primary_index = 0 - - def feed(self, table_data): - for data_item in table_data: - self._dataset.update({ - data_item[self._primary_index]: self._item_class(*data_item) - }) - return self - - def find(self, key, default=None): - return self._dataset.get(key, default) - - def select_as_dict(self, field): - return {k: getattr(v, field) for k, v in self._dataset.items()} - - def __iter__(self): - for item in self._dataset.values(): - yield item diff --git a/borax/structures/percentage.py b/borax/structures/percentage.py index 746a5b7..4067892 100644 --- a/borax/structures/percentage.py +++ b/borax/structures/percentage.py @@ -51,7 +51,7 @@ def display(self) -> str: """old alias name for fraction_display'""" return self.fraction_display - def as_dict(self, prefix='') -> dict: + def as_dict(self, prefix: str = '') -> dict: return { prefix + 'total': self.total, prefix + 'completed': self.completed, @@ -60,7 +60,7 @@ def as_dict(self, prefix='') -> dict: prefix + 'display': self.display } - def generate(self, char_total=100) -> str: + def generate(self, char_total: int = 100) -> str: char_completed = int(self.percent * char_total) return '|{0}{1}| {2:.2f}%'.format( '▇' * char_completed, diff --git a/borax/system.py b/borax/system.py index e00653c..688c20d 100644 --- a/borax/system.py +++ b/borax/system.py @@ -17,7 +17,7 @@ def load_class(s): return getattr(mod, class_) -def check_path_variables(execute_filename): +def check_path_variables(execute_filename: str) -> bool: try: user_paths = os.environ['PYTHONPATH'].split(os.pathsep) except KeyError: @@ -39,7 +39,7 @@ def check_path_variables(execute_filename): SUFFIX_DATE_UNDERLINE = '%Y_%m_%d' -def rotate_filename(filename: str, time_fmt: str = DatetimeFormat.SUFFIX_DT, sep: str = '_', now=None, **kwargs): +def rotate_filename(filename: str, time_fmt: str = DatetimeFormat.SUFFIX_DT, sep: str = '_', now=None, **kwargs) -> str: """ Rotate filename or filepath with datetime string as suffix. :param filename: :param time_fmt: diff --git a/borax/utils.py b/borax/utils.py index 477b32c..c7127c0 100644 --- a/borax/utils.py +++ b/borax/utils.py @@ -34,6 +34,13 @@ def chain_getattr(obj, attr, value=None): def get_item_cycle(data, index, start=0): + """ + get_item_cycle(data, index) == list(itertools.cycle(data))[:index-start][-1] + :param data: + :param index: + :param start: + :return: + """ length = len(data) return data[((index - start) % length + length) % length] @@ -86,7 +93,7 @@ def flatten(iterable): """flat a iterable. https://stackoverflow.com/a/2158532 """ for el in iterable: - if isinstance(el, collections.Iterable) and not isinstance(el, (str, bytes)): + if isinstance(el, collections.abc.Iterable) and not isinstance(el, (str, bytes)): yield from flatten(el) else: yield el @@ -94,8 +101,8 @@ def flatten(iterable): def force_list(val, sep=','): if isinstance(val, (list, set, tuple)): - return val + return tuple(val) elif isinstance(val, str): - return val.split(sep) + return tuple(val.split(sep)) else: return val, diff --git a/docs/README.md b/docs/README.md index 511caca..2bc41e5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,12 +4,13 @@ [![PyPI](https://img.shields.io/pypi/v/borax.svg)](https://pypi.org/project/borax) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/borax.svg)](https://pypi.org/project/borax) [![PyPI - Status](https://img.shields.io/pypi/status/borax.svg)](https://github.com/kinegratii/borax) +![Python package](https://github.com/kinegratii/borax/workflows/Python%20package/badge.svg) Borax 是一个Python3工具集合库。 - 本文档未开启版本化,所有内容都是基于最新版本,函数和类签名的变化请参见各自的文档说明。 + 本文档的所有内容都是基于最新版本,函数和类签名的变化参见各自的文档说明。 ## 开始(Quickstart) @@ -17,12 +18,14 @@ Borax 是一个Python3工具集合库。 ## 话题(Topics) -- **Borax.DataStructures**: [树形结构](guides/tree) | [bjson](guides/bjson) | [cjson](guides/cjson) | [百分数](guides/percentage) -- **Borax.Calendar**: [农历](guides/lunardate) | [节日](guides/festival) | [生日](guides/birthday) +- **Borax.Calendar**: [农历](guides/lunardate) | [节日](guides/festival) | [生日](guides/birthday) | [工具类](guides/calendars-utils) +- **Borax.Datasets**: [数据连接(Join)](guides/join) | [列选择器(fetch)](guides/fetch) +- **Borax.DataStructures**: [树形结构](guides/tree) | [bjson](guides/bjson) | [cjson](guides/cjson) +- **Borax.Numbers:**: [中文数字](guides/numbers) | [百分数](guides/percentage) - **Borax.Pattern**: [单例模式](guides/singleton) | [选项Choices](guides/choices) -- **Borax.Datasets**: [数据连接(Join)](guides/join) | [数据拾取](guides/fetch) -- **其他**: [序列号生成器](guides/serial_generator) |[数字模块](guides/numbers) | [Tkinter界面](guides/ui) +- **其他**: [序列号生成器](guides/serial_generator) | [序列号生成器(Pool)](guides/serial_pool) | [Tkinter界面](guides/ui) ## 开发(Development) +- **代码仓库**:[Github](https://github.com/kinegratii/borax/) | [Gitee (镜像)](https://gitee.com/kinegratii/borax) - **项目开发**: [版本日志](changelog) | [技术文档](develope/develope) \ No newline at end of file diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 4aebe8b..70f799f 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -1,23 +1,28 @@ - **● 入门** - [快速开始](quickstart) +- **● Borax.Calendar** + - [农历](guides/lunardate) + - [节日](guides/festival) + - [生日](guides/birthday) + - [工具类](guides/calendars-utils) +- **● Borax.Datasets** + - [数据连接(Join)New!](guides/join) + - [列选择器(fetch)](guides/fetch) - **● Borax.DataStructures** - [树形结构](guides/tree) - [bjson](guides/bjson) - [cjson](guides/cjson) +- **● Borax.Numbers** + - [中文数字 New!](guides/numbers) - [百分数](guides/percentage) -- **● Borax.Calendar** - - [农历](guides/lunardate) - - [节日](guides/festival) - - [生日](guides/birthday) - **● Borax.Pattern** - [单例模式](guides/singleton) - [选项Choices](guides/choices) -- **● Borax.Datasets** - - [数据连接(Join)New!](guides/join) - - [数据拾取](guides/fetch) +- **● Borax.Utils** + - [字符串工具模块](guides/strings) - **● 其他** - [序列号生成器](guides/serial_generator) - - [数字模块 New!](guides/numbers) + - [序列号生成器(Pool)](guides/serial_pool) - [Tkinter界面](guides/ui) - **● 项目开发** - [版本日志](changelog) diff --git a/docs/changelog.md b/docs/changelog.md index 449c341..d430dac 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,10 +1,35 @@ # 更新日志 +## v3.4.0 (20201115) + +> 新增 Python3.9构建支持 + +- **`borax.choices`** + - `ConstChoices` 新增 labels 、values 等属性 +- **`borax.calendars.lunarDate`** + - 新增 `%N` 月份描述符,将“冬”、“腊”显示为“十一”、“十二” + - 新增 `LCalendars.get_leap_years` 函数 + - 新增 `InvalidLunarDateError` 异常类 + - 修正农历平月日期 `%t` 格式化显示的BUG +- `borax.datasets.join` + - 新增 `deep_join` 、`deep_join_one` 使用赋值传参方式 +- **`borax.numbers`** + - `ChineseNumbers` 类新增 计量/编号 两种数字形式 +- **`borax.htmls`** + - 修正函数 `html_tag` 处理的BUG +- **`borax.serialize`** + - 整合 `bjson` 和 `cjson` ,`cjson` 支持 `__json__` 特性 + - 模块 `bjson` 标记为 `DeprecationWarning` 状态 +- **开发SOP** + - 支持 [Github Action](https://github.com/kinegratii/borax/actions) + - 更新依赖库,参见 *requirements_dev.txt* + - 新增代码覆盖率 [Codecov](https://codecov.io/) + ## v3.3.1 (20200827) - `borax.structures.dictionary` 模块 - 类 `AttrDict` 新增别名 `AD` -- 构建部署 +- 开发SOP - 修正因stacklevel 设置错误导致 `DeprecatedWarning` 无法正确提示的BUG - 参数化测试改用 `unittest.TestCase.subTest` - 支持 unittest / nose / nose2 / pytest 测试框架 @@ -12,17 +37,18 @@ ## v3.3.0 (20200815) -- 移除 `borax.fetch` 模块 - `borax.datasets.join_` 模块 - - `old_join` 和 `old_join_one` 标记为 PendingDeprecationWarning ,将在 V3.5移除 + - `old_join` 和 `old_join_one` 标记为 PendingDeprecationWarning ,将在 v3.5移除 - `borax.runtime` 模块 - `borax.numbers` 模块 (+) - 新增 `ChineseNumbers` 类 - 新增 `finaceNumbers` 类,由 `borax.finace` 模块转化 - `borax.finance` 模块 - 修正小数使用字符串时 `financial_amount_capital` 错误的BUG - - 本模块被标记为 `PendingDeprecationWarning` ,将在V3.5移除 -- 移除 `borax.structures.dic` 模块 + - 本模块被标记为 `PendingDeprecationWarning` +- 被移除模块 + - `borax.structures.dic` + - `borax.fetch` ## v3.2.0 (20200510) @@ -30,18 +56,18 @@ - `borax.datasets.join_`模块 - 重写 `join` 和 `join_one` 函数,原有的重命名为 `old_join` 和 `old_join_one` - - 原有的 `old_*` 将在V4.0版本移除。 + - 原有的 `old_*` 将在v4.0版本移除。 - 新增 `borax.calendars.utils` 模块 - `borax.structures.percentage` 模块 - 新增 `format_percentage` 函数 - 类 `Percentage` 新增 `fraction_display` 属性 - 当 total 为 0 ,显示为 `'-'` ,而不是 `'0.00%'` - `borax.fetch` 模块 - - 本模块被标记为 DeprecationWarning ,将在V3.3移除 + - 本模块被标记为 DeprecationWarning ,将在v3.3移除 ## v3.1.0 (20200118) -> 新增 Python3.8构建 +> 新增 Python3.8构建支持 - `datasets` 包 - 新增 `borax.datasets.fetch` @@ -50,7 +76,7 @@ - `calendars.lunardate` 模块 - 修正农历闰月转平月错误的BUG ([#11](https://github.com/kinegratii/borax/issues/11)) - `borax.fetch` 模块 - - 本模块被标记为 PendingDeprecationWarning ,将在V3.3移除 + - 本模块被标记为 PendingDeprecationWarning ,将在v3.3移除 ## v3.0.0 (20191125) diff --git a/docs/develope/develope.md b/docs/develope/develope.md index 554c533..8893f0e 100644 --- a/docs/develope/develope.md +++ b/docs/develope/develope.md @@ -2,7 +2,13 @@ 本节描述了Borax开源项目使用的技术思想、规范和工具。本页是一个简要的工具清单,具体可以查看[《python项目持续集成与包发布》](https://kinegratii.github.io/2017/04/25/python-project-ci-publish/) 这篇文章。 -## 编码 +## 编码实现 + +**关键字参数** + +> PEP 3102: https://www.python.org/dev/peps/pep-3102/ + +对于一些函数参数必须使用关键字方式调用。 **类型提示 - Typing Hints** @@ -14,23 +20,33 @@ > Flake8工具:http://flake8.pycqa.org/en/latest/ -## 持续集成 +## 单元测试 -**测试框架** +> 主页:https://docs.python.org/3/library/unittest.html -> 主页:https://pypi.org/project/nose2/ +**测试框架** 从v3.3.1 开始,测试用例全部使用标准的 unittest 代码,支持 unittest / nose / nose2 / pytest 等测试框架。 -**持续构建 - Travis CI** +**参数化测试** -> 主页: https://travis-ci.org +参见 `unittest.TestCase.subTest` 。 +**对象模拟** +参见 `unitest.mock` 。 + +## 持续集成 -Travis是一个在线持续集成的平台,支持github登录。配置文件是一个名为 *.travis.yaml* 的配置文件。 +> 主页: https://github.com/kinegratii/borax/actions -**发布 - twine** +Github Action 是 Github 推出的持续集成服务。包括以下内容: + +- 单元测试 +- 代码风格检查 +- 代码覆盖率 + +## 版本发布 [twine](https://pypi.python.org/pypi/twine) 是一个专门用于发布项目到PyPI的工具,可以使用 `pip install twine` 来安装,它的主要优点: @@ -41,13 +57,11 @@ Travis是一个在线持续集成的平台,支持github登录。配置文件 ## 文档 -- 项目徽章 - -在 [https://badge.fury.io](https://badge.fury.io/) 中输入项目名称并查找,把markdown格式复制到README.md文件。 - -点击Travis控制台build pass 图片并复制图片链接到README.md。 - **文档托管 - Docsify** > https://docsify.js.org + +## 依赖库 + +参见 *requirements_dev.txt* 文档。 \ No newline at end of file diff --git a/docs/develope/removed_module.md b/docs/develope/removed_module.md deleted file mode 100644 index 52f355d..0000000 --- a/docs/develope/removed_module.md +++ /dev/null @@ -1,3 +0,0 @@ -# 被移除模块列表 - -本文档描述了哪些已移除的模块。 \ No newline at end of file diff --git a/docs/guides/bjson.md b/docs/guides/bjson.md index 3ad502d..88f608c 100644 --- a/docs/guides/bjson.md +++ b/docs/guides/bjson.md @@ -2,7 +2,7 @@ > 模块:`borax.serialize.bjson` - +> This module has been deprecated in v3.4.0 and will be removed in v4.0. ## 使用方法 diff --git a/docs/guides/calendars-utils.md b/docs/guides/calendars-utils.md new file mode 100644 index 0000000..d7bb523 --- /dev/null +++ b/docs/guides/calendars-utils.md @@ -0,0 +1,22 @@ +# 日期工具库 + +> Add in v3.4.0 + +Borax.Calendars 提供了一系列适用于常见场景的工具方法。这些方法都定义在 `borax.calendars.SCalendars` (公历相关)和 `borax.calendars.LCalendars` (农历相关)类中。 + + + +## 公历工具SCalendars + +- `SCalendars.get_last_day_of_this_month(year: int, month: int) -> date` + +返回year年month月的最后一天日期。 + +- `SCalendars.get_fist_day_of_year_week(year: int, week: int) -> date` + +返回year年第week个星期第一天的日期。 + + + +## 农历日期LCalendars + diff --git a/docs/guides/choices.md b/docs/guides/choices.md index 1217995..770c1a6 100644 --- a/docs/guides/choices.md +++ b/docs/guides/choices.md @@ -2,11 +2,13 @@ > 模块: `borax.choices` +> Update in v3.4.0 +## 开发背景 -## 背景 +`borax.choices` 使用一种 “类声明(Class-Declaration)” 的方式表示 *具有唯一性约束的二维表格式数据* 。 -`borax.choices` 的出现是为了解决 `django.db.models.Field` 中 choices 的一些缺点。下面是一个典型的使用示例: +一个常用的应用场景是为了改进 `django.db.models.Field` 中 choices 定义方式。下面是一个在 Django1.x /2.x 中很常见的代码: ```python from django.db import models @@ -14,16 +16,16 @@ from django.db import models class Student(models.Model): MALE = 'male' # 1 FEMALE = 'female' - UNKOWN = 'unkown' + UNKNOWN = 'unknown' GENDER_CHOICES = ( (MALE, 'male'), # 2 (FEMALE, 'famale'), - (UNKOWN, 'unkown') + (UNKNOWN, 'unknown') ) gender = models.IntergerFIeld( choices=GENDER_CHOICES, - default=UNKOWN + default=UNKNOWN ) ``` @@ -32,7 +34,7 @@ class Student(models.Model): - choices 的定义冗长,每一个选项的内容通常会出现两次 - 每个选项都是挂在 model 下的,即使用 `Student.MALE` 形式访问,当同一个model出现多个choices时,无法很好的区分 -使用 Borax.Choices 可解决上述两个问题,并且代码更为简洁: +使用 Borax.Choices 改写将使得代码更为简洁: ```python from django.db import models @@ -41,17 +43,34 @@ from borax import choices class GenderChoices(choices.ConstChoices): MALE = choices.Item(1, 'male') FEMALE = choices.Item(2, 'female') - UNKOWN = choices.Item(3, 'unkown') + UNKNOWN = choices.Item(3, 'unknown') class Student(models.Model): gender = models.IntergerFIeld( - choices=GenderChoices, - default=GenderChoices.UNKOWN + choices=GenderChoices.choices, + default=GenderChoices.UNKNOWN ) ``` ## 使用示例 +例如对于表单的性别字段设计的逻辑数据如下: + +| 性别 | 数据库存储值 | 可读文本 | +| ------ | ------------ | -------- | +| 男 | 1 | Male | +| 女 | 2 | Female | +| 未填写 | 3 | Unknown | + +对应的类表示如下: + +```python +class GenderChoices(choices.ConstChoices): + MALE = choices.Item(1, 'male') + FEMALE = choices.Item(2,'female') + UNKNOWN = choices.Item(3, 'unknown') +``` + 每个可选选项集合都继承自 `choices.ConstChoices`, 并使用 `choices.Item` 列出所有的可选项。 ```python @@ -74,8 +93,32 @@ True >>> YearInShoolChoices.is_valid('Et') False ``` +## 选项(Item) + +### 显式Item定义 + +在类定义体使用 ` = ` 的格式定义选项。 - ## 简单选项 +名称 name 遵循 Python 变量命名规范,需要注意的是: + +- 以下划线("_")开始的变量不视为一个有效的选项 +- 变量名并不是必须使用大写形式 + +值 value 通常为一个 `Item` 对象,定义如下: + +```python +def __init__(value, display=None, *, order=None):pass +``` + +参数说明如下: + +- value : 保存的值,在一个 Choices 中该值是唯一的 +- display : 可读的文本信息 +- label: 同 display +- order :用于排序的数字 + + + ### 隐式Item定义 在某些选项稀少、意义明确的情况下,可以只使用简单的数据类型定义选项,这些形式包括: @@ -102,54 +145,35 @@ class YearInSchoolChoices(choices.ConstChoices): SENIOR = 'SR', 'Senior' ``` -## 选项(Item)定义 - -在类定义体使用 ` = ` 的格式定义选项。 - -名称 name 遵循 Python 变量命名规范,需要注意的是: - -- 以下划线("_")开始的变量不视为一个有效的选项 -- 变量名并不是必须使用大写形式 - -值 value 通常为一个 `Item` 对象,定义如下: +## ConstChoices API -```python -def __init__(value, display=None, *, order=None):pass -``` +以下所有的方法均为 `ConstChoices` 类的属性和方法。 -参数说明如下: +### 属性 -- value : 保存的值,在一个 Choices 中该值是唯一的 -- display : 可读的文本信息 -- order :用于排序的数字 +- **`ConstChoices.choices`** -## 选项复用和继承 +类型:`List[Tuple[Any, str]]`。所有选项列表。可直接赋值给 django.models.Field.choices 。 -可以使用类继承的方式实现选项的复用和重新定制某些选项的属性。 +类似于 `[(value1, display1), (value2, display2), ...]` 。 -```python -from borax import choices +- **`ConstChoices.fields`** -class VerticalChoices(choices.ConstChoices): - S = choices.Item('S', 'south') - N = choices.Item('N', 'north') +类型:`Dict[str,Item]`。选项字典。 +- **`ConstChoices.names`** -class DirectionChoices(VerticalChoices): - E = choices.Item('E', 'east') - W = choices.Item('W', 'west') -``` -默认情况下,子类的选项在父类选项之后,但可以使用 `order` 属性以调整顺序。 +类型:`List[str]`。类字段名称列表。 -## API +- **`ConstChoices.values`** -以下所有的方法均为 `ConstChoices` 类的属性和方法。 +类型:`List[Any]`。值列表。 -- **`ConstChoices.choices`** +- **`ConstChoices.labels`** -所有选项列表。可直接用于 django.models.Field.choices 。 +类型:`List[str]`。标签值列表。 -类似于 `[(value1, display1), (value2, display2), ...]` 。 +### 方法 - **`ConstChoices.is_valid(value)`** @@ -163,6 +187,28 @@ class DirectionChoices(VerticalChoices): 遍历 `ConstChoices.choices` +## 高级使用 + +### 选项复用和继承 + +可以使用类继承的方式实现选项的复用和重新定制某些选项的属性。 + +```python +from borax import choices + +class VerticalChoices(choices.ConstChoices): + S = choices.Item('S', 'south') + N = choices.Item('N', 'north') + + +class DirectionChoices(VerticalChoices): + E = choices.Item('E', 'east') + W = choices.Item('W', 'west') +``` +默认情况下,子类的选项在父类选项之后,但可以使用 `order` 属性以调整顺序。 + + + ## 关于Django.Choices ### 概述 @@ -214,7 +260,7 @@ class MyChoices(choices.Choices): | MyChoices.GREEN.display | - | - | | | MyChoices.get_value_display | `