timesense 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- timesense-1.0.0/CHANGELOG.md +48 -0
- timesense-1.0.0/CODE_OF_CONDUCT.md +28 -0
- timesense-1.0.0/CONTRIBUTING.md +42 -0
- timesense-1.0.0/LICENSE +21 -0
- timesense-1.0.0/MANIFEST.in +12 -0
- timesense-1.0.0/PKG-INFO +449 -0
- timesense-1.0.0/README.md +414 -0
- timesense-1.0.0/README.ru.md +422 -0
- timesense-1.0.0/SECURITY.md +19 -0
- timesense-1.0.0/SUPPORT.md +9 -0
- timesense-1.0.0/pyproject.toml +57 -0
- timesense-1.0.0/requirements.txt +3 -0
- timesense-1.0.0/setup.cfg +4 -0
- timesense-1.0.0/setup.py +8 -0
- timesense-1.0.0/timesense/__init__.py +35 -0
- timesense-1.0.0/timesense/core/__init__.py +1 -0
- timesense-1.0.0/timesense/core/calendar.py +117 -0
- timesense-1.0.0/timesense/core/config.py +65 -0
- timesense-1.0.0/timesense/core/language.py +178 -0
- timesense-1.0.0/timesense/core/parser.py +980 -0
- timesense-1.0.0/timesense/core/tokenizer.py +37 -0
- timesense-1.0.0/timesense/dict/__init__.py +3 -0
- timesense-1.0.0/timesense/dict/keywords.py +688 -0
- timesense-1.0.0/timesense/dict/morph_adapter.py +50 -0
- timesense-1.0.0/timesense/ics.py +157 -0
- timesense-1.0.0/timesense/locales/__init__.py +1 -0
- timesense-1.0.0/timesense/locales/en.py +1806 -0
- timesense-1.0.0/timesense/models/__init__.py +1 -0
- timesense-1.0.0/timesense/models/datetime_token.py +134 -0
- timesense-1.0.0/timesense/models/event_types.py +197 -0
- timesense-1.0.0/timesense/models/token.py +17 -0
- timesense-1.0.0/timesense/py.typed +0 -0
- timesense-1.0.0/timesense/recognizers/__init__.py +1 -0
- timesense-1.0.0/timesense/recognizers/base.py +21 -0
- timesense-1.0.0/timesense/recognizers/combined.py +291 -0
- timesense-1.0.0/timesense/recognizers/custom_time.py +29 -0
- timesense-1.0.0/timesense/recognizers/date.py +383 -0
- timesense-1.0.0/timesense/recognizers/date_format.py +66 -0
- timesense-1.0.0/timesense/recognizers/deadline_synonym.py +152 -0
- timesense-1.0.0/timesense/recognizers/duration.py +94 -0
- timesense-1.0.0/timesense/recognizers/exclusion.py +84 -0
- timesense-1.0.0/timesense/recognizers/location.py +37 -0
- timesense-1.0.0/timesense/recognizers/open_start.py +104 -0
- timesense-1.0.0/timesense/recognizers/ordinal_day.py +618 -0
- timesense-1.0.0/timesense/recognizers/period.py +475 -0
- timesense-1.0.0/timesense/recognizers/range.py +578 -0
- timesense-1.0.0/timesense/recognizers/recurrence.py +681 -0
- timesense-1.0.0/timesense/recognizers/relative.py +429 -0
- timesense-1.0.0/timesense/recognizers/season.py +121 -0
- timesense-1.0.0/timesense/recognizers/time.py +648 -0
- timesense-1.0.0/timesense/recognizers/time_of_day.py +62 -0
- timesense-1.0.0/timesense/recognizers/weekday_modifier.py +157 -0
- timesense-1.0.0/timesense/recognizers/working_context.py +83 -0
- timesense-1.0.0/timesense/recognizers/working_days.py +123 -0
- timesense-1.0.0/timesense/tests/__init__.py +0 -0
- timesense-1.0.0/timesense/tests/_pretty.py +247 -0
- timesense-1.0.0/timesense/tests/interactive.py +268 -0
- timesense-1.0.0/timesense/tests/test_en.py +263 -0
- timesense-1.0.0/timesense/tests/test_en_fixes.py +1116 -0
- timesense-1.0.0/timesense/tests/test_fixes.py +3776 -0
- timesense-1.0.0/timesense/tests/test_regression_fixes.py +430 -0
- timesense-1.0.0/timesense/tests/test_stress_en_500.py +635 -0
- timesense-1.0.0/timesense/tests/test_stress_ru_500.py +642 -0
- timesense-1.0.0/timesense/tests/test_suite.py +207 -0
- timesense-1.0.0/timesense.egg-info/PKG-INFO +449 -0
- timesense-1.0.0/timesense.egg-info/SOURCES.txt +67 -0
- timesense-1.0.0/timesense.egg-info/dependency_links.txt +1 -0
- timesense-1.0.0/timesense.egg-info/requires.txt +10 -0
- timesense-1.0.0/timesense.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] — 2026-07-07
|
|
9
|
+
|
|
10
|
+
Initial public release.
|
|
11
|
+
|
|
12
|
+
### Highlights
|
|
13
|
+
|
|
14
|
+
- Natural-language date/time & event parser for **Russian and English** with
|
|
15
|
+
automatic language detection.
|
|
16
|
+
- Pure Python 3.9–3.13, **no required dependencies, no ML** — dictionary and
|
|
17
|
+
regex engine: ~0.2 s cold start, a few MB of RAM, <1 ms per phrase. Built
|
|
18
|
+
for turning voice/ASR transcripts into structured events on modest hardware.
|
|
19
|
+
- Result classification: `ReminderResult` (point in time), `CalendarResult`
|
|
20
|
+
(start–end interval), `TaskResult` (period / deadline / open-start / fuzzy),
|
|
21
|
+
with `is_past`, `location`, `duration_minutes`, JSON export.
|
|
22
|
+
- Recurring events exported as iCalendar `rrule` (`FREQ`, `INTERVAL`, `BYDAY`,
|
|
23
|
+
`BYMONTHDAY`, `BYSETPOS`, `COUNT`, `UNTIL`), with `DTSTART` snapped to the
|
|
24
|
+
first valid occurrence.
|
|
25
|
+
- **iCalendar (`.ics`) export** via `result.to_ics()` and
|
|
26
|
+
`to_ics_calendar([...])` — RFC 5545 `VEVENT`s (`DTSTART`/`DTEND`/`RRULE`/
|
|
27
|
+
`SUMMARY`/`LOCATION`), all-day `VALUE=DATE` for periods and deadlines,
|
|
28
|
+
proper escaping and line folding; opens in Google Calendar, Apple Calendar
|
|
29
|
+
and Outlook.
|
|
30
|
+
- Fractional intervals: `через 2,5 часа` (decimal comma), `через два с
|
|
31
|
+
половиной часа`, `in an hour and a half`, `in two and a half hours`.
|
|
32
|
+
- Deadlines incl. named months: `до конца марта`, `by end of March`.
|
|
33
|
+
- Location extraction in both languages: `в офисе`, `at the office`.
|
|
34
|
+
- `parse_multi()` splits multi-event phrases; time-only segments inherit the
|
|
35
|
+
date of the previous event (`завтра в 10 встреча и в 15 созвон` → both on
|
|
36
|
+
tomorrow).
|
|
37
|
+
- Documented conventions, identical in both languages: smart-hour
|
|
38
|
+
(nearest-future bare hours, explicit day-part markers win), *next weekday* =
|
|
39
|
+
nearest occurrence + 7 days.
|
|
40
|
+
- Optional `WorkingCalendar` (holidays / transferred working weekends) for all
|
|
41
|
+
business-day logic; optional `pymorphy3` morphology (`timesense[morph]`).
|
|
42
|
+
- Invalid input never raises — out-of-range times (`at 25:00`), impossible
|
|
43
|
+
dates (`31 февраля`), overflow offsets and timezone-bearing phrases return
|
|
44
|
+
`None`.
|
|
45
|
+
- Test suite: 1126 cases (RU/EN stress 500 each, semantic runners, regression
|
|
46
|
+
suite), wired into CI across Python 3.9–3.13.
|
|
47
|
+
|
|
48
|
+
[1.0.0]: https://github.com/kruatech/timesense/releases/tag/v1.0.0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our
|
|
6
|
+
community a harassment-free experience for everyone, regardless of age, body
|
|
7
|
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
8
|
+
identity and expression, level of experience, education, socio-economic status,
|
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
|
10
|
+
orientation.
|
|
11
|
+
|
|
12
|
+
## Our Standards
|
|
13
|
+
|
|
14
|
+
Examples of behavior that contributes to a positive environment:
|
|
15
|
+
demonstrating empathy and kindness, being respectful of differing opinions,
|
|
16
|
+
giving and gracefully accepting constructive feedback.
|
|
17
|
+
|
|
18
|
+
Unacceptable behavior includes harassment, trolling, insulting or derogatory
|
|
19
|
+
comments, and publishing others' private information without permission.
|
|
20
|
+
|
|
21
|
+
## Enforcement
|
|
22
|
+
|
|
23
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
24
|
+
reported to the maintainer at **a@krutilin.pro**. All complaints will be
|
|
25
|
+
reviewed and investigated promptly and fairly.
|
|
26
|
+
|
|
27
|
+
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
|
|
28
|
+
version 2.1.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Вклад в TimeSense
|
|
2
|
+
|
|
3
|
+
Спасибо за интерес к проекту!
|
|
4
|
+
|
|
5
|
+
## Окружение
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/kruatech/timesense
|
|
9
|
+
cd timesense
|
|
10
|
+
pip install -e ".[dev]" # без морфологии
|
|
11
|
+
pip install -e ".[dev,morph]" # с pymorphy3 (опционально)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Тесты
|
|
15
|
+
|
|
16
|
+
Перед PR убедитесь, что все проверки зелёные:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
python -m timesense.tests.test_fixes --fail-only # RU-раннер
|
|
20
|
+
python -m timesense.tests.test_en_fixes --fail-only # EN-раннер
|
|
21
|
+
pytest -q # обёртки + EN pytest-набор
|
|
22
|
+
|
|
23
|
+
# релизные проверки (перед публикацией)
|
|
24
|
+
python -m build
|
|
25
|
+
python -m twine check dist/*
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Новые возможности и исправления багов сопровождайте кейсами: RU — в
|
|
29
|
+
`timesense/tests/test_fixes.py` (функция `build_tests`, через `add(...)`),
|
|
30
|
+
EN — в `timesense/tests/test_en_fixes.py`.
|
|
31
|
+
|
|
32
|
+
## Стиль
|
|
33
|
+
|
|
34
|
+
- Python 3.9+, без обязательных внешних зависимостей в рантайме.
|
|
35
|
+
- `black` и `flake8` для форматирования/линтинга.
|
|
36
|
+
- Морфология опциональна: код обязан работать и без `pymorphy3`.
|
|
37
|
+
|
|
38
|
+
## Процесс
|
|
39
|
+
|
|
40
|
+
1. Откройте issue для крупных изменений.
|
|
41
|
+
2. Ветка → изменения + тесты → PR.
|
|
42
|
+
3. CI должен проходить на всех поддерживаемых версиях Python.
|
timesense-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anton Krutilin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include README.ru.md
|
|
3
|
+
include LICENSE
|
|
4
|
+
include CHANGELOG.md
|
|
5
|
+
include CONTRIBUTING.md
|
|
6
|
+
include SECURITY.md
|
|
7
|
+
include CODE_OF_CONDUCT.md
|
|
8
|
+
include SUPPORT.md
|
|
9
|
+
include requirements.txt
|
|
10
|
+
include timesense/py.typed
|
|
11
|
+
recursive-include timesense *.py
|
|
12
|
+
recursive-include timesense/tests *.py
|
timesense-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: timesense
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Natural-language date/time & event parser for Russian (and English)
|
|
5
|
+
Author-email: Anton Krutilin <a@krutilin.pro>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kruatech/timesense
|
|
8
|
+
Project-URL: Source, https://github.com/kruatech/timesense
|
|
9
|
+
Project-URL: Bug Reports, https://github.com/kruatech/timesense/issues
|
|
10
|
+
Keywords: nlp,calendar,parser,russian,datetime,natural-language
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Natural Language :: Russian
|
|
22
|
+
Classifier: Natural Language :: English
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Provides-Extra: morph
|
|
27
|
+
Requires-Dist: pymorphy3>=1.2.0; extra == "morph"
|
|
28
|
+
Requires-Dist: pymorphy3-dicts-ru>=2.4.0; extra == "morph"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: flake8>=5.0.0; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# TimeSense
|
|
37
|
+
|
|
38
|
+
**Languages:** **English** · [Русский](https://github.com/kruatech/timesense/blob/main/README.ru.md)
|
|
39
|
+
|
|
40
|
+
[](https://github.com/kruatech/timesense/actions/workflows/ci.yml)
|
|
41
|
+
[](https://pypi.org/project/timesense/)
|
|
42
|
+
[](https://pypi.org/project/timesense/)
|
|
43
|
+
[](https://pypi.org/project/timesense/)
|
|
44
|
+
[](#why-timesense)
|
|
45
|
+
[](https://github.com/kruatech/timesense/blob/main/LICENSE)
|
|
46
|
+
|
|
47
|
+
Natural-language date/time & event parser for **Russian and English**.
|
|
48
|
+
|
|
49
|
+
TimeSense turns phrases like `"tomorrow at 10 meeting"`, `"from 9 to 5 work"`, or
|
|
50
|
+
`"every Monday standup"` — and their Russian equivalents — into structured event
|
|
51
|
+
objects, ready to export as JSON, an iCalendar `rrule`, or a full `.ics`
|
|
52
|
+
file. It is **pure Python**,
|
|
53
|
+
has **no required dependencies**, and is designed for turning voice/ASR
|
|
54
|
+
transcripts into structured dates.
|
|
55
|
+
|
|
56
|
+
There is **no machine learning inside** — the engine is dictionaries and
|
|
57
|
+
regular expressions. It needs no GPU, downloads no models, and runs comfortably
|
|
58
|
+
on the cheapest VPS, a Raspberry-Pi-class board, or inside a serverless
|
|
59
|
+
function: ~0.2 s cold start, a few MB of RAM, and under a millisecond per
|
|
60
|
+
phrase (~1,000+ phrases/sec on a single modest core). That makes it practical
|
|
61
|
+
to call inline on every utterance of a live transcription pipeline —
|
|
62
|
+
speech-to-text output goes in, calendar-ready events come out.
|
|
63
|
+
|
|
64
|
+
| Footprint | |
|
|
65
|
+
|---|---|
|
|
66
|
+
| Required dependencies | **0** — pure Python 3.9–3.13, stdlib only |
|
|
67
|
+
| ML models / GPU | **none** — dictionaries & regular expressions |
|
|
68
|
+
| Cold start | ~0.2 s (interpreter + import) |
|
|
69
|
+
| Memory | a few MB |
|
|
70
|
+
| Latency | **<1 ms** per phrase — 1,000+ phrases/sec on a single modest core |
|
|
71
|
+
| Deployment | cheapest VPS · Raspberry-Pi-class boards · serverless |
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from datetime import datetime
|
|
75
|
+
from timesense import TimeSenseParser
|
|
76
|
+
|
|
77
|
+
parser = TimeSenseParser()
|
|
78
|
+
r = parser.parse("tomorrow at 10 meeting", now=datetime(2026, 2, 14, 14, 0))
|
|
79
|
+
|
|
80
|
+
type(r).__name__ # 'ReminderResult'
|
|
81
|
+
r.title # 'meeting'
|
|
82
|
+
r.human_readable() # '15.02.2026 10:00'
|
|
83
|
+
r.to_dict()
|
|
84
|
+
# {'type': 'reminder', 'title': 'meeting', 'source': 'tomorrow at 10 meeting',
|
|
85
|
+
# 'confidence': 0.9, 'is_past': False, 'language': 'en',
|
|
86
|
+
# 'datetime': '2026-02-15T10:00:00'}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Russian works out of the box too — the language is auto-detected:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
parser.parse("каждый понедельник в 10 планёрка").to_dict()["recurrence"]["rrule"]
|
|
93
|
+
# 'FREQ=WEEKLY;BYDAY=MO'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Why TimeSense?
|
|
97
|
+
|
|
98
|
+
| | **TimeSense** | Classic date parsers¹ | LLM / ML pipelines |
|
|
99
|
+
|---|:---:|:---:|:---:|
|
|
100
|
+
| Dates & times from free text | ✅ | ✅ | ✅ |
|
|
101
|
+
| Event classification (reminder / calendar / task) | ✅ | ❌ | 🟡 prompt-dependent |
|
|
102
|
+
| Recurrence → RFC 5545 `RRULE` | ✅ | ❌ | 🟡 |
|
|
103
|
+
| Ready-to-import `.ics` export | ✅ | ❌ | ❌ |
|
|
104
|
+
| Event title & location extraction | ✅ | ❌ | ✅ |
|
|
105
|
+
| Zero dependencies | ✅ | ❌ | ❌ |
|
|
106
|
+
| Deterministic (same input → same output) | ✅ | ✅ | ❌ |
|
|
107
|
+
| No GPU, no model downloads, fully offline | ✅ | ✅ | ❌ |
|
|
108
|
+
| Runs on Raspberry-Pi-class hardware | ✅ | ✅ | ❌ |
|
|
109
|
+
|
|
110
|
+
¹ e.g. `dateparser`, `parsedatetime` — great at extracting a `datetime`, but they stop there.
|
|
111
|
+
|
|
112
|
+
**Contents:**
|
|
113
|
+
[Features](#features) ·
|
|
114
|
+
[Installation](#installation) ·
|
|
115
|
+
[Quick start](#quick-start) ·
|
|
116
|
+
[Showcase](#showcase-complex-phrases) ·
|
|
117
|
+
[Result types](#result-types) ·
|
|
118
|
+
[Recurrence](#recurring-events-rrule) ·
|
|
119
|
+
[.ics export](#calendar-export-ics) ·
|
|
120
|
+
[Configuration](#configuration) ·
|
|
121
|
+
[Multiple events](#multiple-events-in-one-phrase) ·
|
|
122
|
+
[Patterns](#supported-patterns-excerpt) ·
|
|
123
|
+
[Parsing policy](#status-languages--parsing-policy) ·
|
|
124
|
+
[Development](#development--tests)
|
|
125
|
+
|
|
126
|
+
## Features
|
|
127
|
+
|
|
128
|
+
- **Times:** `at 5pm`, `9:30am`, `at 17:00`, `noon`, `half past ten`,
|
|
129
|
+
`quarter to eight` · RU: `в 15:30`, `в 10 утра`, `без пяти десять`.
|
|
130
|
+
- **Relative:** `in 2 hours`, `in an hour and a half`, `in forty five minutes`,
|
|
131
|
+
`tomorrow`, `next Friday` · RU: `через 2 часа`, `через час`, `через полтора часа`,
|
|
132
|
+
`через 2,5 часа` (decimal comma), `через два с половиной часа`, `через неделю`,
|
|
133
|
+
`5 минут назад`.
|
|
134
|
+
- **Dates:** `Feb 17`, `March 3rd`, `3/17`, ISO `2026-02-17` · RU: `10 января`,
|
|
135
|
+
`первого сентября`, `15 числа`, `в пятницу`.
|
|
136
|
+
- **Ranges → `CalendarResult`:** `from 9 to 5`, `9am-11am`, `between 10 and 12`
|
|
137
|
+
· RU: `с 9 до 10`, `с десяти до одиннадцати`, `с понедельника по среду`.
|
|
138
|
+
- **Recurrence → `rrule`:** `every day`, `every weekday`, `every 2 weeks on Monday`,
|
|
139
|
+
`every month on the 15th`, `N times` (COUNT), `until end of March` (UNTIL),
|
|
140
|
+
`except weekends` (BYDAY) · RU: `каждый понедельник`, `по будням`, `каждые 2 недели`.
|
|
141
|
+
- **Deadlines:** `by Friday`, `by 5pm`, `by end of month`, `by end of March`
|
|
142
|
+
· RU: `до пятницы`, `до конца марта`, `не позднее 20-го`, `в течение часа`.
|
|
143
|
+
- **Duration & location:** `for 2 hours`, `на 2 часа`, `at the office`, `в офисе`.
|
|
144
|
+
- **Result classification:** reminder / calendar event / task, with `is_past`,
|
|
145
|
+
`detected_language`, JSON and `rrule` export.
|
|
146
|
+
|
|
147
|
+
## Installation
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
pip install timesense
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
No dependencies — pure Python (3.9–3.13). Morphology is optional; without it the
|
|
154
|
+
parser uses built-in word-form dictionaries. For improved Russian lemmatization:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
pip install "timesense[morph]" # pulls in pymorphy3
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Quick start
|
|
161
|
+
|
|
162
|
+
`parse()` returns a single object (`ReminderResult` / `CalendarResult` /
|
|
163
|
+
`TaskResult`) or `None` when no date/time is found. Pin the reference time with
|
|
164
|
+
`now`, and force a language with `language=` (otherwise it is auto-detected):
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from datetime import datetime
|
|
168
|
+
from timesense import TimeSenseParser
|
|
169
|
+
|
|
170
|
+
parser = TimeSenseParser()
|
|
171
|
+
now = datetime(2026, 2, 14, 14, 0)
|
|
172
|
+
|
|
173
|
+
parser.parse("in forty five minutes call", now=now) # ReminderResult 14:45
|
|
174
|
+
parser.parse("from 9 to 5 work", now=now) # CalendarResult 09:00–17:00
|
|
175
|
+
parser.parse("every weekday at 9 standup", now=now) # rrule FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
|
|
176
|
+
parser.parse("в 10 мск созвон", now=now) # None (timezones unsupported)
|
|
177
|
+
parser.parse("tomorrow at 10", now=now, language="en") # explicit language
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Showcase: complex phrases
|
|
181
|
+
|
|
182
|
+
One phrase can carry a recurrence rule, a time, a location and a title at once —
|
|
183
|
+
and the result type is picked automatically. All outputs below are real
|
|
184
|
+
(`now = 2026-07-06 15:41`, a Monday):
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
# Recurrence + time + location → ReminderResult with an RRULE
|
|
188
|
+
p.parse("every second Friday of the month at 6:30pm retro in the meeting room")
|
|
189
|
+
# ReminderResult · title='retro' · location='in the meeting room'
|
|
190
|
+
# datetime=2026-07-10 18:30 · rrule='FREQ=MONTHLY;BYDAY=2FR'
|
|
191
|
+
|
|
192
|
+
# Range crossing midnight → CalendarResult
|
|
193
|
+
p.parse("next Tuesday from 23:15 to 00:45 check the maintenance window")
|
|
194
|
+
# CalendarResult · 2026-07-14 23:15 → 2026-07-15 00:45 · duration_minutes=90
|
|
195
|
+
|
|
196
|
+
# Business-day arithmetic → TaskResult (deadline)
|
|
197
|
+
p.parse("two business days before end of month remind about closing documents")
|
|
198
|
+
# TaskResult · task_type=deadline · deadline=2026-07-29 23:59
|
|
199
|
+
|
|
200
|
+
# Season → TaskResult (fuzzy period)
|
|
201
|
+
p.parse("in winter run the security audit")
|
|
202
|
+
# TaskResult · 2026-12-01 → 2027-02-28
|
|
203
|
+
|
|
204
|
+
# Mixed types in one line → parse_multi
|
|
205
|
+
p.parse_multi("today by 18 close the contract, tomorrow at 9 show the demo, "
|
|
206
|
+
"Friday at 16 collect retro notes")
|
|
207
|
+
# TaskResult(deadline 2026-07-06 18:00) · ReminderResult(2026-07-07 09:00)
|
|
208
|
+
# · ReminderResult(2026-07-10 16:00)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Result types
|
|
212
|
+
|
|
213
|
+
All results share a common interface: `.title`, `.source`, `.confidence`,
|
|
214
|
+
`.is_past`, `.location`, `.duration_minutes`, `.recurrence`, `.exclusions`,
|
|
215
|
+
`.detected_language`, `.event_type` (an `EventType` enum), plus `.to_dict()` and
|
|
216
|
+
`.human_readable()`.
|
|
217
|
+
|
|
218
|
+
### `ReminderResult` — a point in time
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
r = parser.parse("at 3:30pm sync")
|
|
222
|
+
r.datetime_at # datetime(..., 15, 30)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### `CalendarResult` — an event with start and end
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
r = parser.parse("from 10 to 11 sync")
|
|
229
|
+
r.start_at # datetime(..., 10, 0)
|
|
230
|
+
r.end_at # datetime(..., 11, 0)
|
|
231
|
+
r.duration_minutes # 60
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### `TaskResult` — a task (period / deadline / open-start / fuzzy)
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
from timesense import TaskType
|
|
238
|
+
|
|
239
|
+
r = parser.parse("by Friday submit report")
|
|
240
|
+
r.task_type # TaskType.PERIOD | DEADLINE | OPEN_START | FUZZY
|
|
241
|
+
r.deadline # deadline datetime (for TaskType.DEADLINE)
|
|
242
|
+
r.start_at, r.end_at # start/end when applicable
|
|
243
|
+
r.fuzzy # bool
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Recurring events (`rrule`)
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
r = parser.parse("every Monday standup")
|
|
250
|
+
r.recurrence.to_rrule() # 'FREQ=WEEKLY;BYDAY=MO'
|
|
251
|
+
r.to_dict()["recurrence"]
|
|
252
|
+
# {'frequency': 'WEEKLY', 'interval': 1, 'by_day': ['MO'],
|
|
253
|
+
# 'until': None, 'count': None, 'rrule': 'FREQ=WEEKLY;BYDAY=MO'}
|
|
254
|
+
|
|
255
|
+
parser.parse("every Friday until end of March sync").recurrence.to_rrule()
|
|
256
|
+
# 'FREQ=WEEKLY;BYDAY=FR;UNTIL=20260331T235900' (naive: no trailing Z)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The start date always satisfies its own rule: a `BYDAY`/`BYMONTHDAY` recurrence
|
|
260
|
+
snaps `DTSTART` to the first valid occurrence.
|
|
261
|
+
|
|
262
|
+
## Calendar export (`.ics`)
|
|
263
|
+
|
|
264
|
+
Export any result — or a whole list — to an iCalendar file that opens in
|
|
265
|
+
Google Calendar, Apple Calendar and Outlook:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
from timesense import TimeSenseParser, to_ics_calendar
|
|
269
|
+
|
|
270
|
+
p = TimeSenseParser()
|
|
271
|
+
events = [
|
|
272
|
+
p.parse("every Monday at 10 standup", language="en"),
|
|
273
|
+
p.parse("meeting Friday 18:00 to 20:00", language="en"),
|
|
274
|
+
]
|
|
275
|
+
with open("events.ics", "w", encoding="utf-8") as f:
|
|
276
|
+
f.write(to_ics_calendar(events))
|
|
277
|
+
|
|
278
|
+
# single event:
|
|
279
|
+
p.parse("tomorrow at 10 call the client", language="en").to_ics()
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
RFC 5545 `VEVENT`s carry `DTSTART`/`DTEND`/`RRULE`/`SUMMARY`/`LOCATION`.
|
|
283
|
+
Periods and deadlines are emitted as all-day (`VALUE=DATE`) events; text is
|
|
284
|
+
escaped and long lines folded per the spec.
|
|
285
|
+
|
|
286
|
+
## Configuration
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from timesense import TimeSenseParser, TimeConfig
|
|
290
|
+
|
|
291
|
+
config = TimeConfig(
|
|
292
|
+
prefer_nearest_future=True, # shift past times into the nearest future
|
|
293
|
+
default_hour_for_one=13, # "at one" → 13:00
|
|
294
|
+
default_language="auto", # "auto" | "ru" | "en"
|
|
295
|
+
timezone="Europe/Amsterdam", # stored but NOT applied — datetimes are naive
|
|
296
|
+
working_hours={"start": 9, "end": 18},
|
|
297
|
+
custom_times={"briefing": (11, 30)}, # custom word → (hour, minute)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
parser = TimeSenseParser(config)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Presets: `TimeConfig.default()` and `TimeConfig.strict()`
|
|
304
|
+
(`prefer_nearest_future=False`, `default_hour_for_one=1`). Invalid language
|
|
305
|
+
values raise `ValueError`.
|
|
306
|
+
|
|
307
|
+
### Production / working calendar
|
|
308
|
+
|
|
309
|
+
Load a working calendar so that all "business day" logic honours public holidays
|
|
310
|
+
(excluded) and transferred working weekends (included):
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
from timesense import TimeSenseParser, TimeConfig, WorkingCalendar
|
|
314
|
+
|
|
315
|
+
cal = WorkingCalendar(
|
|
316
|
+
holidays=["2026-02-26", "2026-02-27"], # non-working days
|
|
317
|
+
working_weekends=["2026-02-28"], # a Saturday declared working
|
|
318
|
+
holiday_names={"new year": "2026-01-01"},
|
|
319
|
+
)
|
|
320
|
+
parser = TimeSenseParser(TimeConfig(calendar=cal))
|
|
321
|
+
parser.parse("last business day of the month report") # → 2026-02-28 (skips the holidays)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
`WorkingCalendar` accepts `date`, `datetime` or `"YYYY-MM-DD"` strings, loads from
|
|
325
|
+
`WorkingCalendar.from_dict(...)` / `from_json(path_or_str)`, and exposes
|
|
326
|
+
`is_working_day`, `is_holiday`, `next_working_day`, `prev_working_day`,
|
|
327
|
+
`resolve_holiday`. With no calendar, a business day is simply Mon–Fri.
|
|
328
|
+
|
|
329
|
+
## Multiple events in one phrase
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
parser.parse_multi("meeting tomorrow at 10 and lunch at 12")
|
|
333
|
+
# [ReminderResult('meeting'), ReminderResult('lunch')]
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`parse_multi()` splits heuristically on `,` `;` `and` / `then` (en) and
|
|
337
|
+
`« и »` (ru). A separator inside an event title may cause a false split;
|
|
338
|
+
`between X and Y` is not split, and a decimal comma (`через 2,5 часа`) is
|
|
339
|
+
never treated as a separator.
|
|
340
|
+
|
|
341
|
+
A time-only segment **inherits the date of the previous event**:
|
|
342
|
+
`"завтра в 10 встреча и в 15 созвон"` puts both events on tomorrow. Segments
|
|
343
|
+
with their own date words (`послезавтра`, weekdays, `today`/`сегодня`, …)
|
|
344
|
+
keep their own anchor.
|
|
345
|
+
|
|
346
|
+
## Supported patterns (excerpt)
|
|
347
|
+
|
|
348
|
+
| Category | Examples (EN) |
|
|
349
|
+
|-------------|---------------|
|
|
350
|
+
| Times | `at 5pm`, `9:30am`, `at 17:00`, `noon`, `midnight` |
|
|
351
|
+
| Word times | `at ten thirty`, `half past ten`, `quarter to eight` |
|
|
352
|
+
| Relative | `in 2 hours`, `in forty five minutes`, `in 3 days` |
|
|
353
|
+
| Dates | `Feb 17`, `March 3rd`, `3/17`, `2026-02-17` |
|
|
354
|
+
| Weekdays | `next Friday`, `this Monday`, `Friday` |
|
|
355
|
+
| Ranges | `from 9 to 5`, `9am-11am`, `between 10 and 12` |
|
|
356
|
+
| Recurrence | `every day`, `every weekday`, `every 2 weeks on Monday`, `every month on the 15th` |
|
|
357
|
+
| Modifiers | `5 times` (COUNT), `until end of March` (UNTIL), `except weekends` (BYDAY) |
|
|
358
|
+
| Deadlines | `by Friday`, `by 5pm`, `by end of month` |
|
|
359
|
+
|
|
360
|
+
Russian examples are listed in the [Russian README](https://github.com/kruatech/timesense/blob/main/README.ru.md).
|
|
361
|
+
|
|
362
|
+
## Status, languages & parsing policy
|
|
363
|
+
|
|
364
|
+
- **Status:** stable (1.0.0). The public API (`parse`, `parse_multi`,
|
|
365
|
+
`TimeConfig`, result types, `detected_language`, `to_dict()`) is frozen;
|
|
366
|
+
breaking changes only in the next major (semver).
|
|
367
|
+
- **Python:** 3.9–3.13.
|
|
368
|
+
- **Languages:** Russian is the primary, most complete locale; English is a
|
|
369
|
+
dedicated locale (`timesense/locales/en.py`) covering the core grammar,
|
|
370
|
+
including spelled-out numbers (`in forty five minutes`, `at ten thirty`,
|
|
371
|
+
`half past ten`, `quarter to eight`, `from nine to five`,
|
|
372
|
+
`every forty five minutes`). The language is auto-detected
|
|
373
|
+
(`default_language="auto"`) or set explicitly via `parse(text, language=...)`;
|
|
374
|
+
the chosen language is reported in `detected_language`.
|
|
375
|
+
- **Time zones:** not supported. All `datetime`s are **naive** (no `tzinfo`).
|
|
376
|
+
`TimeConfig.timezone` is stored but never applied. Zone-bearing phrases
|
|
377
|
+
(`в 10 мск`, `UTC`, `GMT+3`) deliberately return `None`.
|
|
378
|
+
- **Smart-hour:** a bare hour after the current time is read as the nearest
|
|
379
|
+
future. Example: with `now=14:00`, `"at 10"` → **22:00** today (not 10:00
|
|
380
|
+
tomorrow); bare hours `1–7` shift to PM. Explicit minutes disable this:
|
|
381
|
+
`"at 10:00"` → 10:00 sharp (tomorrow if already past). An explicit day-part
|
|
382
|
+
marker always wins over the heuristics: `"завтра в 7 утра"` → 07:00,
|
|
383
|
+
`"завтра в 3 ночи"` → 03:00, `"tomorrow at 3am"` → 03:00. Turn smart-hour
|
|
384
|
+
off with `prefer_nearest_future=False`.
|
|
385
|
+
- **"Next" weekday:** plain weekday means the nearest upcoming one; `next` /
|
|
386
|
+
`следующий` means the nearest one **plus a week** — the same convention in
|
|
387
|
+
both languages. On Saturday, `"Friday"` → the coming Friday and
|
|
388
|
+
`"next Friday"` / `"в следующую пятницу"` → a week after it.
|
|
389
|
+
- **Reminder / Calendar / Task:** a point in time → `ReminderResult`; an interval
|
|
390
|
+
with start and end → `CalendarResult`; period / deadline / open-start / fuzzy →
|
|
391
|
+
`TaskResult`. Past events are flagged with `is_past=True`.
|
|
392
|
+
|
|
393
|
+
### Known limitations
|
|
394
|
+
|
|
395
|
+
- Time zones are not applied (naive datetimes).
|
|
396
|
+
- Spelled-out numbers are supported, including compounds and verbose forms
|
|
397
|
+
(`in forty five minutes`, `from nine to five`, `every forty five minutes`;
|
|
398
|
+
RU `в десять часов пятнадцать минут`, `час пятнадцать`, `с двух до половины
|
|
399
|
+
четвёртого`). A few rare colloquialisms (e.g. English `"ten fifteen"` with no
|
|
400
|
+
`at`/idiom) still need explicit forms.
|
|
401
|
+
- `parse_multi()` splits into multiple events only when every segment is self-contained (parses on its own); otherwise it returns a single result, so plain lists like `buy milk and bread tomorrow` are not broken apart.
|
|
402
|
+
- Holidays and typo correction are out of scope (they return `None`).
|
|
403
|
+
|
|
404
|
+
## Development & tests
|
|
405
|
+
|
|
406
|
+
Tests are **not shipped in the wheel** (`pip install timesense` does not
|
|
407
|
+
include them), but they **are** included in the source distribution. They
|
|
408
|
+
live in the repository; run them from a clone:
|
|
409
|
+
|
|
410
|
+
```bash
|
|
411
|
+
git clone https://github.com/kruatech/timesense
|
|
412
|
+
cd timesense
|
|
413
|
+
pip install -e ".[dev]"
|
|
414
|
+
|
|
415
|
+
python -m timesense.tests.test_fixes --fail-only # RU runner
|
|
416
|
+
python -m timesense.tests.test_en_fixes --fail-only # EN runner
|
|
417
|
+
pytest -q # wrappers + EN pytest suite
|
|
418
|
+
|
|
419
|
+
# release checks
|
|
420
|
+
python -m build
|
|
421
|
+
python -m twine check dist/*
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Examples run as a module from the repo root:
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
python -m examples.basic_usage
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Contributing
|
|
431
|
+
|
|
432
|
+
Pull requests are welcome. For larger changes, please open an issue first.
|
|
433
|
+
See [CONTRIBUTING.md](https://github.com/kruatech/timesense/blob/main/CONTRIBUTING.md),
|
|
434
|
+
[CODE_OF_CONDUCT.md](https://github.com/kruatech/timesense/blob/main/CODE_OF_CONDUCT.md)
|
|
435
|
+
and [SECURITY.md](https://github.com/kruatech/timesense/blob/main/SECURITY.md).
|
|
436
|
+
|
|
437
|
+
## License
|
|
438
|
+
|
|
439
|
+
MIT — see [LICENSE](https://github.com/kruatech/timesense/blob/main/LICENSE).
|
|
440
|
+
|
|
441
|
+
## Author & contact
|
|
442
|
+
|
|
443
|
+
**Anton Krutilin**
|
|
444
|
+
|
|
445
|
+
- GitHub: <https://github.com/kruatech>
|
|
446
|
+
- Telegram: [@kruatech](https://t.me/kruatech)
|
|
447
|
+
- Email: a@krutilin.pro
|
|
448
|
+
|
|
449
|
+
Bugs & feature requests: <https://github.com/kruatech/timesense/issues>
|