taganizer 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.
- taganizer-0.1.0/.flake8 +13 -0
- taganizer-0.1.0/.github/workflows/ci.yml +35 -0
- taganizer-0.1.0/.github/workflows/publish.yml +33 -0
- taganizer-0.1.0/.gitignore +4 -0
- taganizer-0.1.0/PKG-INFO +174 -0
- taganizer-0.1.0/README.md +156 -0
- taganizer-0.1.0/pyproject.toml +40 -0
- taganizer-0.1.0/setup.cfg +4 -0
- taganizer-0.1.0/src/taganizer/__init__.py +6 -0
- taganizer-0.1.0/src/taganizer/cli.py +232 -0
- taganizer-0.1.0/src/taganizer/item.py +254 -0
- taganizer-0.1.0/src/taganizer/item_store.py +437 -0
- taganizer-0.1.0/src/taganizer.egg-info/PKG-INFO +174 -0
- taganizer-0.1.0/src/taganizer.egg-info/SOURCES.txt +21 -0
- taganizer-0.1.0/src/taganizer.egg-info/dependency_links.txt +1 -0
- taganizer-0.1.0/src/taganizer.egg-info/entry_points.txt +2 -0
- taganizer-0.1.0/src/taganizer.egg-info/requires.txt +7 -0
- taganizer-0.1.0/src/taganizer.egg-info/scm_file_list.json +17 -0
- taganizer-0.1.0/src/taganizer.egg-info/scm_version.json +8 -0
- taganizer-0.1.0/src/taganizer.egg-info/top_level.txt +1 -0
- taganizer-0.1.0/tests/test_cli.py +57 -0
- taganizer-0.1.0/tests/test_item.py +65 -0
- taganizer-0.1.0/tests/test_item_store.py +189 -0
taganizer-0.1.0/.flake8
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[flake8]
|
|
2
|
+
max-line-length = 100
|
|
3
|
+
# Ignore rules that conflict with black's formatting:
|
|
4
|
+
# E203 - whitespace before ':' (black formats slices this way)
|
|
5
|
+
# W503 - line break before binary operator (black's preferred style)
|
|
6
|
+
extend-ignore = E203, W503
|
|
7
|
+
exclude =
|
|
8
|
+
.git,
|
|
9
|
+
.venv,
|
|
10
|
+
dist,
|
|
11
|
+
build,
|
|
12
|
+
__pycache__,
|
|
13
|
+
*.egg-info
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- name: Check out repository
|
|
12
|
+
uses: actions/checkout@v4
|
|
13
|
+
with:
|
|
14
|
+
# Full history so setuptools-scm can derive the version from tags.
|
|
15
|
+
fetch-depth: 0
|
|
16
|
+
|
|
17
|
+
- name: Set up Python
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.14"
|
|
21
|
+
cache: pip
|
|
22
|
+
|
|
23
|
+
- name: Install package and dev dependencies
|
|
24
|
+
run: |
|
|
25
|
+
python -m pip install --upgrade pip
|
|
26
|
+
pip install -e ".[dev]"
|
|
27
|
+
|
|
28
|
+
- name: Check formatting
|
|
29
|
+
run: black --check .
|
|
30
|
+
|
|
31
|
+
- name: Lint
|
|
32
|
+
run: flake8 src tests
|
|
33
|
+
|
|
34
|
+
- name: Run tests
|
|
35
|
+
run: pytest tests
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: pypi
|
|
12
|
+
permissions:
|
|
13
|
+
# Required for PyPI Trusted Publishing (OIDC); no API token needed.
|
|
14
|
+
id-token: write
|
|
15
|
+
steps:
|
|
16
|
+
- name: Check out repository
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
# Full history + tags so setuptools-scm derives the version from the tag.
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
|
|
22
|
+
- name: Set up Python
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.14"
|
|
26
|
+
|
|
27
|
+
- name: Build distributions
|
|
28
|
+
run: |
|
|
29
|
+
python -m pip install --upgrade pip build
|
|
30
|
+
python -m build
|
|
31
|
+
|
|
32
|
+
- name: Publish to PyPI
|
|
33
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
taganizer-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: taganizer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A tag-based organization system
|
|
5
|
+
Author-email: Hilko <hilkoc@users.noreply.github.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/hilkoc/taganizer
|
|
7
|
+
Project-URL: Repository, https://github.com/hilkoc/taganizer
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.3.5; extra == "dev"
|
|
14
|
+
Requires-Dist: black>=24; extra == "dev"
|
|
15
|
+
Requires-Dist: flake8>=7; extra == "dev"
|
|
16
|
+
Requires-Dist: build>=1; extra == "dev"
|
|
17
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
18
|
+
|
|
19
|
+
# Taganizer
|
|
20
|
+
|
|
21
|
+
Taganizer is a tag-based item organizer. It stores items in SQLite, arranges them
|
|
22
|
+
into a parent/child tag hierarchy, and lets you search by combining tags.
|
|
23
|
+
|
|
24
|
+
It can be used as tool for agent memory.
|
|
25
|
+
|
|
26
|
+
## Overview
|
|
27
|
+
|
|
28
|
+
- Items are anything worth tracking: files, directories, URLs, snippets of text or other tags
|
|
29
|
+
- Items are linked into a parent/child **tag hierarchy** (a directed acyclic graph); a
|
|
30
|
+
database trigger prevents cycles.
|
|
31
|
+
- **Search** finds the intersection of tags, and supports the *union of intersections*:
|
|
32
|
+
`A B + C` means *(A and B) or C*.
|
|
33
|
+
- Pure standard library, no dependencies. Requires Python 3.8+.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
The package defines a `tzr` entry point, so installing it puts a `tzr` command on your
|
|
38
|
+
`PATH` — you can run `tzr <subcommand>` directly, with no `python -m taganizer.cli` prefix.
|
|
39
|
+
Install from a checkout of the repository (use `.`), or once published, from PyPI (use
|
|
40
|
+
`taganizer`). Verify with `tzr --version`.
|
|
41
|
+
|
|
42
|
+
### Pipx
|
|
43
|
+
|
|
44
|
+
Installs `tzr` into an isolated, globally available environment:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pipx install taganizer
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Pip
|
|
51
|
+
|
|
52
|
+
Installs into the currently active Python environment:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install taganizer
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
By default the database lives at `~/.tzr.db`. Point at a different file with the global
|
|
62
|
+
`--db PATH` flag (usable before or after the subcommand). See `tzr --version` and
|
|
63
|
+
`tzr --help` for details.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Create the database schema
|
|
67
|
+
tzr init
|
|
68
|
+
|
|
69
|
+
# Create some items
|
|
70
|
+
tzr create tag python
|
|
71
|
+
tzr create url https://example.com
|
|
72
|
+
|
|
73
|
+
# Tag one item with another (child, parent)
|
|
74
|
+
tzr tag https://example.com python # -> "https://example.com tagged with python"
|
|
75
|
+
|
|
76
|
+
# List items (optionally as a tree)
|
|
77
|
+
tzr ls
|
|
78
|
+
tzr ls --tree
|
|
79
|
+
|
|
80
|
+
# Search: intersection of tags, or a union of intersections
|
|
81
|
+
tzr search python
|
|
82
|
+
tzr search python web + news # (python and web) or news
|
|
83
|
+
|
|
84
|
+
# Rename or delete items
|
|
85
|
+
tzr rename python py
|
|
86
|
+
tzr delete py
|
|
87
|
+
|
|
88
|
+
# Export the database to a SQL dump (default: tzr_export.sql)
|
|
89
|
+
tzr export
|
|
90
|
+
tzr export my_dump.sql
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Commands
|
|
94
|
+
|
|
95
|
+
| Command | Description |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| `tzr init` | Initialize the database schema. |
|
|
98
|
+
| `tzr export [FILE]` | Export the database to an SQL dump file. |
|
|
99
|
+
| `tzr ls` / `tzr list` | List all items (`--tree` shows the association tree). |
|
|
100
|
+
| `tzr create <type> <name>` | Create a new item of the given type. |
|
|
101
|
+
| `tzr rename <old> <new>` | Rename an item (its type is unchanged). |
|
|
102
|
+
| `tzr delete <name>` | Delete an item (its tag links are removed; other items are unaffected). |
|
|
103
|
+
| `tzr tag <child> <parent>` | Associate an item with a parent tag. |
|
|
104
|
+
| `tzr search <query>...` | Search items by tag; `+` unions groups. |
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
## Library usage
|
|
108
|
+
|
|
109
|
+
Taganizer can also be used directly as a library. `ItemStore` owns a single database
|
|
110
|
+
connection and works as a context manager:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from taganizer import ItemStore, Item
|
|
114
|
+
|
|
115
|
+
with ItemStore("my.db") as store:
|
|
116
|
+
store.initialize() # create schema (idempotent)
|
|
117
|
+
store.create_item(Item(-1, "TAG", "python"))
|
|
118
|
+
store.create_item(Item(-1, "URL", "https://example.com"))
|
|
119
|
+
store.associate("python", "https://example.com")
|
|
120
|
+
|
|
121
|
+
# search returns a tuple (map id ->Item, list of associations)
|
|
122
|
+
matches, _ = store.search([["python"]])
|
|
123
|
+
for item in matches.values():
|
|
124
|
+
print(item)
|
|
125
|
+
# the connection is closed on exit
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`SqlItemStore` is a drop-in subclass whose search resolves the union of intersections in
|
|
129
|
+
a single SQL statement.
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
Set up an isolated virtual environment and install the project in editable mode so code
|
|
134
|
+
changes take effect without reinstalling. Run `tzr <subcommand>` afterwards to exercise
|
|
135
|
+
your changes.
|
|
136
|
+
|
|
137
|
+
### Virtual environment
|
|
138
|
+
|
|
139
|
+
Create and activate a virtual environment in the repository root:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
python3 -m venv .venv # create the environment
|
|
143
|
+
source .venv/bin/activate # activate it (Linux/macOS)
|
|
144
|
+
# .venv\Scripts\activate # activate it (Windows PowerShell/cmd)
|
|
145
|
+
python -m pip install --upgrade pip
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Then install the project. Include the `dev` extra to get the test and lint tools
|
|
149
|
+
(`pytest`, `black`, `flake8`, `build`, `twine`):
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pip install -e ".[dev]" # editable install with dev tooling
|
|
153
|
+
# pip install -e . # or just the package, without dev tooling
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Deactivate the environment when you're done with `deactivate`.
|
|
157
|
+
|
|
158
|
+
### Pipx
|
|
159
|
+
|
|
160
|
+
Alternatively, install `tzr` into an isolated, globally available environment:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
pipx install --editable .
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Run the test suite from the repository root (with the virtual environment activated):
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
pytest tests
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
Released under the MIT License.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Taganizer
|
|
2
|
+
|
|
3
|
+
Taganizer is a tag-based item organizer. It stores items in SQLite, arranges them
|
|
4
|
+
into a parent/child tag hierarchy, and lets you search by combining tags.
|
|
5
|
+
|
|
6
|
+
It can be used as tool for agent memory.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
- Items are anything worth tracking: files, directories, URLs, snippets of text or other tags
|
|
11
|
+
- Items are linked into a parent/child **tag hierarchy** (a directed acyclic graph); a
|
|
12
|
+
database trigger prevents cycles.
|
|
13
|
+
- **Search** finds the intersection of tags, and supports the *union of intersections*:
|
|
14
|
+
`A B + C` means *(A and B) or C*.
|
|
15
|
+
- Pure standard library, no dependencies. Requires Python 3.8+.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
The package defines a `tzr` entry point, so installing it puts a `tzr` command on your
|
|
20
|
+
`PATH` — you can run `tzr <subcommand>` directly, with no `python -m taganizer.cli` prefix.
|
|
21
|
+
Install from a checkout of the repository (use `.`), or once published, from PyPI (use
|
|
22
|
+
`taganizer`). Verify with `tzr --version`.
|
|
23
|
+
|
|
24
|
+
### Pipx
|
|
25
|
+
|
|
26
|
+
Installs `tzr` into an isolated, globally available environment:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pipx install taganizer
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Pip
|
|
33
|
+
|
|
34
|
+
Installs into the currently active Python environment:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install taganizer
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
By default the database lives at `~/.tzr.db`. Point at a different file with the global
|
|
44
|
+
`--db PATH` flag (usable before or after the subcommand). See `tzr --version` and
|
|
45
|
+
`tzr --help` for details.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Create the database schema
|
|
49
|
+
tzr init
|
|
50
|
+
|
|
51
|
+
# Create some items
|
|
52
|
+
tzr create tag python
|
|
53
|
+
tzr create url https://example.com
|
|
54
|
+
|
|
55
|
+
# Tag one item with another (child, parent)
|
|
56
|
+
tzr tag https://example.com python # -> "https://example.com tagged with python"
|
|
57
|
+
|
|
58
|
+
# List items (optionally as a tree)
|
|
59
|
+
tzr ls
|
|
60
|
+
tzr ls --tree
|
|
61
|
+
|
|
62
|
+
# Search: intersection of tags, or a union of intersections
|
|
63
|
+
tzr search python
|
|
64
|
+
tzr search python web + news # (python and web) or news
|
|
65
|
+
|
|
66
|
+
# Rename or delete items
|
|
67
|
+
tzr rename python py
|
|
68
|
+
tzr delete py
|
|
69
|
+
|
|
70
|
+
# Export the database to a SQL dump (default: tzr_export.sql)
|
|
71
|
+
tzr export
|
|
72
|
+
tzr export my_dump.sql
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Commands
|
|
76
|
+
|
|
77
|
+
| Command | Description |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `tzr init` | Initialize the database schema. |
|
|
80
|
+
| `tzr export [FILE]` | Export the database to an SQL dump file. |
|
|
81
|
+
| `tzr ls` / `tzr list` | List all items (`--tree` shows the association tree). |
|
|
82
|
+
| `tzr create <type> <name>` | Create a new item of the given type. |
|
|
83
|
+
| `tzr rename <old> <new>` | Rename an item (its type is unchanged). |
|
|
84
|
+
| `tzr delete <name>` | Delete an item (its tag links are removed; other items are unaffected). |
|
|
85
|
+
| `tzr tag <child> <parent>` | Associate an item with a parent tag. |
|
|
86
|
+
| `tzr search <query>...` | Search items by tag; `+` unions groups. |
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
## Library usage
|
|
90
|
+
|
|
91
|
+
Taganizer can also be used directly as a library. `ItemStore` owns a single database
|
|
92
|
+
connection and works as a context manager:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from taganizer import ItemStore, Item
|
|
96
|
+
|
|
97
|
+
with ItemStore("my.db") as store:
|
|
98
|
+
store.initialize() # create schema (idempotent)
|
|
99
|
+
store.create_item(Item(-1, "TAG", "python"))
|
|
100
|
+
store.create_item(Item(-1, "URL", "https://example.com"))
|
|
101
|
+
store.associate("python", "https://example.com")
|
|
102
|
+
|
|
103
|
+
# search returns a tuple (map id ->Item, list of associations)
|
|
104
|
+
matches, _ = store.search([["python"]])
|
|
105
|
+
for item in matches.values():
|
|
106
|
+
print(item)
|
|
107
|
+
# the connection is closed on exit
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`SqlItemStore` is a drop-in subclass whose search resolves the union of intersections in
|
|
111
|
+
a single SQL statement.
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
Set up an isolated virtual environment and install the project in editable mode so code
|
|
116
|
+
changes take effect without reinstalling. Run `tzr <subcommand>` afterwards to exercise
|
|
117
|
+
your changes.
|
|
118
|
+
|
|
119
|
+
### Virtual environment
|
|
120
|
+
|
|
121
|
+
Create and activate a virtual environment in the repository root:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
python3 -m venv .venv # create the environment
|
|
125
|
+
source .venv/bin/activate # activate it (Linux/macOS)
|
|
126
|
+
# .venv\Scripts\activate # activate it (Windows PowerShell/cmd)
|
|
127
|
+
python -m pip install --upgrade pip
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Then install the project. Include the `dev` extra to get the test and lint tools
|
|
131
|
+
(`pytest`, `black`, `flake8`, `build`, `twine`):
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pip install -e ".[dev]" # editable install with dev tooling
|
|
135
|
+
# pip install -e . # or just the package, without dev tooling
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Deactivate the environment when you're done with `deactivate`.
|
|
139
|
+
|
|
140
|
+
### Pipx
|
|
141
|
+
|
|
142
|
+
Alternatively, install `tzr` into an isolated, globally available environment:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
pipx install --editable .
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Run the test suite from the repository root (with the virtual environment activated):
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pytest tests
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
Released under the MIT License.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=80", "setuptools-scm[simple]>=9.2"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "taganizer"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Hilko", email = "hilkoc@users.noreply.github.com"},
|
|
10
|
+
]
|
|
11
|
+
description = "A tag-based organization system"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
# dependencies = ["sqlite3",] # Built-in, no need to specify
|
|
19
|
+
dependencies = []
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest>=8.3.5",
|
|
24
|
+
"black>=24",
|
|
25
|
+
"flake8>=7",
|
|
26
|
+
"build>=1",
|
|
27
|
+
"twine>=5",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
tzr = "taganizer.cli:main"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/hilkoc/taganizer"
|
|
35
|
+
Repository = "https://github.com/hilkoc/taganizer"
|
|
36
|
+
|
|
37
|
+
[tool.black]
|
|
38
|
+
line-length = 100
|
|
39
|
+
target-version = ["py314"]
|
|
40
|
+
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""The main entry point for tzr."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
8
|
+
from .item import Item
|
|
9
|
+
from . import item_store
|
|
10
|
+
|
|
11
|
+
# Default database location: a hidden file in the user's home directory.
|
|
12
|
+
DEFAULT_DB = "~/.tzr.db"
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
__version__ = version("taganizer")
|
|
16
|
+
except PackageNotFoundError: # not installed (e.g. run from a source checkout)
|
|
17
|
+
__version__ = "0.0.0"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def db(db_path):
|
|
21
|
+
"""Open and return an ItemStore for the given database path.
|
|
22
|
+
|
|
23
|
+
The returned store owns a single connection; use it as a context manager.
|
|
24
|
+
"""
|
|
25
|
+
return item_store.ItemStore(os.path.expanduser(db_path))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def output(*args, **kwargs):
|
|
29
|
+
print(*args, **kwargs)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def show_item(item, level, indent):
|
|
33
|
+
temp = level * indent
|
|
34
|
+
output(temp, str(item), sep="")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def init(store, args):
|
|
38
|
+
output("initializing database")
|
|
39
|
+
result = store.initialize()
|
|
40
|
+
output(result)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def export(store, args):
|
|
44
|
+
output("Exporting database to %s" % args.dump_file)
|
|
45
|
+
with open(args.dump_file, "w") as f:
|
|
46
|
+
result = store.dump_db(f)
|
|
47
|
+
output(result)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_tree(item_dict, pairs_list):
|
|
51
|
+
for pair in pairs_list:
|
|
52
|
+
p = item_dict[pair[0]]
|
|
53
|
+
c = item_dict[pair[1]]
|
|
54
|
+
p.add_child(c)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def list_items(store, args):
|
|
58
|
+
item_dict = store.select_all()
|
|
59
|
+
if not args.tree:
|
|
60
|
+
for x in item_dict.values():
|
|
61
|
+
output(x)
|
|
62
|
+
else:
|
|
63
|
+
pairs_list = store.all_associations()
|
|
64
|
+
build_tree(item_dict, pairs_list)
|
|
65
|
+
for root in item_dict.values():
|
|
66
|
+
if root.parents is None:
|
|
67
|
+
root.traverse_children(show_item, " ")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def create(store, args):
|
|
71
|
+
item = Item(-1, args.type.upper(), args.name)
|
|
72
|
+
rowid = store.create_item(item)
|
|
73
|
+
output("created item with id: %i" % rowid)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def rename(store, args):
|
|
77
|
+
rowcount = store.rename_item(args.old, args.new)
|
|
78
|
+
output("renamed %i item" % rowcount)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def delete(store, args):
|
|
82
|
+
rowcount = store.delete_item(args.name)
|
|
83
|
+
output("deleted %i item" % rowcount)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def add_association(store, args):
|
|
87
|
+
try:
|
|
88
|
+
store.associate(args.parent_name, args.child_name)
|
|
89
|
+
except ValueError as e:
|
|
90
|
+
output(
|
|
91
|
+
"tzr: cannot tag: %s. "
|
|
92
|
+
"Usage: tzr tag <child> <parent> — did you swap the arguments?" % e,
|
|
93
|
+
file=sys.stderr,
|
|
94
|
+
)
|
|
95
|
+
return 1
|
|
96
|
+
output("%s tagged with %s" % (args.child_name, args.parent_name))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def split_groups(tokens):
|
|
100
|
+
"""Split search tokens into groups on the '+' separator.
|
|
101
|
+
|
|
102
|
+
E.g. ['A', 'B', '+', 'C'] -> [['A', 'B'], ['C']], meaning (A ∩ B) ∪ (C).
|
|
103
|
+
"""
|
|
104
|
+
groups, current = [], []
|
|
105
|
+
for tok in tokens:
|
|
106
|
+
if tok == "+":
|
|
107
|
+
if current:
|
|
108
|
+
groups.append(current)
|
|
109
|
+
current = []
|
|
110
|
+
else:
|
|
111
|
+
current.append(tok)
|
|
112
|
+
if current:
|
|
113
|
+
groups.append(current)
|
|
114
|
+
return groups
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def search(store, args):
|
|
118
|
+
groups = split_groups(args.query)
|
|
119
|
+
item_dict, pairs_list = store.search(groups)
|
|
120
|
+
build_tree(item_dict, pairs_list)
|
|
121
|
+
for root in item_dict.values():
|
|
122
|
+
if root.parents is None:
|
|
123
|
+
root.traverse_children(show_item, " ")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def main(main_args=None):
|
|
127
|
+
parser = argparse.ArgumentParser(
|
|
128
|
+
prog="tzr", description="Taganizer - a tag-based item organizer."
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
131
|
+
# --db is accepted both before the subcommand (top-level, with the default) and after
|
|
132
|
+
# it (via the shared parent parser). SUPPRESS on the parent means it only overrides
|
|
133
|
+
# args.db when actually given, so it never clobbers a value passed before the subcommand.
|
|
134
|
+
parser.add_argument(
|
|
135
|
+
"--db",
|
|
136
|
+
default=DEFAULT_DB,
|
|
137
|
+
help=f"Path to the SQLite database file (default: {DEFAULT_DB}).",
|
|
138
|
+
)
|
|
139
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
140
|
+
common.add_argument(
|
|
141
|
+
"--db",
|
|
142
|
+
default=argparse.SUPPRESS,
|
|
143
|
+
help=f"Path to the SQLite database file (default: {DEFAULT_DB}).",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
subparsers = parser.add_subparsers(
|
|
147
|
+
dest="command_name",
|
|
148
|
+
title="subcommands",
|
|
149
|
+
description="valid subcommands",
|
|
150
|
+
help="sub-command help",
|
|
151
|
+
)
|
|
152
|
+
# the parser for the "init" command
|
|
153
|
+
parser_init = subparsers.add_parser(
|
|
154
|
+
"init", parents=[common], help="Initialize the database schema."
|
|
155
|
+
)
|
|
156
|
+
parser_init.set_defaults(func=init)
|
|
157
|
+
# the parser for the "export" command
|
|
158
|
+
parser_export = subparsers.add_parser(
|
|
159
|
+
"export", parents=[common], help="Export the database to an SQL dump file."
|
|
160
|
+
)
|
|
161
|
+
parser_export.add_argument(
|
|
162
|
+
"dump_file",
|
|
163
|
+
nargs="?",
|
|
164
|
+
default="tzr_export.sql",
|
|
165
|
+
help="File to write the SQL dump to (default: tzr_export.sql).",
|
|
166
|
+
)
|
|
167
|
+
parser_export.set_defaults(func=export)
|
|
168
|
+
# the parser for the "ls" command
|
|
169
|
+
parser_ls = subparsers.add_parser(
|
|
170
|
+
"ls", parents=[common], aliases=["list"], help="List all items."
|
|
171
|
+
)
|
|
172
|
+
parser_ls.add_argument(
|
|
173
|
+
"-t", "--tree", action="store_true", help="Show items as a tree of associations."
|
|
174
|
+
)
|
|
175
|
+
parser_ls.set_defaults(func=list_items)
|
|
176
|
+
# the parser for the "create" command
|
|
177
|
+
parser_create = subparsers.add_parser(
|
|
178
|
+
"create",
|
|
179
|
+
parents=[common],
|
|
180
|
+
aliases=["cr", "add"],
|
|
181
|
+
help="Create a new item of the given type.",
|
|
182
|
+
)
|
|
183
|
+
parser_create.add_argument(
|
|
184
|
+
"type", choices=["tag", "dir", "url", "txt", "cmd"], help="Item type."
|
|
185
|
+
)
|
|
186
|
+
parser_create.add_argument("name", help="Tag name, or URL/value of the item.")
|
|
187
|
+
parser_create.set_defaults(func=create)
|
|
188
|
+
# the parser for the "rename" command
|
|
189
|
+
parser_rename = subparsers.add_parser(
|
|
190
|
+
"rename", parents=[common], aliases=["mv"], help="Rename an item (its type is unchanged)."
|
|
191
|
+
)
|
|
192
|
+
parser_rename.add_argument("old", help="Current name or URL of the item.")
|
|
193
|
+
parser_rename.add_argument("new", help="New name or URL of the item.")
|
|
194
|
+
parser_rename.set_defaults(func=rename)
|
|
195
|
+
# the parser for the "delete" command
|
|
196
|
+
parser_delete = subparsers.add_parser(
|
|
197
|
+
"delete", parents=[common], aliases=["rm", "del"], help="Delete an item."
|
|
198
|
+
)
|
|
199
|
+
parser_delete.add_argument(
|
|
200
|
+
"name",
|
|
201
|
+
help="Delete the item with the given name and all its associations.",
|
|
202
|
+
)
|
|
203
|
+
parser_delete.set_defaults(func=delete)
|
|
204
|
+
# the parser for the "tag" command
|
|
205
|
+
parser_tag = subparsers.add_parser(
|
|
206
|
+
"tag", parents=[common], aliases=["label"], help="Associate an item with a parent tag."
|
|
207
|
+
)
|
|
208
|
+
parser_tag.add_argument("child_name", help="Item to be tagged (child).")
|
|
209
|
+
parser_tag.add_argument("parent_name", help="Parent tag to apply.")
|
|
210
|
+
parser_tag.set_defaults(func=add_association)
|
|
211
|
+
# the parser for the "search" command
|
|
212
|
+
parser_search = subparsers.add_parser(
|
|
213
|
+
"search", parents=[common], aliases=["s", "show", "select"], help="Search items by tag."
|
|
214
|
+
)
|
|
215
|
+
parser_search.add_argument(
|
|
216
|
+
"query",
|
|
217
|
+
nargs="+",
|
|
218
|
+
help="Tags to search for. Example: 'A B + C' means items tagged with (A and B) or C.",
|
|
219
|
+
)
|
|
220
|
+
parser_search.set_defaults(func=search)
|
|
221
|
+
|
|
222
|
+
args = parser.parse_args(main_args)
|
|
223
|
+
|
|
224
|
+
if hasattr(args, "func"):
|
|
225
|
+
with db(args.db) as store:
|
|
226
|
+
return args.func(store, args) or 0
|
|
227
|
+
parser.print_help()
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
sys.exit(main(sys.argv[1:]))
|