shifter-converter 0.0.1__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.
- shifter_converter-0.0.1/PKG-INFO +19 -0
- shifter_converter-0.0.1/README.md +10 -0
- shifter_converter-0.0.1/pyproject.toml +20 -0
- shifter_converter-0.0.1/setup.cfg +4 -0
- shifter_converter-0.0.1/src/shifter_converter/__init__.py +0 -0
- shifter_converter-0.0.1/src/shifter_converter/cli.py +59 -0
- shifter_converter-0.0.1/src/shifter_converter/converter.py +230 -0
- shifter_converter-0.0.1/src/shifter_converter.egg-info/PKG-INFO +19 -0
- shifter_converter-0.0.1/src/shifter_converter.egg-info/SOURCES.txt +11 -0
- shifter_converter-0.0.1/src/shifter_converter.egg-info/dependency_links.txt +1 -0
- shifter_converter-0.0.1/src/shifter_converter.egg-info/entry_points.txt +2 -0
- shifter_converter-0.0.1/src/shifter_converter.egg-info/requires.txt +3 -0
- shifter_converter-0.0.1/src/shifter_converter.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shifter-converter
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A program for converting .Shifter files into .ics files
|
|
5
|
+
Description-Content-Type: text/markdown
|
|
6
|
+
Requires-Dist: ics>=0.7.3
|
|
7
|
+
Requires-Dist: tzlocal>=5.3.1
|
|
8
|
+
Requires-Dist: rich>=15.0.0
|
|
9
|
+
|
|
10
|
+
# Shifter-Converter
|
|
11
|
+
|
|
12
|
+
A program to convert .Shifter files to .ics files
|
|
13
|
+
|
|
14
|
+
## AI
|
|
15
|
+
|
|
16
|
+
AI was used for the following:
|
|
17
|
+
|
|
18
|
+
- Formatting output of database commands
|
|
19
|
+
- Instructions on setting up PyPI project
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools >= 77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shifter-converter"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "A program for converting .Shifter files into .ics files"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"ics>=0.7.3",
|
|
12
|
+
"tzlocal>=5.3.1",
|
|
13
|
+
"rich>=15.0.0"
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
shifter-converter = "shifter_converter.cli:main"
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
where = ["src"]
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.prompt import Prompt
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
import sqlite3
|
|
5
|
+
from .converter import main as converter_main
|
|
6
|
+
import questionary
|
|
7
|
+
import os
|
|
8
|
+
from .converter import validate_shifter_file
|
|
9
|
+
|
|
10
|
+
cwd = os.getcwd()
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_file_path(name, folder=False):
|
|
15
|
+
return questionary.path(f"Give file path of {name}", default=cwd, only_directories=folder).ask()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def validate_file(file_path: str):
|
|
19
|
+
try:
|
|
20
|
+
db = sqlite3.connect(file_path)
|
|
21
|
+
if validate_shifter_file(db):
|
|
22
|
+
console.print("[green]File is a valid .Shifter file[/green]")
|
|
23
|
+
return db
|
|
24
|
+
else:
|
|
25
|
+
console.print("[yellow]File is a valid sqlite3 database, but doesn't contain the correct tables for a .Shifter file[/yellow]")
|
|
26
|
+
main()
|
|
27
|
+
except Exception as e:
|
|
28
|
+
console.print(f"[red]File is not a valid sqlite3 database: {e}[/red]")
|
|
29
|
+
main()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main():
|
|
33
|
+
console.print(Panel.fit("Shifter Converter", style=" bold"))
|
|
34
|
+
console.print("Main menu:")
|
|
35
|
+
console.print("[1] Quick convert (.Shifter -> .ics)")
|
|
36
|
+
console.print("[2] Validate .Shifter")
|
|
37
|
+
console.print("[3] Exit")
|
|
38
|
+
option = Prompt.ask("Select option:", choices=["1", "2", "3", "4"], default="1")
|
|
39
|
+
|
|
40
|
+
if option == "1":
|
|
41
|
+
file_path = get_file_path(".Shifter file")
|
|
42
|
+
shifter_file = validate_file(file_path)
|
|
43
|
+
output_dir = get_file_path("output directory", folder=True)
|
|
44
|
+
converter_main(shifter_file, output_dir) # pyright: ignore[reportArgumentType]
|
|
45
|
+
console.print("[green]Succesfully converted .Shifter to .ics[/green]")
|
|
46
|
+
console.print(f"[cyan]Saved as [italic]{os.path.join(output_dir, 'output_calendar.ics')}[/italic][/cyan]")
|
|
47
|
+
main()
|
|
48
|
+
elif option == "2":
|
|
49
|
+
file_path = get_file_path(".Shifter file")
|
|
50
|
+
validate_file(file_path)
|
|
51
|
+
main()
|
|
52
|
+
elif option == "3":
|
|
53
|
+
quit()
|
|
54
|
+
else:
|
|
55
|
+
print("No clue how you did it, but you selected a number outside of the normal range")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
main()
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
from sqlite3 import Connection
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
from datetime import datetime, date, time, timedelta
|
|
4
|
+
from dataclasses import dataclass, asdict
|
|
5
|
+
import ics
|
|
6
|
+
import re
|
|
7
|
+
from tzlocal import get_localzone
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
shift_lables = ["_id", "text", "alarmTime", "alarm", "alarmOneDayBefore", "color", "textColor", "sequentialCode", "textSize", "alarmTime2", "alarm2", "alarmTwoDayBefore", "sound1", "sound2", "abbreviation", "startTime1", "endTime1", "startTime2", "endTime2", "splitShift", "performAction", "hourlyEarnings", "extraHourlyEarnings", "restTime", "shiftTime", "calculatedManualShiftDuration"]
|
|
11
|
+
|
|
12
|
+
local_tz = get_localzone()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Shift():
|
|
17
|
+
_id: int
|
|
18
|
+
text: str
|
|
19
|
+
alarmTime: time
|
|
20
|
+
alarm: bool
|
|
21
|
+
alarmOneDayBefore: bool
|
|
22
|
+
color: str
|
|
23
|
+
textColor: str
|
|
24
|
+
sequentialCode: int
|
|
25
|
+
textSize: float
|
|
26
|
+
alarmTime2: time
|
|
27
|
+
alarm2: bool
|
|
28
|
+
alarmTwoDayBefore: bool
|
|
29
|
+
sound1: Optional[str]
|
|
30
|
+
sound2: Optional[str]
|
|
31
|
+
abbreviation: str
|
|
32
|
+
startTime1: time
|
|
33
|
+
endTime1: time
|
|
34
|
+
startTime2: time
|
|
35
|
+
endTime2: time
|
|
36
|
+
splitShift: bool
|
|
37
|
+
performAction: Optional[str]
|
|
38
|
+
hourlyEarnings: Optional[float]
|
|
39
|
+
extraHourlyEarnings: Optional[float]
|
|
40
|
+
restTime: Optional[float]
|
|
41
|
+
shiftTime: Optional[str]
|
|
42
|
+
calculatedManualShiftDuration: Optional[float]
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_dict(cls, shift: dict[str, Any]):
|
|
46
|
+
# Turn boolean integers into actual python booleans
|
|
47
|
+
boolean_labels = ["alarm", "alarmOneDayBefore", "alarm2", "alarmTwoDayBefore", "splitShift"]
|
|
48
|
+
for label in boolean_labels:
|
|
49
|
+
if shift.get(label, 0) == 0:
|
|
50
|
+
shift[label] = False
|
|
51
|
+
else:
|
|
52
|
+
shift[label] = True
|
|
53
|
+
|
|
54
|
+
# Turn color 32 bit integers into hex color codes
|
|
55
|
+
color_labels = ["color", "textColor"]
|
|
56
|
+
for label in color_labels:
|
|
57
|
+
color_int = shift.get(label)
|
|
58
|
+
if color_int:
|
|
59
|
+
blue = color_int & 0xff
|
|
60
|
+
green = (color_int >> 8) & 0xff
|
|
61
|
+
red = (color_int >> 16) & 0xff
|
|
62
|
+
|
|
63
|
+
color_hex = ('#%02x%02x%02x' % (red, green, blue))
|
|
64
|
+
shift[label] = color_hex
|
|
65
|
+
|
|
66
|
+
# Turn time string into datetime.time object
|
|
67
|
+
time_labels = ["alarmTime", "alarmTime2", "startTime1", "endTime1", "startTime2", "endTime2"]
|
|
68
|
+
for label in time_labels:
|
|
69
|
+
time_value = shift.get(label)
|
|
70
|
+
if time_value:
|
|
71
|
+
datetime_object = datetime.strptime(time_value, '%H:%M').time()
|
|
72
|
+
shift[label] = datetime_object
|
|
73
|
+
return cls(**shift)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class DayWithShift(Shift):
|
|
78
|
+
date: date
|
|
79
|
+
note: str | None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class Day():
|
|
84
|
+
date: date
|
|
85
|
+
shift_id: Optional[int]
|
|
86
|
+
note: Optional[str]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def validate_shifter_file(db: Connection):
|
|
90
|
+
table_names: list[tuple[str]] = fetch_tables(db)
|
|
91
|
+
|
|
92
|
+
# Check if table format is correct
|
|
93
|
+
table_name_list = [row[0] for row in table_names]
|
|
94
|
+
if ('tablaTurnos' not in table_name_list) or ('dias' not in table_name_list):
|
|
95
|
+
return False
|
|
96
|
+
else:
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def fetch_tables(database: Connection) -> list[tuple[str]]:
|
|
101
|
+
"""Find all tables in database and return them as a list"""
|
|
102
|
+
query = "SELECT name FROM sqlite_schema WHERE type = 'table'"
|
|
103
|
+
tables = database.execute(query).fetchall()
|
|
104
|
+
return tables
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def fetch_shift_types(database: Connection) -> dict[int, Shift]:
|
|
108
|
+
"""Fetch different types of shift and format them as a dict of _id and Shift object"""
|
|
109
|
+
|
|
110
|
+
# Fetch all shift types from database
|
|
111
|
+
query = "SELECT * FROM tablaTurnos"
|
|
112
|
+
sql_shifts: list[tuple[Any]] = database.execute(query).fetchall()
|
|
113
|
+
|
|
114
|
+
# Pair every shift value with a label from shift_labels
|
|
115
|
+
dict_shifts: list = [dict(zip(shift_lables, shift)) for shift in sql_shifts]
|
|
116
|
+
|
|
117
|
+
# Turn every shift into a Shift object
|
|
118
|
+
shifts: dict[int, Shift] = {}
|
|
119
|
+
for shift in dict_shifts:
|
|
120
|
+
shift_object = Shift.from_dict(shift)
|
|
121
|
+
shifts[shift_object._id] = shift_object
|
|
122
|
+
|
|
123
|
+
return shifts
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def fetch_calendar(database: Connection) -> list[Any]:
|
|
127
|
+
"""Fetch all notes from database and return them"""
|
|
128
|
+
|
|
129
|
+
# Fetch dates, shifts, and notes from table containing the dates
|
|
130
|
+
query = "SELECT fecha, turno1, notas FROM dias"
|
|
131
|
+
calendar: list[tuple[int, int | None, str | None]] = database.execute(query).fetchall()
|
|
132
|
+
|
|
133
|
+
formatted_calendar: list[Day] = []
|
|
134
|
+
|
|
135
|
+
for calendar_day in calendar:
|
|
136
|
+
# Turn SQL date into a datetime.date object
|
|
137
|
+
try:
|
|
138
|
+
insane_date_str = str(calendar_day[0]) # Format is YYYYMMDD, but the month is apperantly in integers, so january is 00, this has wasted 2 hours of my life (╯°□°)╯︵ ┻━┻
|
|
139
|
+
year = insane_date_str[:4]
|
|
140
|
+
month = int(insane_date_str[4:6]) + 1
|
|
141
|
+
month_str = f"{month:02d}"
|
|
142
|
+
day = insane_date_str[6:]
|
|
143
|
+
|
|
144
|
+
sane_date_str = f"{year}{month_str}{day}"
|
|
145
|
+
|
|
146
|
+
date_object = datetime.strptime(sane_date_str, "%Y%m%d").date()
|
|
147
|
+
except ValueError:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
# Get shift type and note of day
|
|
151
|
+
shift_type: int | None = calendar_day[1]
|
|
152
|
+
note: str | None = calendar_day[2]
|
|
153
|
+
|
|
154
|
+
# Unformated notes look like this: <p dir="ltr">TEXT_HERE</p>
|
|
155
|
+
# To parse this into normal text a regex match is used
|
|
156
|
+
if note:
|
|
157
|
+
note = re.findall(r'>(.*?)<', note)[0]
|
|
158
|
+
|
|
159
|
+
# Add formatted day to list as a Day object
|
|
160
|
+
formatted_calendar.append(Day(date_object, shift_type, note))
|
|
161
|
+
|
|
162
|
+
return formatted_calendar
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def combine_calendar_and_shifts(days: list[Day], shifts: dict[int, Shift]) -> list[Day | DayWithShift]:
|
|
166
|
+
"""Combine calendar entries with details about shifts"""
|
|
167
|
+
combined_calendar: list[Day | DayWithShift] = []
|
|
168
|
+
|
|
169
|
+
for day in days:
|
|
170
|
+
shift_id = day.shift_id
|
|
171
|
+
if shift_id:
|
|
172
|
+
shift: Shift = shifts[shift_id]
|
|
173
|
+
shift_day = DayWithShift(date=day.date, note=day.note, **asdict(shift))
|
|
174
|
+
combined_calendar.append(shift_day)
|
|
175
|
+
else:
|
|
176
|
+
combined_calendar.append(day)
|
|
177
|
+
|
|
178
|
+
return combined_calendar
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def add_ics_event(calendar: ics.Calendar, name: str, start_date: date, start_time: time = time(0, 0), end_time=None, all_day=False, abreviation=None, second_part=False):
|
|
182
|
+
event = ics.Event()
|
|
183
|
+
|
|
184
|
+
event.name = name
|
|
185
|
+
event.begin = datetime.combine(start_date, start_time).replace(tzinfo=None).replace(tzinfo=local_tz)
|
|
186
|
+
|
|
187
|
+
if all_day:
|
|
188
|
+
event.make_all_day()
|
|
189
|
+
event.uid = f"note-{start_date.strftime('%Y-%m-%d')}-{name[:10].replace(' ', '')}"
|
|
190
|
+
elif end_time:
|
|
191
|
+
end_date = start_date if not (second_part or (start_time > end_time)) else start_date + timedelta(days=1)
|
|
192
|
+
event.end = datetime.combine(end_date, end_time).replace(tzinfo=local_tz)
|
|
193
|
+
event.uid = f"{abreviation if abreviation else 'shift'}-{start_date.strftime('%Y-%m-%d')}-{start_time.strftime('%H%M')}"
|
|
194
|
+
else:
|
|
195
|
+
print("ERROR: no end time found")
|
|
196
|
+
|
|
197
|
+
calendar.events.add(event)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def shifts_to_ics(shifts: list[Day | DayWithShift], output_dir):
|
|
201
|
+
ics_calendar = ics.Calendar()
|
|
202
|
+
|
|
203
|
+
for day in shifts:
|
|
204
|
+
if isinstance(day, DayWithShift):
|
|
205
|
+
if day.startTime1 == day.endTime1:
|
|
206
|
+
add_ics_event(ics_calendar, day.text, day.date, abreviation=day.abbreviation, all_day=True)
|
|
207
|
+
else:
|
|
208
|
+
add_ics_event(ics_calendar, day.text, day.date, start_time=day.startTime1, end_time=day.endTime1, abreviation=day.abbreviation)
|
|
209
|
+
if day.splitShift:
|
|
210
|
+
add_ics_event(ics_calendar, day.text, day.date, start_time=day.startTime2, end_time=day.endTime2, abreviation=f"{day.abbreviation}-2", second_part=True)
|
|
211
|
+
if day.note:
|
|
212
|
+
add_ics_event(ics_calendar, day.note, day.date, all_day=True)
|
|
213
|
+
|
|
214
|
+
with open(os.path.join(output_dir, 'output_calendar.ics'), 'w') as f:
|
|
215
|
+
f.write(ics_calendar.serialize())
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def main(db: Connection, output_dir: str):
|
|
219
|
+
# Get information of shift types
|
|
220
|
+
shift_types: dict[int, Shift] = fetch_shift_types(db)
|
|
221
|
+
|
|
222
|
+
# Get all days and their corresponding shift_type and notes
|
|
223
|
+
calendar: list[Day] = fetch_calendar(db)
|
|
224
|
+
|
|
225
|
+
# Add shift details to the calendar items
|
|
226
|
+
combined_calendar = combine_calendar_and_shifts(calendar, shift_types)
|
|
227
|
+
|
|
228
|
+
shifts_to_ics(combined_calendar, output_dir)
|
|
229
|
+
|
|
230
|
+
db.close()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shifter-converter
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A program for converting .Shifter files into .ics files
|
|
5
|
+
Description-Content-Type: text/markdown
|
|
6
|
+
Requires-Dist: ics>=0.7.3
|
|
7
|
+
Requires-Dist: tzlocal>=5.3.1
|
|
8
|
+
Requires-Dist: rich>=15.0.0
|
|
9
|
+
|
|
10
|
+
# Shifter-Converter
|
|
11
|
+
|
|
12
|
+
A program to convert .Shifter files to .ics files
|
|
13
|
+
|
|
14
|
+
## AI
|
|
15
|
+
|
|
16
|
+
AI was used for the following:
|
|
17
|
+
|
|
18
|
+
- Formatting output of database commands
|
|
19
|
+
- Instructions on setting up PyPI project
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/shifter_converter/__init__.py
|
|
4
|
+
src/shifter_converter/cli.py
|
|
5
|
+
src/shifter_converter/converter.py
|
|
6
|
+
src/shifter_converter.egg-info/PKG-INFO
|
|
7
|
+
src/shifter_converter.egg-info/SOURCES.txt
|
|
8
|
+
src/shifter_converter.egg-info/dependency_links.txt
|
|
9
|
+
src/shifter_converter.egg-info/entry_points.txt
|
|
10
|
+
src/shifter_converter.egg-info/requires.txt
|
|
11
|
+
src/shifter_converter.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shifter_converter
|