pttm 0.1.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.
- pttm-0.1.0/PKG-INFO +96 -0
- pttm-0.1.0/README.md +79 -0
- pttm-0.1.0/pttm/__init__.py +3 -0
- pttm-0.1.0/pttm/app.py +183 -0
- pttm-0.1.0/pttm/clock.py +28 -0
- pttm-0.1.0/pttm/config.py +53 -0
- pttm-0.1.0/pttm/pttm.css +483 -0
- pttm-0.1.0/pttm/widgets/__init__.py +1 -0
- pttm-0.1.0/pttm/widgets/dashboard.py +9 -0
- pttm-0.1.0/pttm/widgets/settings_tab.py +65 -0
- pttm-0.1.0/pttm/widgets/shortcuts_screen.py +40 -0
- pttm-0.1.0/pttm/widgets/task_item.py +109 -0
- pttm-0.1.0/pttm/widgets/task_list_widget.py +104 -0
- pttm-0.1.0/pttm/widgets/timer_widget.py +109 -0
- pttm-0.1.0/pttm.egg-info/PKG-INFO +96 -0
- pttm-0.1.0/pttm.egg-info/SOURCES.txt +20 -0
- pttm-0.1.0/pttm.egg-info/dependency_links.txt +1 -0
- pttm-0.1.0/pttm.egg-info/entry_points.txt +2 -0
- pttm-0.1.0/pttm.egg-info/requires.txt +10 -0
- pttm-0.1.0/pttm.egg-info/top_level.txt +1 -0
- pttm-0.1.0/pyproject.toml +31 -0
- pttm-0.1.0/setup.cfg +4 -0
pttm-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pttm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pomodoro Terminal Timer & Manager
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: linkify-it-py==2.1.0
|
|
8
|
+
Requires-Dist: markdown-it-py==4.2.0
|
|
9
|
+
Requires-Dist: mdit-py-plugins==0.6.1
|
|
10
|
+
Requires-Dist: mdurl==0.1.2
|
|
11
|
+
Requires-Dist: platformdirs==4.10.0
|
|
12
|
+
Requires-Dist: Pygments==2.20.0
|
|
13
|
+
Requires-Dist: rich==15.0.0
|
|
14
|
+
Requires-Dist: textual==8.2.7
|
|
15
|
+
Requires-Dist: typing_extensions==4.15.0
|
|
16
|
+
Requires-Dist: uc-micro-py==2.0.0
|
|
17
|
+
|
|
18
|
+
# PTTM - Pomodoro Terminal Timer & Manager
|
|
19
|
+
|
|
20
|
+
PTTM is a terminal-based Pomodoro app built with [Textual](https://textual.textualize.io/). It provides a timer, task tracking, and configurable work/break settings in a compact TUI.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- Pomodoro timer with focus, short break, and long break modes
|
|
25
|
+
- Task list with per-task Pomodoro counts
|
|
26
|
+
- Persistent configuration stored as JSON
|
|
27
|
+
- Keyboard shortcuts for common timer and task actions
|
|
28
|
+
- In-app settings tab for adjusting timing values
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Python 3.10 or newer
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
Install the dependencies from the project root:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install -r requirements.txt
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Run
|
|
43
|
+
|
|
44
|
+
From the repository root, start the app with:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
python pttm.py
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
You can also run the app module directly:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
python -m pttm.app
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Keyboard Shortcuts
|
|
57
|
+
|
|
58
|
+
- `q` quit
|
|
59
|
+
- `s` start or pause the timer
|
|
60
|
+
- `r` reset the current timer
|
|
61
|
+
- `ctrl+r` reset the full session
|
|
62
|
+
- `k` skip to the next timer mode
|
|
63
|
+
- `f` switch to focus mode
|
|
64
|
+
- `g` switch to short break mode
|
|
65
|
+
- `b` switch to long break mode
|
|
66
|
+
- `t` focus the new task input
|
|
67
|
+
- `ctrl+p` show or hide the shortcuts screen
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
The app reads and writes a JSON config file. By default, the file is stored in your user config directory. You can override the location by setting `PMO_CONFIG_PATH` before launch.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
export PMO_CONFIG_PATH=./pmo_config.json
|
|
77
|
+
python pttm.py
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The config includes timer settings, completed focus session count, and task data.
|
|
81
|
+
|
|
82
|
+
## Tests
|
|
83
|
+
|
|
84
|
+
Run the test suite with:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
python -m unittest test_pttm.py
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Project Layout
|
|
91
|
+
|
|
92
|
+
- `pttm/app.py` application entry point
|
|
93
|
+
- `pttm/config.py` config load/save helpers
|
|
94
|
+
- `pttm/clock.py` ASCII clock rendering
|
|
95
|
+
- `pttm/widgets/` UI components
|
|
96
|
+
- `pttm/pttm.css` Textual stylesheet
|
pttm-0.1.0/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# PTTM - Pomodoro Terminal Timer & Manager
|
|
2
|
+
|
|
3
|
+
PTTM is a terminal-based Pomodoro app built with [Textual](https://textual.textualize.io/). It provides a timer, task tracking, and configurable work/break settings in a compact TUI.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Pomodoro timer with focus, short break, and long break modes
|
|
8
|
+
- Task list with per-task Pomodoro counts
|
|
9
|
+
- Persistent configuration stored as JSON
|
|
10
|
+
- Keyboard shortcuts for common timer and task actions
|
|
11
|
+
- In-app settings tab for adjusting timing values
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Python 3.10 or newer
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Install the dependencies from the project root:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install -r requirements.txt
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Run
|
|
26
|
+
|
|
27
|
+
From the repository root, start the app with:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python pttm.py
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
You can also run the app module directly:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python -m pttm.app
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Keyboard Shortcuts
|
|
40
|
+
|
|
41
|
+
- `q` quit
|
|
42
|
+
- `s` start or pause the timer
|
|
43
|
+
- `r` reset the current timer
|
|
44
|
+
- `ctrl+r` reset the full session
|
|
45
|
+
- `k` skip to the next timer mode
|
|
46
|
+
- `f` switch to focus mode
|
|
47
|
+
- `g` switch to short break mode
|
|
48
|
+
- `b` switch to long break mode
|
|
49
|
+
- `t` focus the new task input
|
|
50
|
+
- `ctrl+p` show or hide the shortcuts screen
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
The app reads and writes a JSON config file. By default, the file is stored in your user config directory. You can override the location by setting `PMO_CONFIG_PATH` before launch.
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
export PMO_CONFIG_PATH=./pmo_config.json
|
|
60
|
+
python pttm.py
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The config includes timer settings, completed focus session count, and task data.
|
|
64
|
+
|
|
65
|
+
## Tests
|
|
66
|
+
|
|
67
|
+
Run the test suite with:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python -m unittest test_pttm.py
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Project Layout
|
|
74
|
+
|
|
75
|
+
- `pttm/app.py` application entry point
|
|
76
|
+
- `pttm/config.py` config load/save helpers
|
|
77
|
+
- `pttm/clock.py` ASCII clock rendering
|
|
78
|
+
- `pttm/widgets/` UI components
|
|
79
|
+
- `pttm/pttm.css` Textual stylesheet
|
pttm-0.1.0/pttm/app.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from textual.app import App, ComposeResult
|
|
4
|
+
from textual.widgets import Header, TabbedContent, TabPane, Label, Input
|
|
5
|
+
from pttm.config import load_config, save_config
|
|
6
|
+
from pttm.widgets.dashboard import Dashboard
|
|
7
|
+
from pttm.widgets.settings_tab import SettingsTab
|
|
8
|
+
from pttm.widgets.shortcuts_screen import ShortcutsScreen
|
|
9
|
+
from pttm.widgets.timer_widget import TimerWidget
|
|
10
|
+
from pttm.widgets.task_list_widget import TaskListWidget
|
|
11
|
+
|
|
12
|
+
class PomodoroApp(App):
|
|
13
|
+
CSS_PATH = "pttm.css"
|
|
14
|
+
TITLE = "TS PMO"
|
|
15
|
+
COMMANDS = set()
|
|
16
|
+
ENABLE_COMMAND_PALETTE = False
|
|
17
|
+
|
|
18
|
+
BINDINGS = [
|
|
19
|
+
("q", "quit", "Quit"),
|
|
20
|
+
("s", "toggle_timer", "Start/Pause"),
|
|
21
|
+
("r", "reset_timer", "Reset"),
|
|
22
|
+
("ctrl+r", "reset_session", "Reset Session"),
|
|
23
|
+
("k", "skip_timer", "Skip"),
|
|
24
|
+
("f", "set_mode_focus", "Focus Mode"),
|
|
25
|
+
("g", "set_mode_short", "Short Break"),
|
|
26
|
+
("b", "set_mode_long", "Long Break"),
|
|
27
|
+
("t", "focus_todo_input", "Focus Todo"),
|
|
28
|
+
("ctrl+p", "toggle_shortcuts", "Shortcuts"),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
def __init__(self, **kwargs):
|
|
32
|
+
super().__init__(**kwargs)
|
|
33
|
+
self.config = load_config()
|
|
34
|
+
# Log config to startup file next to config file
|
|
35
|
+
from pttm.config import CONFIG_FILE
|
|
36
|
+
log_dir = os.path.dirname(CONFIG_FILE)
|
|
37
|
+
if log_dir:
|
|
38
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
39
|
+
log_file = os.path.join(log_dir, "pmo_startup.log")
|
|
40
|
+
else:
|
|
41
|
+
log_file = "pmo_startup.log"
|
|
42
|
+
try:
|
|
43
|
+
with open(log_file, "w") as f:
|
|
44
|
+
f.write(json.dumps(self.config, indent=4))
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
self.active_task_id = None
|
|
48
|
+
|
|
49
|
+
def compose(self) -> ComposeResult:
|
|
50
|
+
# yield Header(show_clock=True)
|
|
51
|
+
with TabbedContent(initial="timer-tab"):
|
|
52
|
+
with TabPane("Timer", id="timer-tab"):
|
|
53
|
+
yield TimerWidget()
|
|
54
|
+
with TabPane("Tasks", id="tasks-tab"):
|
|
55
|
+
yield TaskListWidget()
|
|
56
|
+
with TabPane("Settings", id="settings-tab"):
|
|
57
|
+
yield SettingsTab()
|
|
58
|
+
|
|
59
|
+
def action_toggle_timer(self) -> None:
|
|
60
|
+
try:
|
|
61
|
+
timer_widget = self.query_one(TimerWidget)
|
|
62
|
+
timer_widget.is_running = not timer_widget.is_running #type: ignore
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
def action_reset_timer(self) -> None:
|
|
67
|
+
try:
|
|
68
|
+
timer_widget = self.query_one(TimerWidget)
|
|
69
|
+
timer_widget.reset_timer_to_mode()
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
def action_skip_timer(self) -> None:
|
|
74
|
+
try:
|
|
75
|
+
timer_widget = self.query_one(TimerWidget)
|
|
76
|
+
timer_widget.transition_to_next()
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def action_set_mode_focus(self) -> None:
|
|
81
|
+
try:
|
|
82
|
+
timer_widget = self.query_one(TimerWidget)
|
|
83
|
+
timer_widget.mode = "Focus"
|
|
84
|
+
timer_widget.reset_timer_to_mode()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
def action_set_mode_short(self) -> None:
|
|
89
|
+
try:
|
|
90
|
+
timer_widget = self.query_one(TimerWidget)
|
|
91
|
+
timer_widget.mode = "Short Break"
|
|
92
|
+
timer_widget.reset_timer_to_mode()
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
def action_set_mode_long(self) -> None:
|
|
97
|
+
try:
|
|
98
|
+
timer_widget = self.query_one(TimerWidget)
|
|
99
|
+
timer_widget.mode = "Long Break"
|
|
100
|
+
timer_widget.reset_timer_to_mode()
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
def action_focus_todo_input(self) -> None:
|
|
105
|
+
try:
|
|
106
|
+
input_box = self.query_one("#new-task-input", Input)
|
|
107
|
+
input_box.focus()
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def action_toggle_shortcuts(self) -> None:
|
|
112
|
+
if isinstance(self.screen, ShortcutsScreen):
|
|
113
|
+
self.pop_screen()
|
|
114
|
+
else:
|
|
115
|
+
self.push_screen(ShortcutsScreen())
|
|
116
|
+
|
|
117
|
+
def action_reset_session(self) -> None:
|
|
118
|
+
try:
|
|
119
|
+
timer_widget = self.query_one(TimerWidget)
|
|
120
|
+
timer_widget.is_running = False #type: ignore
|
|
121
|
+
timer_widget.completed_focus_sessions = 0
|
|
122
|
+
self.config["completed_focus_sessions"] = 0
|
|
123
|
+
save_config(self.config)
|
|
124
|
+
|
|
125
|
+
timer_widget.query_one("#session-count-display", Label).update("Completed: 0 sessions")
|
|
126
|
+
timer_widget.mode = "Focus"
|
|
127
|
+
timer_widget.reset_timer_to_mode()
|
|
128
|
+
self.notify("Pomodoro session reset completely.", title="Session Reset", severity="information")
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
def update_active_task_display(self) -> None:
|
|
133
|
+
try:
|
|
134
|
+
timer_widget = self.query_one(TimerWidget)
|
|
135
|
+
active_label = timer_widget.query_one("#active-task-display", Label)
|
|
136
|
+
|
|
137
|
+
active_task = None
|
|
138
|
+
if self.active_task_id:
|
|
139
|
+
for task in self.config.get("tasks", []):
|
|
140
|
+
if task["id"] == self.active_task_id:
|
|
141
|
+
active_task = task
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
if active_task:
|
|
145
|
+
active_label.update(f"Active Task: [bold #f9e2af]{active_task['title']}[/bold #f9e2af]")
|
|
146
|
+
timer_widget.query_one("#timer-clock").add_class("timer-clock-active")
|
|
147
|
+
else:
|
|
148
|
+
active_label.update("No active task selected")
|
|
149
|
+
timer_widget.query_one("#timer-clock").remove_class("timer-clock-active")
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
def increment_active_task_pomodoro(self) -> None:
|
|
154
|
+
if not self.active_task_id:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
for task in self.config["tasks"]:
|
|
158
|
+
if task["id"] == self.active_task_id:
|
|
159
|
+
task["pomodoros"] += 1
|
|
160
|
+
self.notify(f"Pomodoro recorded for: {task['title']}", title="Task Updated", severity="success") #type: ignore
|
|
161
|
+
break
|
|
162
|
+
save_config(self.config)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
task_list_widget = self.query_one(TaskListWidget)
|
|
166
|
+
task_list_widget.refresh_tasks()
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
def on_settings_updated(self) -> None:
|
|
171
|
+
try:
|
|
172
|
+
timer_widget = self.query_one(TimerWidget)
|
|
173
|
+
if not timer_widget.is_running:
|
|
174
|
+
timer_widget.reset_timer_to_mode()
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
def main():
|
|
179
|
+
app = PomodoroApp()
|
|
180
|
+
app.run()
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__":
|
|
183
|
+
main()
|
pttm-0.1.0/pttm/clock.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Big blocky font digits for the clock
|
|
2
|
+
DIGITS = {
|
|
3
|
+
'0': ["███", "█ █", "█ █", "█ █", "███"],
|
|
4
|
+
'1': [" █ ", "██ ", " █ ", " █ ", "███"],
|
|
5
|
+
'2': ["███", " █", "███", "█ ", "███"],
|
|
6
|
+
'3': ["███", " █", "███", " █", "███"],
|
|
7
|
+
'4': ["█ █", "█ █", "███", " █", " █"],
|
|
8
|
+
'5': ["███", "█ ", "███", " █", "███"],
|
|
9
|
+
'6': ["███", "█ ", "███", "█ █", "███"],
|
|
10
|
+
'7': ["███", " █", " █", " █", " █"],
|
|
11
|
+
'8': ["███", "█ █", "███", "█ █", "███"],
|
|
12
|
+
'9': ["███", "█ █", "███", " █", "███"],
|
|
13
|
+
':': [" ", " █ ", " ", " █ ", " "],
|
|
14
|
+
' ': [" ", " ", " ", " ", " "]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def make_clock_ascii(seconds: int) -> str:
|
|
18
|
+
mins = seconds // 60
|
|
19
|
+
secs = seconds % 60
|
|
20
|
+
time_str = f"{mins:02d}:{secs:02d}"
|
|
21
|
+
|
|
22
|
+
lines = ["", "", "", "", ""]
|
|
23
|
+
for char in time_str:
|
|
24
|
+
char_lines = DIGITS.get(char, DIGITS[' '])
|
|
25
|
+
for idx in range(5):
|
|
26
|
+
lines[idx] += char_lines[idx] + " "
|
|
27
|
+
|
|
28
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import platformdirs
|
|
4
|
+
|
|
5
|
+
CONFIG_PATH_ENV = os.getenv("PMO_CONFIG_PATH")
|
|
6
|
+
if CONFIG_PATH_ENV:
|
|
7
|
+
CONFIG_FILE = CONFIG_PATH_ENV
|
|
8
|
+
else:
|
|
9
|
+
CONFIG_DIR = platformdirs.user_config_dir("pttm", appauthor=False)
|
|
10
|
+
CONFIG_FILE = os.path.join(CONFIG_DIR, "pttm_config.json")
|
|
11
|
+
|
|
12
|
+
DEFAULT_CONFIG = {
|
|
13
|
+
"settings": {
|
|
14
|
+
"focus_time": 25,
|
|
15
|
+
"short_break_time": 5,
|
|
16
|
+
"long_break_time": 15,
|
|
17
|
+
"long_break_interval": 4
|
|
18
|
+
},
|
|
19
|
+
"completed_focus_sessions": 0,
|
|
20
|
+
"tasks": [
|
|
21
|
+
{"id": "1", "title": "Task 1", "completed": True, "pomodoros": 1},
|
|
22
|
+
{"id": "2", "title": "Task 2", "completed": False, "pomodoros": 0},
|
|
23
|
+
{"id": "3", "title": "Task 3", "completed": False, "pomodoros": 0}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def load_config():
|
|
28
|
+
if os.path.exists(CONFIG_FILE):
|
|
29
|
+
try:
|
|
30
|
+
with open(CONFIG_FILE, "r") as f:
|
|
31
|
+
config = json.load(f)
|
|
32
|
+
# Ensure all sections exist
|
|
33
|
+
if "settings" not in config:
|
|
34
|
+
config["settings"] = DEFAULT_CONFIG["settings"]
|
|
35
|
+
if "tasks" not in config:
|
|
36
|
+
config["tasks"] = DEFAULT_CONFIG["tasks"]
|
|
37
|
+
if "completed_focus_sessions" not in config:
|
|
38
|
+
config["completed_focus_sessions"] = 0
|
|
39
|
+
return config
|
|
40
|
+
except Exception:
|
|
41
|
+
return DEFAULT_CONFIG.copy()
|
|
42
|
+
return DEFAULT_CONFIG.copy()
|
|
43
|
+
|
|
44
|
+
def save_config(config):
|
|
45
|
+
try:
|
|
46
|
+
dir_name = os.path.dirname(CONFIG_FILE)
|
|
47
|
+
if dir_name:
|
|
48
|
+
os.makedirs(dir_name, exist_ok=True)
|
|
49
|
+
with open(CONFIG_FILE, "w") as f:
|
|
50
|
+
json.dump(config, f, indent=4)
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
|