frist 0.7.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.
Potentially problematic release.
This version of frist might be problematic. Click here for more details.
- frist-0.7.0/PKG-INFO +5 -0
- frist-0.7.0/README.md +213 -0
- frist-0.7.0/pyproject.toml +14 -0
- frist-0.7.0/setup.cfg +4 -0
- frist-0.7.0/src/frist/__init__.py +26 -0
- frist-0.7.0/src/frist/_age.py +132 -0
- frist-0.7.0/src/frist/_cal.py +480 -0
- frist-0.7.0/src/frist/_constants.py +50 -0
- frist-0.7.0/src/frist/_frist.py +176 -0
- frist-0.7.0/src/frist/py.typed +1 -0
- frist-0.7.0/src/frist.egg-info/PKG-INFO +5 -0
- frist-0.7.0/src/frist.egg-info/SOURCES.txt +21 -0
- frist-0.7.0/src/frist.egg-info/dependency_links.txt +1 -0
- frist-0.7.0/src/frist.egg-info/top_level.txt +1 -0
- frist-0.7.0/test/test__age.py +179 -0
- frist-0.7.0/test/test_cal.py +649 -0
- frist-0.7.0/test/test_cal_ago.py +0 -0
- frist-0.7.0/test/test_cal_fiscal_year.py +46 -0
- frist-0.7.0/test/test_cal_interval_boundaries.py +162 -0
- frist-0.7.0/test/test_core.py +175 -0
- frist-0.7.0/test/test_edge_cases.py +169 -0
- frist-0.7.0/test/test_fiscal.py +43 -0
- frist-0.7.0/test/test_holiday.py +33 -0
frist-0.7.0/PKG-INFO
ADDED
frist-0.7.0/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# Frist
|
|
4
|
+
|
|
5
|
+
Frist is a property-based date and time utility for Python, designed to make relative date calculations and calendar logic simple and intuitive—without manual math. You create a Frist object with a target time (when something happened) and a reference time (usually "now").
|
|
6
|
+
|
|
7
|
+
Frist lets you answer questions like "Did this event happen today, this month, or this year?" using properties such as `in_day`, `in_month`, and `in_year`. These properties check if the target time lands anywhere in the current time unit window—even exactly at the start or end. You can also use ranges, like `in_days(-7, 0)` for "in the last 7 days" or `in_months(-1, 1)` for "from last month to next month." This is different from `.age.days`, which gives you the precise floating-point age in days between the target and reference times.
|
|
8
|
+
|
|
9
|
+
With Frist, you get instant answers to time-based questions with a single property, instead of writing complex date math. This makes it easy to work with calendar windows, fiscal periods, and custom time ranges.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
**Note:** In German, "Frist" means "deadline" or "time limit." This reflects the package's focus on time windows, periods, and calendar logic.
|
|
14
|
+
|
|
15
|
+
## Key Concepts
|
|
16
|
+
|
|
17
|
+
- **Property-based API:** Most calculations are exposed as properties (e.g., `.age.days`, `.fiscal_year`, `.holiday`), not methods, so you rarely need to call functions or do arithmetic.
|
|
18
|
+
- **Minimal math required:** You can answer most date and calendar questions by accessing properties, not by writing formulas.
|
|
19
|
+
- **Flexible reference time:** Zeit lets you compare any target date/time to any reference date/time, not just "now".
|
|
20
|
+
- **Calendar and fiscal logic:** Built-in support for calendar windows (days, weeks, months, quarters, years) and fiscal year/quarter calculations.
|
|
21
|
+
- **Holiday detection:** Pass a set of holiday dates and instantly check if a date is a holiday.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import datetime as dt
|
|
28
|
+
from frist import Frist
|
|
29
|
+
|
|
30
|
+
# Create a Frist object for a target date
|
|
31
|
+
meeting = Frist(target_time=dt.datetime(2025, 12, 25))
|
|
32
|
+
|
|
33
|
+
# Check age properties
|
|
34
|
+
print(meeting.age.days) # Days since meeting
|
|
35
|
+
print(meeting.age.hours) # Hours since meeting
|
|
36
|
+
|
|
37
|
+
# Calendar windows
|
|
38
|
+
if meeting.cal.in_days(0):
|
|
39
|
+
print("Meeting is today!")
|
|
40
|
+
if meeting.cal.in_weeks(-1):
|
|
41
|
+
print("Meeting was last week.")
|
|
42
|
+
|
|
43
|
+
# Fiscal year and quarter
|
|
44
|
+
print(meeting.fiscal_year) # Fiscal year for the meeting
|
|
45
|
+
print(meeting.fiscal_quarter) # Fiscal quarter for the meeting
|
|
46
|
+
|
|
47
|
+
# Holiday detection
|
|
48
|
+
holidays = {'2025-12-25', '2025-01-01'}
|
|
49
|
+
meeting = Frist(target_time=dt.datetime(2025, 12, 25), holidays=holidays)
|
|
50
|
+
if meeting.holiday:
|
|
51
|
+
print("This date is a holiday!")
|
|
52
|
+
|
|
53
|
+
# Compare to a custom reference time
|
|
54
|
+
project = Frist(target_time=dt.datetime(2025, 1, 1), reference_time=dt.datetime(2025, 2, 1))
|
|
55
|
+
print(project.age.days) # Days between Jan 1 and Feb 1, 2025
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Features
|
|
59
|
+
|
|
60
|
+
## Time Scale Properties Table
|
|
61
|
+
|
|
62
|
+
| Time Scale | Age Property | Window Function(s) |
|
|
63
|
+
|-------------- |----------------------|---------------------------------------|
|
|
64
|
+
| Seconds | `.age.seconds` | `.cal.in_seconds(start, end)` |
|
|
65
|
+
| Minutes | `.age.minutes` | `.cal.in_minutes(start, end)` |
|
|
66
|
+
| Hours | `.age.hours` | `.cal.in_hours(start, end)` |
|
|
67
|
+
| Days | `.age.days` | `.cal.in_days(start, end)` |
|
|
68
|
+
| Weeks | `.age.weeks` | `.cal.in_weeks(start, end)` |
|
|
69
|
+
| Months | `.age.months` | `.cal.in_months(start, end)` |
|
|
70
|
+
| Quarters | `.age.quarters` | `.cal.in_quarters(start, end)` |
|
|
71
|
+
| Years | `.age.years` | `.cal.in_years(start, end)` |
|
|
72
|
+
| Fiscal Years | `.age.fiscal_year` | `.cal.in_fiscal_years(start, end)` |
|
|
73
|
+
| Fiscal Qtrs | `.age.fiscal_quarter`| `.cal.in_fiscal_quarters(start, end)` |
|
|
74
|
+
|
|
75
|
+
Each age property gives the precise floating-point difference in that unit. Each window function generates a boolean if the target time falls within the specified range of time units relative to the reference time.
|
|
76
|
+
Age properties (like `.age.days`) are designed for direct comparisons—use them to ask questions like `>`, `<`, `==`, or `!=` between the target and reference times. In contrast, the `in_*` window functions (like `.cal.in_days()`) return `True` or `False` depending on whether the target time falls within the specified range.
|
|
77
|
+
Note: The `end` parameter is optional. If omitted, the function checks for a single time unit (e.g., `.cal.in_days(0)` means "today").
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## Fiscal Year & Quarter Example
|
|
81
|
+
|
|
82
|
+
Frist supports fiscal year and quarter calculations with customizable fiscal year start months. For example:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
# Fiscal year starts in April (fy_start_month=4)
|
|
86
|
+
meeting = Frist(target_time=dt.datetime(2025, 7, 15), fy_start_month=4)
|
|
87
|
+
print(meeting.fiscal_year) # 2025 (fiscal year for July 15, 2025)
|
|
88
|
+
print(meeting.fiscal_quarter) # 2 (Q2: July–September for April start)
|
|
89
|
+
|
|
90
|
+
# Check if a date is in a fiscal quarter or year window
|
|
91
|
+
if meeting.cal.in_fiscal_quarters(0):
|
|
92
|
+
print("Meeting is in the current fiscal quarter.")
|
|
93
|
+
if meeting.cal.in_fiscal_years(0):
|
|
94
|
+
print("Meeting is in the current fiscal year.")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
## Holiday Detection Example
|
|
99
|
+
|
|
100
|
+
Frist can instantly check if a date is a holiday using a set of holiday dates:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
holidays = {
|
|
104
|
+
'2025-12-25', # Christmas
|
|
105
|
+
'2025-01-01', # New Year's Day
|
|
106
|
+
'2025-07-04', # Independence Day
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Check a specific date
|
|
110
|
+
meeting = Frist(target_time=dt.datetime(2025, 12, 25), holidays=holidays)
|
|
111
|
+
if meeting.holiday:
|
|
112
|
+
print("Meeting date is a holiday!")
|
|
113
|
+
|
|
114
|
+
# Check multiple dates
|
|
115
|
+
for date_str in holidays:
|
|
116
|
+
date = dt.datetime.strptime(date_str, '%Y-%m-%d')
|
|
117
|
+
c = Frist(target_time=date, holidays=holidays)
|
|
118
|
+
print(f"{date.date()}: Holiday? {c.holiday}")
|
|
119
|
+
|
|
120
|
+
# Use with custom reference time
|
|
121
|
+
project = Frist(target_time=dt.datetime(2025, 7, 4), reference_time=dt.datetime(2025, 7, 5), holidays=holidays)
|
|
122
|
+
if project.holiday:
|
|
123
|
+
print("Project start date is a holiday!")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Short Examples
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
### Age Calculation
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
person = Frist(target_time=dt.datetime(1990, 5, 1), reference_time=dt.datetime(2025, 5, 1))
|
|
133
|
+
print(f"Age in days: {person.age.days}, Age in years: {person.age.years:.2f}")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
### Calendar Windows
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
meeting = Frist(target_time=dt.datetime(2025, 12, 25))
|
|
141
|
+
if meeting.cal.in_days(0):
|
|
142
|
+
print("Meeting is today!")
|
|
143
|
+
if meeting.cal.in_weeks(-1):
|
|
144
|
+
print("Meeting was last week.")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## API Reference
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
### Frist
|
|
151
|
+
|
|
152
|
+
`Frist(target_time: datetime, reference_time: datetime = None, fy_start_month: int = 1, holidays: set[str] = None)`
|
|
153
|
+
|
|
154
|
+
- **Properties:**
|
|
155
|
+
- `age`: Age object with properties for `.days`, `.hours`, `.minutes`, `.seconds`, `.weeks`, `.months`, `.quarters`, `.years`, `.fiscal_year`, `.fiscal_quarter`.
|
|
156
|
+
- `cal`: Cal object for calendar window logic.
|
|
157
|
+
- `fiscal_year`: Fiscal year for the target time.
|
|
158
|
+
- `fiscal_quarter`: Fiscal quarter for the target time.
|
|
159
|
+
- `holiday`: True if target time is a holiday (if holidays set provided).
|
|
160
|
+
|
|
161
|
+
### Cal
|
|
162
|
+
|
|
163
|
+
`Cal(time_span: TimeSpan, fy_start_month: int = 1, holidays: set[str] = None)`
|
|
164
|
+
|
|
165
|
+
- **Properties:**
|
|
166
|
+
- `dt_val`: Target datetime.
|
|
167
|
+
- `base_time`: Reference datetime.
|
|
168
|
+
- `fiscal_year`: Fiscal year for `dt_val`.
|
|
169
|
+
- `fiscal_quarter`: Fiscal quarter for `dt_val`.
|
|
170
|
+
- `holiday`: True if `dt_val` is a holiday.
|
|
171
|
+
- `time_span`: The TimeSpan object.
|
|
172
|
+
|
|
173
|
+
- **Interval Methods:**
|
|
174
|
+
- `in_minutes(start: int = 0, end: int | None = None) -> bool`
|
|
175
|
+
- `in_hours(start: int = 0, end: int | None = None) -> bool`
|
|
176
|
+
- `in_days(start: int = 0, end: int | None = None) -> bool`
|
|
177
|
+
- `in_weeks(start: int = 0, end: int | None = None, week_start: str = "monday") -> bool`
|
|
178
|
+
- `in_months(start: int = 0, end: int | None = None) -> bool`
|
|
179
|
+
- `in_quarters(start: int = 0, end: int | None = None) -> bool`
|
|
180
|
+
- `in_years(start: int = 0, end: int | None = None) -> bool`
|
|
181
|
+
- `in_fiscal_quarters(start: int = 0, end: int | None = None) -> bool`
|
|
182
|
+
- `in_fiscal_years(start: int = 0, end: int | None = None) -> bool`
|
|
183
|
+
|
|
184
|
+
- **Static Methods:**
|
|
185
|
+
- `get_fiscal_year(dt: datetime, fy_start_month: int) -> int`
|
|
186
|
+
- `get_fiscal_quarter(dt: datetime, fy_start_month: int) -> int`
|
|
187
|
+
|
|
188
|
+
- **Exceptions:**
|
|
189
|
+
- All interval methods raise `ValueError` if `start > end`.
|
|
190
|
+
- `normalize_weekday(day_spec: str) -> int` raises `ValueError` for invalid day specifications, with detailed error messages.
|
|
191
|
+
|
|
192
|
+
### Age
|
|
193
|
+
|
|
194
|
+
`Age(target_time: datetime, reference_time: datetime)`
|
|
195
|
+
|
|
196
|
+
- **Properties:**
|
|
197
|
+
- `days`, `hours`, `minutes`, `seconds`, `weeks`, `months`, `quarters`, `years`, `fiscal_year`, `fiscal_quarter`
|
|
198
|
+
|
|
199
|
+
### TimeSpan (Protocol)
|
|
200
|
+
|
|
201
|
+
`TimeSpan`
|
|
202
|
+
|
|
203
|
+
- **Properties:**
|
|
204
|
+
- `target_dt`: Target datetime.
|
|
205
|
+
- `ref_dt`: Reference datetime.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Testing and Support
|
|
210
|
+
|
|
211
|
+
[](https://www.python.org/)
|
|
212
|
+
[](https://github.com/hucker/zeit/actions)
|
|
213
|
+
[](https://github.com/charliermarsh/ruff)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
[build-system]
|
|
3
|
+
requires = ["setuptools", "wheel"]
|
|
4
|
+
build-backend = "setuptools.build_meta"
|
|
5
|
+
|
|
6
|
+
[project]
|
|
7
|
+
name = "frist"
|
|
8
|
+
version = "0.7.0"
|
|
9
|
+
description = "Test Package"
|
|
10
|
+
authors = [{name = "Chuck Bass"}]
|
|
11
|
+
|
|
12
|
+
[tool.setuptools]
|
|
13
|
+
packages = ["frist"]
|
|
14
|
+
package-dir = {"" = "src"}
|
frist-0.7.0/setup.cfg
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Frist: Standalone datetime utility package
|
|
3
|
+
|
|
4
|
+
Provides robust tools for:
|
|
5
|
+
- Age and duration calculations across multiple time units
|
|
6
|
+
- Calendar window filtering (days, weeks, months, quarters, years)
|
|
7
|
+
- Fiscal year/quarter logic and holiday detection
|
|
8
|
+
- Flexible datetime parsing and normalization
|
|
9
|
+
|
|
10
|
+
Designed for use in any Python project requiring advanced datetime analysis, not limited to file operations.
|
|
11
|
+
|
|
12
|
+
Exports:
|
|
13
|
+
Frist -- Main datetime utility class
|
|
14
|
+
Age -- Duration and age calculations
|
|
15
|
+
Cal -- Calendar window and filtering logic
|
|
16
|
+
TimeSpan -- Time span representation for advanced calculations
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from ._age import Age
|
|
20
|
+
from ._cal import Cal, TimeSpan
|
|
21
|
+
from ._frist import Frist
|
|
22
|
+
|
|
23
|
+
__version__ = "0.7.0"
|
|
24
|
+
__author__ = "Chuck Bass"
|
|
25
|
+
|
|
26
|
+
__all__ = ["Frist", "Age", "Cal", "TimeSpan"]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Age property implementation for Frist package.
|
|
3
|
+
|
|
4
|
+
Handles age calculations in various time units, supporting both file-based and standalone usage.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import datetime as dt
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from ._constants import (
|
|
12
|
+
DAYS_PER_MONTH,
|
|
13
|
+
DAYS_PER_YEAR,
|
|
14
|
+
SECONDS_PER_DAY,
|
|
15
|
+
SECONDS_PER_HOUR,
|
|
16
|
+
SECONDS_PER_MINUTE,
|
|
17
|
+
SECONDS_PER_MONTH,
|
|
18
|
+
SECONDS_PER_WEEK,
|
|
19
|
+
SECONDS_PER_YEAR,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Age:
|
|
24
|
+
"""Property class for handling age calculations in various time units."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, path: Path | None, timestamp: float, base_time: dt.datetime):
|
|
27
|
+
self.path = path
|
|
28
|
+
self.timestamp = timestamp
|
|
29
|
+
self.base_time = base_time
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def seconds(self) -> float:
|
|
33
|
+
"""Get age in seconds."""
|
|
34
|
+
# Only check file existence if we have a path
|
|
35
|
+
if self.path is not None and not self.path.exists():
|
|
36
|
+
return 0
|
|
37
|
+
file_time = dt.datetime.fromtimestamp(self.timestamp)
|
|
38
|
+
return (self.base_time - file_time).total_seconds()
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def minutes(self) -> float:
|
|
42
|
+
"""Get age in minutes."""
|
|
43
|
+
return self.seconds / SECONDS_PER_MINUTE
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def hours(self) -> float:
|
|
47
|
+
"""Get age in hours."""
|
|
48
|
+
return self.seconds / SECONDS_PER_HOUR
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def days(self) -> float:
|
|
52
|
+
"""Get age in days."""
|
|
53
|
+
return self.seconds / SECONDS_PER_DAY
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def weeks(self) -> float:
|
|
57
|
+
"""Get age in weeks."""
|
|
58
|
+
return self.days / 7
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def months(self) -> float:
|
|
62
|
+
"""Get age in months (approximate - 30.44 days)."""
|
|
63
|
+
return self.days / DAYS_PER_MONTH
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def years(self) -> float:
|
|
67
|
+
"""Get age in years (approximate - 365.25 days, can be negative)."""
|
|
68
|
+
# Allow negative ages if base_time is before timestamp
|
|
69
|
+
return self.days / DAYS_PER_YEAR
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def parse(age_str: str) -> float:
|
|
73
|
+
"""
|
|
74
|
+
Parse an age string and return the age in seconds.
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
"30" -> 30 seconds
|
|
78
|
+
"5m" -> 300 seconds (5 minutes)
|
|
79
|
+
"2h" -> 7200 seconds (2 hours)
|
|
80
|
+
"3d" -> 259200 seconds (3 days)
|
|
81
|
+
"1w" -> 604800 seconds (1 week)
|
|
82
|
+
"2months" -> 5260032 seconds (2 months)
|
|
83
|
+
"1y" -> 31557600 seconds (1 year)
|
|
84
|
+
"""
|
|
85
|
+
age_str = age_str.strip().lower()
|
|
86
|
+
|
|
87
|
+
# Handle plain numbers (seconds)
|
|
88
|
+
if age_str.isdigit():
|
|
89
|
+
return float(age_str)
|
|
90
|
+
|
|
91
|
+
# Regular expression to parse age with unit
|
|
92
|
+
match = re.match(r"^(\d+(?:\.\d+)?)\s*([a-zA-Z]+)$", age_str)
|
|
93
|
+
if not match:
|
|
94
|
+
raise ValueError(f"Invalid age format: {age_str}")
|
|
95
|
+
|
|
96
|
+
value: float = float(match.group(1))
|
|
97
|
+
unit: str = match.group(2).lower()
|
|
98
|
+
|
|
99
|
+
# Define multipliers (convert to seconds)
|
|
100
|
+
unit_multipliers = {
|
|
101
|
+
"s": 1,
|
|
102
|
+
"sec": 1,
|
|
103
|
+
"second": 1,
|
|
104
|
+
"seconds": 1,
|
|
105
|
+
"m": SECONDS_PER_MINUTE,
|
|
106
|
+
"min": SECONDS_PER_MINUTE,
|
|
107
|
+
"minute": SECONDS_PER_MINUTE,
|
|
108
|
+
"minutes": SECONDS_PER_MINUTE,
|
|
109
|
+
"h": SECONDS_PER_HOUR,
|
|
110
|
+
"hr": SECONDS_PER_HOUR,
|
|
111
|
+
"hour": SECONDS_PER_HOUR,
|
|
112
|
+
"hours": SECONDS_PER_HOUR,
|
|
113
|
+
"d": SECONDS_PER_DAY,
|
|
114
|
+
"day": SECONDS_PER_DAY,
|
|
115
|
+
"days": SECONDS_PER_DAY,
|
|
116
|
+
"w": SECONDS_PER_WEEK,
|
|
117
|
+
"week": SECONDS_PER_WEEK,
|
|
118
|
+
"weeks": SECONDS_PER_WEEK,
|
|
119
|
+
"month": SECONDS_PER_MONTH,
|
|
120
|
+
"months": SECONDS_PER_MONTH,
|
|
121
|
+
"y": SECONDS_PER_YEAR,
|
|
122
|
+
"year": SECONDS_PER_YEAR,
|
|
123
|
+
"years": SECONDS_PER_YEAR,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if unit not in unit_multipliers:
|
|
127
|
+
raise ValueError(f"Unknown unit: {unit}")
|
|
128
|
+
|
|
129
|
+
return value * unit_multipliers[unit]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
__all__ = ["Age"]
|