kctl-glpi 0.2.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,33 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ *.egg
6
+ dist/
7
+ build/
8
+ .eggs/
9
+
10
+ # Virtual environments
11
+ .venv/
12
+ venv/
13
+
14
+ # IDE
15
+ .idea/
16
+ .vscode/
17
+ *.swp
18
+ *.swo
19
+
20
+ # Testing
21
+ .pytest_cache/
22
+ .coverage
23
+ htmlcov/
24
+ .mypy_cache/
25
+ .ruff_cache/
26
+
27
+ # OS
28
+ .DS_Store
29
+ Thumbs.db
30
+
31
+ # Environment
32
+ .env
33
+ .env.local
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: kctl-glpi
3
+ Version: 0.2.0
4
+ Summary: Kodemeio GLPI CLI - manage GLPI IT Service Management platform
5
+ Author-email: Kodemeio <dev@kodeme.io>
6
+ License-Expression: MIT
7
+ Keywords: asset-management,cli,glpi,helpdesk,itsm,kodemeio
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: System Administrators
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: System :: Systems Administration
15
+ Requires-Python: >=3.12
16
+ Requires-Dist: httpx>=0.28.0
17
+ Requires-Dist: kctl-lib>=0.7.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: mypy>=1.14.0; extra == 'dev'
20
+ Requires-Dist: pytest-httpx>=0.35.0; extra == 'dev'
21
+ Requires-Dist: pytest>=8.3.0; extra == 'dev'
22
+ Requires-Dist: ruff>=0.9.0; extra == 'dev'
23
+ Requires-Dist: types-pyyaml>=6.0.0; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # kctl-glpi
27
+
28
+ GLPI IT asset management CLI for Kodemeio infrastructure.
@@ -0,0 +1,3 @@
1
+ # kctl-glpi
2
+
3
+ GLPI IT asset management CLI for Kodemeio infrastructure.
@@ -0,0 +1,60 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "kctl-glpi"
7
+ version = "0.2.0"
8
+ description = "Kodemeio GLPI CLI - manage GLPI IT Service Management platform"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.12"
12
+ authors = [{ name = "Kodemeio", email = "dev@kodeme.io" }]
13
+ keywords = ["glpi", "itsm", "helpdesk", "asset-management", "cli", "kodemeio"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: System Administrators",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: System :: Systems Administration",
22
+ ]
23
+ dependencies = [
24
+ "kctl-lib>=0.7.0",
25
+ "httpx>=0.28.0",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=8.3.0",
31
+ "pytest-httpx>=0.35.0",
32
+ "ruff>=0.9.0",
33
+ "mypy>=1.14.0",
34
+ "types-PyYAML>=6.0.0",
35
+ ]
36
+
37
+ [project.scripts]
38
+ kctl-glpi = "kctl_glpi.cli:_run"
39
+
40
+ [tool.uv.sources]
41
+ kctl-lib = { workspace = true }
42
+
43
+ [project.entry-points."kctl_glpi.plugins"]
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/kctl_glpi"]
47
+
48
+ [tool.ruff]
49
+ target-version = "py312"
50
+ line-length = 120
51
+
52
+ [tool.ruff.lint]
53
+ select = ["E", "F", "I", "W", "UP", "B", "SIM"]
54
+
55
+ [tool.mypy]
56
+ python_version = "3.12"
57
+ strict = true
58
+
59
+ [tool.pytest.ini_options]
60
+ testpaths = ["tests"]
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -0,0 +1,3 @@
1
+ from kctl_glpi.cli import _run
2
+
3
+ _run()
@@ -0,0 +1,138 @@
1
+ """Main CLI entry point for kctl-glpi."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from kctl_lib import KctlError, handle_cli_error
9
+ from kctl_lib.self_update import notify_if_outdated
10
+ from kctl_lib.tui import add_tui_command
11
+
12
+ from kctl_glpi import __version__
13
+ from kctl_glpi.commands.assets import app as assets_app
14
+ from kctl_glpi.commands.config_cmd import app as config_app
15
+ from kctl_glpi.commands.dashboard import app as dashboard_app
16
+ from kctl_glpi.commands.doctor_cmd import app as doctor_app
17
+ from kctl_glpi.commands.entities import app as entities_app
18
+ from kctl_glpi.commands.health import app as health_app
19
+ from kctl_glpi.commands.plugins_cmd import app as plugins_app
20
+ from kctl_glpi.commands.search_cmd import app as search_app
21
+ from kctl_glpi.commands.skill_cmd import app as skill_app
22
+ from kctl_glpi.commands.tickets import app as tickets_app
23
+ from kctl_glpi.commands.users import app as users_app
24
+ from kctl_glpi.core.callbacks import AppContext
25
+ from kctl_glpi.core.plugins import discover_and_load_plugins
26
+
27
+
28
+ def version_callback(value: bool) -> None:
29
+ if value:
30
+ typer.echo(f"kctl-glpi {__version__}")
31
+ raise typer.Exit()
32
+
33
+
34
+ app = typer.Typer(
35
+ name="kctl-glpi",
36
+ help="Kodemeio GLPI CLI - manage your GLPI IT Service Management platform.",
37
+ no_args_is_help=True,
38
+ rich_markup_mode="rich",
39
+ pretty_exceptions_enable=False,
40
+ )
41
+
42
+
43
+ @app.callback()
44
+ def main(
45
+ ctx: typer.Context,
46
+ json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
47
+ quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
48
+ profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile name")] = None,
49
+ format: Annotated[str, typer.Option("--format", "-f", help="Output format: pretty, json, csv, yaml")] = "pretty",
50
+ no_header: Annotated[bool, typer.Option("--no-header", help="Omit header row in CSV output")] = False,
51
+ url: Annotated[str | None, typer.Option("--url", help="API URL override")] = None,
52
+ app_token: Annotated[str | None, typer.Option("--app-token", help="App-Token override")] = None,
53
+ user_token: Annotated[str | None, typer.Option("--user-token", help="User token override")] = None,
54
+ version: Annotated[
55
+ bool, typer.Option("--version", "-V", callback=version_callback, is_eager=True, help="Show version")
56
+ ] = False,
57
+ ) -> None:
58
+ """Kodemeio GLPI CLI."""
59
+ ctx.ensure_object(dict)
60
+ ctx.obj = AppContext(
61
+ json_mode=json_output,
62
+ quiet=quiet,
63
+ profile=profile,
64
+ format=format,
65
+ no_header=no_header,
66
+ url_override=url,
67
+ app_token_override=app_token,
68
+ user_token_override=user_token,
69
+ )
70
+ notify_if_outdated(ctx.obj.output, "kctl-glpi", __version__)
71
+
72
+
73
+ # Register all command groups
74
+ app.add_typer(tickets_app, name="tickets")
75
+ app.add_typer(assets_app, name="assets")
76
+ app.add_typer(users_app, name="users")
77
+ app.add_typer(entities_app, name="entities")
78
+ app.add_typer(search_app, name="search")
79
+ app.add_typer(plugins_app, name="plugins")
80
+ app.add_typer(health_app, name="health")
81
+ app.add_typer(dashboard_app, name="dashboard")
82
+ app.add_typer(config_app, name="config")
83
+ app.add_typer(doctor_app, name="doctor")
84
+ app.add_typer(skill_app, name="skill", hidden=True)
85
+
86
+ # Load third-party plugins via entry points
87
+ discover_and_load_plugins(app)
88
+ add_tui_command(app, service_key="glpi", version=__version__)
89
+
90
+
91
+ @app.command("self-update")
92
+ def self_update_cmd(ctx: typer.Context) -> None:
93
+ """Check for updates and upgrade kctl-glpi."""
94
+ actx = ctx.obj
95
+ out = actx.output
96
+
97
+ from kctl_lib.self_update import check_update
98
+ from kctl_lib.self_update import update as do_update
99
+
100
+ latest = check_update("kctl-glpi", __version__)
101
+ if latest:
102
+ out.info(f"Updating to {latest}...")
103
+ do_update("kctl-glpi")
104
+ out.success(f"Updated to {latest}")
105
+ else:
106
+ out.success("Already up to date")
107
+
108
+
109
+ @app.command()
110
+ def completions(
111
+ shell: Annotated[str, typer.Argument(help="Shell type: zsh, bash, fish")] = "zsh",
112
+ install: Annotated[bool, typer.Option("--install", help="Install completions")] = False,
113
+ ) -> None:
114
+ """Generate or install shell completions."""
115
+ from kctl_lib.completions import get_completion_script, install_completions
116
+
117
+ if install:
118
+ path = install_completions("kctl-glpi", shell)
119
+ if path:
120
+ typer.echo(f"Completions installed to {path}")
121
+ else:
122
+ typer.echo(f"Could not install completions for {shell}", err=True)
123
+ raise typer.Exit(code=1)
124
+ else:
125
+ script = get_completion_script("kctl-glpi", shell)
126
+ typer.echo(script)
127
+
128
+
129
+ def _run() -> None:
130
+ """Entry point with error handling."""
131
+ try:
132
+ app()
133
+ except KctlError as e:
134
+ handle_cli_error(e)
135
+
136
+
137
+ if __name__ == "__main__":
138
+ _run()
File without changes
@@ -0,0 +1,220 @@
1
+ """Asset management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from kctl_lib.exceptions import KctlError
9
+
10
+ from kctl_glpi.core.callbacks import AppContext
11
+
12
+ app = typer.Typer(help="Manage GLPI assets (computers).")
13
+
14
+
15
+ @app.command("list")
16
+ def list_assets(
17
+ ctx: typer.Context,
18
+ limit: Annotated[int, typer.Option("--limit", "-l", help="Max results")] = 50,
19
+ name_filter: Annotated[str | None, typer.Option("--name", "-n", help="Filter by name (contains)")] = None,
20
+ ) -> None:
21
+ """List computers/assets with optional filters."""
22
+ actx: AppContext = ctx.obj
23
+ c, out = actx.client, actx.output
24
+
25
+ criteria: list[dict] = []
26
+ if name_filter:
27
+ criteria.append({"field": 1, "searchtype": "contains", "value": name_filter})
28
+
29
+ try:
30
+ result = c.search("Computer", criteria=criteria or None, params={"range": f"0-{limit - 1}"})
31
+ assets = result.get("data", [])
32
+ except KctlError as e:
33
+ out.error(f"Failed to list assets: {e}")
34
+ raise typer.Exit(1) from e
35
+
36
+ if not assets:
37
+ out.info("No assets found")
38
+ return
39
+
40
+ rows: list[list[str]] = []
41
+ for a in assets:
42
+ rows.append(
43
+ [
44
+ str(a.get("2", a.get("id", ""))), # ID
45
+ str(a.get("1", "")), # Name
46
+ str(a.get("4", "")), # Type
47
+ str(a.get("5", "")), # Serial
48
+ str(a.get("31", "")), # Status
49
+ str(a.get("70", "")), # User
50
+ ]
51
+ )
52
+
53
+ out.table(
54
+ f"Computers ({len(assets)})",
55
+ [("ID", "cyan"), ("Name", ""), ("Type", ""), ("Serial", "dim"), ("Status", ""), ("User", "")],
56
+ rows,
57
+ data_for_json=assets,
58
+ )
59
+
60
+
61
+ @app.command()
62
+ def get(
63
+ ctx: typer.Context,
64
+ asset_id: Annotated[int, typer.Argument(help="Computer/asset ID")],
65
+ ) -> None:
66
+ """Get asset details by ID."""
67
+ actx: AppContext = ctx.obj
68
+ c, out = actx.client, actx.output
69
+
70
+ try:
71
+ asset = c.get_item("Computer", asset_id)
72
+ except KctlError as e:
73
+ out.error(f"Failed to get asset {asset_id}: {e}")
74
+ raise typer.Exit(1) from e
75
+
76
+ if not asset:
77
+ out.error(f"Asset {asset_id} not found")
78
+ raise typer.Exit(1)
79
+
80
+ sections: list[tuple[str, list[tuple[str, str]]]] = [
81
+ (
82
+ f"Computer #{asset.get('id', asset_id)}",
83
+ [
84
+ ("Name", asset.get("name", "")),
85
+ ("Serial", asset.get("serial", "")),
86
+ ("OTH Number", asset.get("otherserial", "")),
87
+ ("Type", str(asset.get("computertypes_id", ""))),
88
+ ("Model", str(asset.get("computermodels_id", ""))),
89
+ ("Manufacturer", str(asset.get("manufacturers_id", ""))),
90
+ ("Status", str(asset.get("states_id", ""))),
91
+ ("Location", str(asset.get("locations_id", ""))),
92
+ ("User", str(asset.get("users_id", ""))),
93
+ ("Group", str(asset.get("groups_id", ""))),
94
+ ("Entity", str(asset.get("entities_id", ""))),
95
+ ("Date Created", asset.get("date_creation", "")),
96
+ ("Date Modified", asset.get("date_mod", "")),
97
+ ("Comment", (asset.get("comment", "") or "")[:300]),
98
+ ],
99
+ )
100
+ ]
101
+
102
+ out.detail(f"Computer #{asset_id}", sections, data_for_json=asset)
103
+
104
+
105
+ @app.command()
106
+ def create(
107
+ ctx: typer.Context,
108
+ name: Annotated[str, typer.Option("--name", "-n", help="Computer name")],
109
+ serial: Annotated[str | None, typer.Option("--serial", "-s", help="Serial number")] = None,
110
+ computer_type: Annotated[int | None, typer.Option("--type", help="Computer type ID")] = None,
111
+ status: Annotated[int | None, typer.Option("--status", help="Status ID")] = None,
112
+ location: Annotated[int | None, typer.Option("--location", help="Location ID")] = None,
113
+ user: Annotated[int | None, typer.Option("--user", help="User ID")] = None,
114
+ comment: Annotated[str | None, typer.Option("--comment", help="Comment")] = None,
115
+ ) -> None:
116
+ """Create a new computer/asset."""
117
+ actx: AppContext = ctx.obj
118
+ c, out = actx.client, actx.output
119
+
120
+ data: dict = {"name": name}
121
+ if serial is not None:
122
+ data["serial"] = serial
123
+ if computer_type is not None:
124
+ data["computertypes_id"] = computer_type
125
+ if status is not None:
126
+ data["states_id"] = status
127
+ if location is not None:
128
+ data["locations_id"] = location
129
+ if user is not None:
130
+ data["users_id"] = user
131
+ if comment is not None:
132
+ data["comment"] = comment
133
+
134
+ try:
135
+ result = c.create_item("Computer", data)
136
+ out.success(f"Computer created: {result}")
137
+ except KctlError as e:
138
+ out.error(f"Failed to create computer: {e}")
139
+ raise typer.Exit(1) from e
140
+
141
+ if out.json_mode:
142
+ out.raw_json(result)
143
+
144
+
145
+ @app.command()
146
+ def update(
147
+ ctx: typer.Context,
148
+ asset_id: Annotated[int, typer.Argument(help="Computer/asset ID")],
149
+ name: Annotated[str | None, typer.Option("--name", "-n", help="New name")] = None,
150
+ serial: Annotated[str | None, typer.Option("--serial", "-s", help="New serial number")] = None,
151
+ status: Annotated[int | None, typer.Option("--status", help="New status ID")] = None,
152
+ location: Annotated[int | None, typer.Option("--location", help="New location ID")] = None,
153
+ user: Annotated[int | None, typer.Option("--user", help="New user ID")] = None,
154
+ comment: Annotated[str | None, typer.Option("--comment", help="New comment")] = None,
155
+ ) -> None:
156
+ """Update a computer/asset."""
157
+ actx: AppContext = ctx.obj
158
+ c, out = actx.client, actx.output
159
+
160
+ data: dict = {}
161
+ if name is not None:
162
+ data["name"] = name
163
+ if serial is not None:
164
+ data["serial"] = serial
165
+ if status is not None:
166
+ data["states_id"] = status
167
+ if location is not None:
168
+ data["locations_id"] = location
169
+ if user is not None:
170
+ data["users_id"] = user
171
+ if comment is not None:
172
+ data["comment"] = comment
173
+
174
+ if not data:
175
+ out.error("No fields to update. Use --name, --serial, --status, etc.")
176
+ raise typer.Exit(1)
177
+
178
+ try:
179
+ result = c.update_item("Computer", asset_id, data)
180
+ out.success(f"Computer {asset_id} updated")
181
+ except KctlError as e:
182
+ out.error(f"Failed to update computer {asset_id}: {e}")
183
+ raise typer.Exit(1) from e
184
+
185
+ if out.json_mode:
186
+ out.raw_json(result)
187
+
188
+
189
+ @app.command()
190
+ def types(ctx: typer.Context) -> None:
191
+ """List available computer types."""
192
+ actx: AppContext = ctx.obj
193
+ c, out = actx.client, actx.output
194
+
195
+ try:
196
+ items = c.get_items("ComputerType")
197
+ except KctlError as e:
198
+ out.error(f"Failed to list computer types: {e}")
199
+ raise typer.Exit(1) from e
200
+
201
+ if not items:
202
+ out.info("No computer types found")
203
+ return
204
+
205
+ rows: list[list[str]] = []
206
+ for item in items:
207
+ rows.append(
208
+ [
209
+ str(item.get("id", "")),
210
+ item.get("name", ""),
211
+ item.get("comment", "") or "",
212
+ ]
213
+ )
214
+
215
+ out.table(
216
+ "Computer Types",
217
+ [("ID", "cyan"), ("Name", ""), ("Comment", "dim")],
218
+ rows,
219
+ data_for_json=items,
220
+ )