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.
Files changed (69) hide show
  1. timesense-1.0.0/CHANGELOG.md +48 -0
  2. timesense-1.0.0/CODE_OF_CONDUCT.md +28 -0
  3. timesense-1.0.0/CONTRIBUTING.md +42 -0
  4. timesense-1.0.0/LICENSE +21 -0
  5. timesense-1.0.0/MANIFEST.in +12 -0
  6. timesense-1.0.0/PKG-INFO +449 -0
  7. timesense-1.0.0/README.md +414 -0
  8. timesense-1.0.0/README.ru.md +422 -0
  9. timesense-1.0.0/SECURITY.md +19 -0
  10. timesense-1.0.0/SUPPORT.md +9 -0
  11. timesense-1.0.0/pyproject.toml +57 -0
  12. timesense-1.0.0/requirements.txt +3 -0
  13. timesense-1.0.0/setup.cfg +4 -0
  14. timesense-1.0.0/setup.py +8 -0
  15. timesense-1.0.0/timesense/__init__.py +35 -0
  16. timesense-1.0.0/timesense/core/__init__.py +1 -0
  17. timesense-1.0.0/timesense/core/calendar.py +117 -0
  18. timesense-1.0.0/timesense/core/config.py +65 -0
  19. timesense-1.0.0/timesense/core/language.py +178 -0
  20. timesense-1.0.0/timesense/core/parser.py +980 -0
  21. timesense-1.0.0/timesense/core/tokenizer.py +37 -0
  22. timesense-1.0.0/timesense/dict/__init__.py +3 -0
  23. timesense-1.0.0/timesense/dict/keywords.py +688 -0
  24. timesense-1.0.0/timesense/dict/morph_adapter.py +50 -0
  25. timesense-1.0.0/timesense/ics.py +157 -0
  26. timesense-1.0.0/timesense/locales/__init__.py +1 -0
  27. timesense-1.0.0/timesense/locales/en.py +1806 -0
  28. timesense-1.0.0/timesense/models/__init__.py +1 -0
  29. timesense-1.0.0/timesense/models/datetime_token.py +134 -0
  30. timesense-1.0.0/timesense/models/event_types.py +197 -0
  31. timesense-1.0.0/timesense/models/token.py +17 -0
  32. timesense-1.0.0/timesense/py.typed +0 -0
  33. timesense-1.0.0/timesense/recognizers/__init__.py +1 -0
  34. timesense-1.0.0/timesense/recognizers/base.py +21 -0
  35. timesense-1.0.0/timesense/recognizers/combined.py +291 -0
  36. timesense-1.0.0/timesense/recognizers/custom_time.py +29 -0
  37. timesense-1.0.0/timesense/recognizers/date.py +383 -0
  38. timesense-1.0.0/timesense/recognizers/date_format.py +66 -0
  39. timesense-1.0.0/timesense/recognizers/deadline_synonym.py +152 -0
  40. timesense-1.0.0/timesense/recognizers/duration.py +94 -0
  41. timesense-1.0.0/timesense/recognizers/exclusion.py +84 -0
  42. timesense-1.0.0/timesense/recognizers/location.py +37 -0
  43. timesense-1.0.0/timesense/recognizers/open_start.py +104 -0
  44. timesense-1.0.0/timesense/recognizers/ordinal_day.py +618 -0
  45. timesense-1.0.0/timesense/recognizers/period.py +475 -0
  46. timesense-1.0.0/timesense/recognizers/range.py +578 -0
  47. timesense-1.0.0/timesense/recognizers/recurrence.py +681 -0
  48. timesense-1.0.0/timesense/recognizers/relative.py +429 -0
  49. timesense-1.0.0/timesense/recognizers/season.py +121 -0
  50. timesense-1.0.0/timesense/recognizers/time.py +648 -0
  51. timesense-1.0.0/timesense/recognizers/time_of_day.py +62 -0
  52. timesense-1.0.0/timesense/recognizers/weekday_modifier.py +157 -0
  53. timesense-1.0.0/timesense/recognizers/working_context.py +83 -0
  54. timesense-1.0.0/timesense/recognizers/working_days.py +123 -0
  55. timesense-1.0.0/timesense/tests/__init__.py +0 -0
  56. timesense-1.0.0/timesense/tests/_pretty.py +247 -0
  57. timesense-1.0.0/timesense/tests/interactive.py +268 -0
  58. timesense-1.0.0/timesense/tests/test_en.py +263 -0
  59. timesense-1.0.0/timesense/tests/test_en_fixes.py +1116 -0
  60. timesense-1.0.0/timesense/tests/test_fixes.py +3776 -0
  61. timesense-1.0.0/timesense/tests/test_regression_fixes.py +430 -0
  62. timesense-1.0.0/timesense/tests/test_stress_en_500.py +635 -0
  63. timesense-1.0.0/timesense/tests/test_stress_ru_500.py +642 -0
  64. timesense-1.0.0/timesense/tests/test_suite.py +207 -0
  65. timesense-1.0.0/timesense.egg-info/PKG-INFO +449 -0
  66. timesense-1.0.0/timesense.egg-info/SOURCES.txt +67 -0
  67. timesense-1.0.0/timesense.egg-info/dependency_links.txt +1 -0
  68. timesense-1.0.0/timesense.egg-info/requires.txt +10 -0
  69. 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.
@@ -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
@@ -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
+ [![CI](https://github.com/kruatech/timesense/actions/workflows/ci.yml/badge.svg)](https://github.com/kruatech/timesense/actions/workflows/ci.yml)
41
+ [![PyPI](https://img.shields.io/pypi/v/timesense.svg)](https://pypi.org/project/timesense/)
42
+ [![Python](https://img.shields.io/pypi/pyversions/timesense.svg)](https://pypi.org/project/timesense/)
43
+ [![Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](https://pypi.org/project/timesense/)
44
+ [![No ML](https://img.shields.io/badge/ML%20models-none-blue)](#why-timesense)
45
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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>