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.
@@ -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
@@ -0,0 +1,4 @@
1
+ tzr.db
2
+ *.pyc
3
+ src/taganizer.egg-info/
4
+ dist/
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """Taganizer package."""
2
+
3
+ from .item import Item, ItemError
4
+ from .item_store import ItemStore, SqlItemStore
5
+
6
+ __all__ = ["Item", "ItemError", "ItemStore", "SqlItemStore"]
@@ -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:]))