makerrepo-cli 0.1.0__py3-none-any.whl
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.
- makerrepo_cli/__init__.py +0 -0
- makerrepo_cli/cmds/__init__.py +0 -0
- makerrepo_cli/cmds/aliase.py +26 -0
- makerrepo_cli/cmds/artifacts/__init__.py +0 -0
- makerrepo_cli/cmds/artifacts/capture_image.py +231 -0
- makerrepo_cli/cmds/artifacts/cli.py +11 -0
- makerrepo_cli/cmds/artifacts/main.py +157 -0
- makerrepo_cli/cmds/artifacts/ocp_data_types.py +72 -0
- makerrepo_cli/cmds/artifacts/utils.py +31 -0
- makerrepo_cli/cmds/cli.py +57 -0
- makerrepo_cli/cmds/environment.py +33 -0
- makerrepo_cli/cmds/main.py +7 -0
- makerrepo_cli-0.1.0.dist-info/METADATA +17 -0
- makerrepo_cli-0.1.0.dist-info/RECORD +18 -0
- makerrepo_cli-0.1.0.dist-info/WHEEL +5 -0
- makerrepo_cli-0.1.0.dist-info/entry_points.txt +3 -0
- makerrepo_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- makerrepo_cli-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from click.core import Context
|
|
3
|
+
|
|
4
|
+
CMD_ALIAS_MAP = {
|
|
5
|
+
"ls": "list",
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AliasedGroup(click.Group):
|
|
10
|
+
def get_command(self, ctx: Context, cmd_name: str):
|
|
11
|
+
if cmd_name in CMD_ALIAS_MAP:
|
|
12
|
+
cmd_name = CMD_ALIAS_MAP[cmd_name]
|
|
13
|
+
rv = click.Group.get_command(self, ctx, cmd_name)
|
|
14
|
+
if rv is not None:
|
|
15
|
+
return rv
|
|
16
|
+
matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)]
|
|
17
|
+
if not matches:
|
|
18
|
+
return None
|
|
19
|
+
elif len(matches) == 1:
|
|
20
|
+
return click.Group.get_command(self, ctx, matches[0])
|
|
21
|
+
ctx.fail(f"Too many matches: {', '.join(sorted(matches))}")
|
|
22
|
+
|
|
23
|
+
def resolve_command(self, ctx: Context, args: list[str]):
|
|
24
|
+
# always return the full command name
|
|
25
|
+
_, cmd, args = super().resolve_command(ctx, args)
|
|
26
|
+
return cmd.name, cmd, args
|
|
File without changes
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import base64
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import pathlib
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from playwright.async_api import async_playwright
|
|
10
|
+
from playwright.async_api import Browser
|
|
11
|
+
from playwright.async_api import BrowserContext
|
|
12
|
+
from playwright.async_api import Page
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
DATA_FOLDER = pathlib.Path(__file__).parent.parent.parent / "data"
|
|
16
|
+
DEFAULT_CONFIG = {
|
|
17
|
+
# Display options
|
|
18
|
+
"cadWidth": 1200,
|
|
19
|
+
"height": 1200,
|
|
20
|
+
"treeWidth": 0,
|
|
21
|
+
"theme": "light",
|
|
22
|
+
"glass": True,
|
|
23
|
+
"tools": False,
|
|
24
|
+
# Render options
|
|
25
|
+
"ambientIntensity": 1.0,
|
|
26
|
+
"directIntensity": 1.1,
|
|
27
|
+
"metalness": 0.3,
|
|
28
|
+
"roughness": 0.65,
|
|
29
|
+
"edgeColor": 0x707070,
|
|
30
|
+
# Viewer options
|
|
31
|
+
"ortho": True,
|
|
32
|
+
"control": "trackball",
|
|
33
|
+
"up": "Z",
|
|
34
|
+
}
|
|
35
|
+
DEFAULT_ARGS = (
|
|
36
|
+
"--no-sandbox",
|
|
37
|
+
"--disable-setuid-sandbox",
|
|
38
|
+
"--disable-dev-shm-usage",
|
|
39
|
+
"--use-gl=angle",
|
|
40
|
+
"--use-angle=swiftshader",
|
|
41
|
+
"--enable-webgl",
|
|
42
|
+
"--ignore-gpu-blocklist",
|
|
43
|
+
"--enable-unsafe-swiftshader",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CADViewerService:
|
|
48
|
+
"""Service class for loading and interacting with CAD viewer HTML using Playwright."""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
data_dir: pathlib.Path | None = None,
|
|
53
|
+
chrome_executable_path: pathlib.Path | None = None,
|
|
54
|
+
args: tuple[str, ...] | None = None,
|
|
55
|
+
logger: logging.Logger | None = None,
|
|
56
|
+
):
|
|
57
|
+
if data_dir is None:
|
|
58
|
+
data_dir = DATA_FOLDER
|
|
59
|
+
self.data_dir = pathlib.Path(data_dir)
|
|
60
|
+
self.chrome_executable_path = chrome_executable_path
|
|
61
|
+
if self.chrome_executable_path is None:
|
|
62
|
+
self.chrome_executable_path = os.environ.get(
|
|
63
|
+
"PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH"
|
|
64
|
+
)
|
|
65
|
+
self.args = args
|
|
66
|
+
if self.args is None:
|
|
67
|
+
self.args = DEFAULT_ARGS
|
|
68
|
+
self.html_file = self.data_dir / "cad_viewer.html"
|
|
69
|
+
self.browser: Browser | None = None
|
|
70
|
+
self.context: BrowserContext | None = None
|
|
71
|
+
self.page: Page | None = None
|
|
72
|
+
self._playwright = None
|
|
73
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
74
|
+
|
|
75
|
+
async def start(self):
|
|
76
|
+
"""Start the Playwright browser and load the HTML file."""
|
|
77
|
+
self._playwright = await async_playwright().start()
|
|
78
|
+
self.logger.info(
|
|
79
|
+
"Starting CAD viewer service with executable_path=%s, args=%s",
|
|
80
|
+
self.chrome_executable_path,
|
|
81
|
+
self.args,
|
|
82
|
+
)
|
|
83
|
+
self.browser = await self._playwright.chromium.launch(
|
|
84
|
+
headless=True,
|
|
85
|
+
args=self.args,
|
|
86
|
+
executable_path=self.chrome_executable_path,
|
|
87
|
+
)
|
|
88
|
+
self.context = await self.browser.new_context()
|
|
89
|
+
self.page = await self.context.new_page()
|
|
90
|
+
|
|
91
|
+
# Set up message handler for postMessage from the page
|
|
92
|
+
async def handle_console(msg):
|
|
93
|
+
self.logger.info("Console: %s", msg)
|
|
94
|
+
|
|
95
|
+
self.page.on("console", handle_console)
|
|
96
|
+
|
|
97
|
+
# Load the HTML file
|
|
98
|
+
html_path = f"file://{self.html_file.absolute()}"
|
|
99
|
+
await self.page.goto(html_path, wait_until="networkidle")
|
|
100
|
+
|
|
101
|
+
async def load_cad_data(
|
|
102
|
+
self, data: dict[str, Any], config: dict[str, Any] | None = None
|
|
103
|
+
):
|
|
104
|
+
"""
|
|
105
|
+
Load CAD model data into the viewer.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
data: CAD model data dictionary
|
|
109
|
+
config: Optional configuration dictionary
|
|
110
|
+
"""
|
|
111
|
+
if self.page is None:
|
|
112
|
+
raise RuntimeError("Service not started. Call start() first.")
|
|
113
|
+
|
|
114
|
+
if config is None:
|
|
115
|
+
config = DEFAULT_CONFIG
|
|
116
|
+
|
|
117
|
+
message = {
|
|
118
|
+
"model": data["data"],
|
|
119
|
+
"config": config,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await self.page.evaluate(
|
|
123
|
+
"""(data) => {
|
|
124
|
+
if (window.viewer && window.viewer.clear) {
|
|
125
|
+
window.viewer.clear();
|
|
126
|
+
}
|
|
127
|
+
window.loadModel(data);
|
|
128
|
+
}""",
|
|
129
|
+
message,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Wait for viewer to be ready and data to be processed
|
|
133
|
+
await asyncio.sleep(1.0)
|
|
134
|
+
await self.page.wait_for_function(
|
|
135
|
+
"window.viewer !== null && window.viewer !== undefined",
|
|
136
|
+
timeout=5 * 1_000,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def take_screenshot(self) -> bytes:
|
|
140
|
+
"""
|
|
141
|
+
Take a screenshot of the CAD viewer.
|
|
142
|
+
|
|
143
|
+
"""
|
|
144
|
+
if self.page is None:
|
|
145
|
+
raise RuntimeError("Service not started. Call start() first.")
|
|
146
|
+
|
|
147
|
+
task_id = uuid.uuid4().hex
|
|
148
|
+
# Call getImage directly and wait for the promise to resolve
|
|
149
|
+
result = await self.page.evaluate(
|
|
150
|
+
"""
|
|
151
|
+
async (filename) => {
|
|
152
|
+
if (!window.getImage) {
|
|
153
|
+
throw new Error('getImage function not available');
|
|
154
|
+
}
|
|
155
|
+
return await window.getImage(filename);
|
|
156
|
+
}
|
|
157
|
+
""",
|
|
158
|
+
task_id,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if not isinstance(result, dict):
|
|
162
|
+
raise RuntimeError(f"Invalid screenshot response type: {type(result)}")
|
|
163
|
+
|
|
164
|
+
if "dataUrl" not in result:
|
|
165
|
+
raise RuntimeError(f"Unexpected screenshot response: {result}")
|
|
166
|
+
|
|
167
|
+
data_url = result["dataUrl"]
|
|
168
|
+
if not data_url.startswith("data:image"):
|
|
169
|
+
raise RuntimeError(f"Invalid screenshot data format: {data_url[:50]}...")
|
|
170
|
+
|
|
171
|
+
base64_data = data_url.split(",")[1]
|
|
172
|
+
return base64.b64decode(base64_data)
|
|
173
|
+
|
|
174
|
+
async def stop(self):
|
|
175
|
+
"""Stop the browser and clean up resources."""
|
|
176
|
+
if self.page:
|
|
177
|
+
await self.page.close()
|
|
178
|
+
if self.context:
|
|
179
|
+
await self.context.close()
|
|
180
|
+
if self.browser:
|
|
181
|
+
await self.browser.close()
|
|
182
|
+
if self._playwright:
|
|
183
|
+
await self._playwright.stop()
|
|
184
|
+
|
|
185
|
+
self.page = None
|
|
186
|
+
self.context = None
|
|
187
|
+
self.browser = None
|
|
188
|
+
self._playwright = None
|
|
189
|
+
|
|
190
|
+
async def __aenter__(self):
|
|
191
|
+
"""Async context manager entry."""
|
|
192
|
+
await self.start()
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
196
|
+
"""Async context manager exit."""
|
|
197
|
+
await self.stop()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async def main():
|
|
201
|
+
"""Download model file and take a screenshot."""
|
|
202
|
+
import httpx
|
|
203
|
+
|
|
204
|
+
logger = logging.getLogger(__name__)
|
|
205
|
+
model_url = "https://makerrepo.com/r/fangpenlin/open-models/artifact/master/5081ffa2-d61f-4925-87e3-573ec291b53c/model.json"
|
|
206
|
+
output_path = pathlib.Path("model_screenshot.png")
|
|
207
|
+
|
|
208
|
+
logger.info("Downloading model from %s...", model_url)
|
|
209
|
+
async with httpx.AsyncClient(follow_redirects=True) as client:
|
|
210
|
+
response = await client.get(model_url)
|
|
211
|
+
response.raise_for_status()
|
|
212
|
+
model_data = response.json()
|
|
213
|
+
|
|
214
|
+
logger.info("Model downloaded successfully")
|
|
215
|
+
logger.info("Starting CAD viewer service...")
|
|
216
|
+
|
|
217
|
+
async with CADViewerService(logger=logger) as viewer:
|
|
218
|
+
logger.info("Loading CAD model data...")
|
|
219
|
+
await viewer.load_cad_data(model_data)
|
|
220
|
+
|
|
221
|
+
logger.info("Taking screenshot...")
|
|
222
|
+
screenshot_bytes = await viewer.take_screenshot()
|
|
223
|
+
|
|
224
|
+
# Save screenshot to file
|
|
225
|
+
output_path.write_bytes(screenshot_bytes)
|
|
226
|
+
logger.info("Screenshot saved to %s", output_path.absolute())
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
logging.basicConfig(level=logging.INFO)
|
|
231
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import pathlib
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
import rich
|
|
6
|
+
from mr.artifacts.registry import collect
|
|
7
|
+
from mr.artifacts.registry import Registry
|
|
8
|
+
from mr.artifacts.utils import load_module
|
|
9
|
+
from rich import box
|
|
10
|
+
from rich.markup import escape
|
|
11
|
+
from rich.padding import Padding
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from ..environment import Environment
|
|
15
|
+
from ..environment import pass_env
|
|
16
|
+
from .cli import cli
|
|
17
|
+
from .utils import convert
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
TABLE_HEADER_STYLE = "yellow"
|
|
21
|
+
TABLE_COLUMN_STYLE = "cyan"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def collect_artifacts(module_spec: str) -> Registry:
|
|
25
|
+
registry = collect([load_module(module_spec)])
|
|
26
|
+
if not registry.artifacts:
|
|
27
|
+
logger.error("No artifacts found")
|
|
28
|
+
return registry
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@cli.command(name="list", help="List artifacts")
|
|
32
|
+
@click.argument("MODULE")
|
|
33
|
+
@pass_env
|
|
34
|
+
def list_artifacts(env: Environment, module: str):
|
|
35
|
+
registry = collect_artifacts(module)
|
|
36
|
+
|
|
37
|
+
env.logger.info(
|
|
38
|
+
"Listing artifacts for [blue]%s[/]",
|
|
39
|
+
module,
|
|
40
|
+
extra={"markup": True, "highlighter": None},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
table = Table(
|
|
44
|
+
title="Artifacts",
|
|
45
|
+
box=box.SIMPLE,
|
|
46
|
+
header_style=TABLE_HEADER_STYLE,
|
|
47
|
+
expand=True,
|
|
48
|
+
)
|
|
49
|
+
table.add_column("Module", style=TABLE_COLUMN_STYLE)
|
|
50
|
+
table.add_column("Name", style=TABLE_COLUMN_STYLE)
|
|
51
|
+
table.add_column("Sample", style=TABLE_COLUMN_STYLE)
|
|
52
|
+
for module, artifacts in registry.artifacts.items():
|
|
53
|
+
for i, (name, artifact) in enumerate(artifacts.items()):
|
|
54
|
+
table.add_row(
|
|
55
|
+
escape(module) if i == 0 else "", escape(name), str(artifact.sample)
|
|
56
|
+
)
|
|
57
|
+
rich.print(Padding(table, (1, 0, 0, 4)))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@cli.command(help="View artifact")
|
|
61
|
+
@click.argument("MODULE")
|
|
62
|
+
@click.argument("ARTIFACTS", nargs=-1)
|
|
63
|
+
@click.option(
|
|
64
|
+
"-p", "--port", help="OCP Viewer port to send the model data to", default=3939
|
|
65
|
+
)
|
|
66
|
+
@pass_env
|
|
67
|
+
def view(env: Environment, module: str, artifacts: tuple[str, ...], port: int):
|
|
68
|
+
registry = collect_artifacts(module)
|
|
69
|
+
if not artifacts:
|
|
70
|
+
target_artifact = list(list(registry.artifacts.values())[0].values())[0]
|
|
71
|
+
logger.info(
|
|
72
|
+
"No artifacts provided, use the first one %s/%s",
|
|
73
|
+
target_artifact.module,
|
|
74
|
+
target_artifact.name,
|
|
75
|
+
)
|
|
76
|
+
target_artifacts = [target_artifact]
|
|
77
|
+
else:
|
|
78
|
+
if len(registry.artifacts) > 1:
|
|
79
|
+
raise ValueError("Unexpected more than one modules found")
|
|
80
|
+
module_artifacts = list(registry.artifacts.values())[0]
|
|
81
|
+
target_artifacts = [
|
|
82
|
+
module_artifacts[artifact_name] for artifact_name in artifacts
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
# TODO: this is going to be a bit slow, provide a progress bar & cache?
|
|
86
|
+
realized_artifacts = [artifact.func() for artifact in target_artifacts]
|
|
87
|
+
# defer the import to make testing mocking much easier
|
|
88
|
+
from ocp_vscode import show
|
|
89
|
+
|
|
90
|
+
# TODO: pass in some args
|
|
91
|
+
show(realized_artifacts, port=port)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@cli.command(help="Export artifact")
|
|
95
|
+
@click.argument("MODULE")
|
|
96
|
+
@pass_env
|
|
97
|
+
def export(env: Environment, module: str):
|
|
98
|
+
registry = collect_artifacts(module)
|
|
99
|
+
# TODO:
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@cli.command(name="snapshot", help="Capture a snapshot from artifacts")
|
|
103
|
+
@click.argument("MODULE")
|
|
104
|
+
@click.argument("ARTIFACTS", nargs=-1)
|
|
105
|
+
@click.option(
|
|
106
|
+
"-o",
|
|
107
|
+
"--output",
|
|
108
|
+
help="Output image file path",
|
|
109
|
+
default="snapshot.png",
|
|
110
|
+
type=click.Path(path_type=pathlib.Path),
|
|
111
|
+
)
|
|
112
|
+
@pass_env
|
|
113
|
+
def snapshot(
|
|
114
|
+
env: Environment, module: str, artifacts: tuple[str, ...], output: pathlib.Path
|
|
115
|
+
):
|
|
116
|
+
"""Capture a screenshot from artifacts."""
|
|
117
|
+
import asyncio
|
|
118
|
+
|
|
119
|
+
from .capture_image import CADViewerService
|
|
120
|
+
|
|
121
|
+
registry = collect_artifacts(module)
|
|
122
|
+
if not artifacts:
|
|
123
|
+
target_artifact = list(list(registry.artifacts.values())[0].values())[0]
|
|
124
|
+
env.logger.info(
|
|
125
|
+
"No artifacts provided, use the first one %s/%s",
|
|
126
|
+
target_artifact.module,
|
|
127
|
+
target_artifact.name,
|
|
128
|
+
)
|
|
129
|
+
target_artifacts = [target_artifact]
|
|
130
|
+
else:
|
|
131
|
+
if len(registry.artifacts) > 1:
|
|
132
|
+
raise ValueError("Unexpected more than one modules found")
|
|
133
|
+
module_artifacts = list(registry.artifacts.values())[0]
|
|
134
|
+
target_artifacts = [
|
|
135
|
+
module_artifacts[artifact_name] for artifact_name in artifacts
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
# Realize artifacts
|
|
139
|
+
realized_artifacts = [artifact.func() for artifact in target_artifacts]
|
|
140
|
+
|
|
141
|
+
# Convert to model data format using convert from utils
|
|
142
|
+
model_data = convert(realized_artifacts)
|
|
143
|
+
|
|
144
|
+
async def capture_snapshot():
|
|
145
|
+
env.logger.info("Starting CAD viewer service...")
|
|
146
|
+
async with CADViewerService(logger=env.logger) as viewer:
|
|
147
|
+
env.logger.info("Loading CAD model data...")
|
|
148
|
+
await viewer.load_cad_data(model_data.model_dump(mode="json"))
|
|
149
|
+
|
|
150
|
+
env.logger.info("Taking screenshot...")
|
|
151
|
+
screenshot_bytes = await viewer.take_screenshot()
|
|
152
|
+
|
|
153
|
+
# Save screenshot to file
|
|
154
|
+
output.write_bytes(screenshot_bytes)
|
|
155
|
+
env.logger.info("Screenshot saved to %s", output.absolute())
|
|
156
|
+
|
|
157
|
+
asyncio.run(capture_snapshot())
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Buffer(BaseModel):
|
|
7
|
+
shape: list[int]
|
|
8
|
+
dtype: str
|
|
9
|
+
buffer: str
|
|
10
|
+
codec: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Instance(BaseModel):
|
|
14
|
+
vertices: Buffer
|
|
15
|
+
triangles: Buffer
|
|
16
|
+
normals: Buffer
|
|
17
|
+
edges: Buffer
|
|
18
|
+
obj_vertices: Buffer
|
|
19
|
+
face_types: Buffer
|
|
20
|
+
edge_types: Buffer
|
|
21
|
+
triangles_per_face: Buffer
|
|
22
|
+
segments_per_edge: Buffer
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BoundingBox(BaseModel):
|
|
26
|
+
xmin: float
|
|
27
|
+
xmax: float
|
|
28
|
+
ymin: float
|
|
29
|
+
ymax: float
|
|
30
|
+
zmin: float
|
|
31
|
+
zmax: float
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ShapeRef(BaseModel):
|
|
35
|
+
ref: int
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Part(BaseModel):
|
|
39
|
+
id: str
|
|
40
|
+
type: str
|
|
41
|
+
subtype: str
|
|
42
|
+
name: str
|
|
43
|
+
shape: ShapeRef
|
|
44
|
+
state: list[int]
|
|
45
|
+
color: str
|
|
46
|
+
alpha: float
|
|
47
|
+
texture: Any | None = None
|
|
48
|
+
loc: list[list[float]]
|
|
49
|
+
renderback: bool
|
|
50
|
+
accuracy: Any | None = None
|
|
51
|
+
bb: BoundingBox | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Shapes(BaseModel):
|
|
55
|
+
version: int
|
|
56
|
+
parts: list[Part]
|
|
57
|
+
loc: list[list[float]]
|
|
58
|
+
name: str
|
|
59
|
+
id: str
|
|
60
|
+
normal_len: int
|
|
61
|
+
bb: BoundingBox
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class OcpData(BaseModel):
|
|
65
|
+
instances: list[Instance]
|
|
66
|
+
shapes: Shapes
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class OcpPayload(BaseModel):
|
|
70
|
+
data: OcpData
|
|
71
|
+
type: str
|
|
72
|
+
count: int
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from ocp_tessellate import OcpGroup
|
|
2
|
+
from ocp_tessellate.convert import tessellate_group
|
|
3
|
+
from ocp_tessellate.convert import to_ocpgroup
|
|
4
|
+
from ocp_tessellate.utils import numpy_to_buffer_json
|
|
5
|
+
|
|
6
|
+
from .ocp_data_types import OcpData
|
|
7
|
+
from .ocp_data_types import OcpPayload
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def convert(*cad_objs, names=None) -> OcpPayload:
|
|
11
|
+
part_group, instances = to_ocpgroup(
|
|
12
|
+
*cad_objs,
|
|
13
|
+
names=names,
|
|
14
|
+
)
|
|
15
|
+
if len(part_group.objects) == 1 and isinstance(part_group.objects[0], OcpGroup):
|
|
16
|
+
loc = part_group.loc
|
|
17
|
+
part_group = part_group.objects[0]
|
|
18
|
+
part_group.loc = loc * part_group.loc
|
|
19
|
+
instances, shapes, mapping = tessellate_group(
|
|
20
|
+
group=part_group,
|
|
21
|
+
instances=instances,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
data = numpy_to_buffer_json(
|
|
25
|
+
dict(instances=instances, shapes=shapes),
|
|
26
|
+
)
|
|
27
|
+
return OcpPayload(
|
|
28
|
+
data=OcpData.model_validate(data),
|
|
29
|
+
type="data",
|
|
30
|
+
count=part_group.count_shapes(),
|
|
31
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from rich.logging import RichHandler
|
|
6
|
+
|
|
7
|
+
from .environment import Environment
|
|
8
|
+
from .environment import LOG_LEVEL_MAP
|
|
9
|
+
from .environment import LogLevel
|
|
10
|
+
from .environment import pass_env
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group(help="Command line tools for MakerRepo")
|
|
14
|
+
@click.option(
|
|
15
|
+
"-l",
|
|
16
|
+
"--log-level",
|
|
17
|
+
type=click.Choice(
|
|
18
|
+
list(map(lambda key: key.value, LOG_LEVEL_MAP.keys())), case_sensitive=False
|
|
19
|
+
),
|
|
20
|
+
default=lambda: os.environ.get("LOG_LEVEL", "INFO"),
|
|
21
|
+
)
|
|
22
|
+
@click.option(
|
|
23
|
+
"--log-format",
|
|
24
|
+
type=str,
|
|
25
|
+
default="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
26
|
+
help="logging format (only used when rich log is disabled)",
|
|
27
|
+
)
|
|
28
|
+
@click.option(
|
|
29
|
+
"--disable-rich-log",
|
|
30
|
+
is_flag=True,
|
|
31
|
+
help="disable rich log handler",
|
|
32
|
+
)
|
|
33
|
+
@click.version_option(prog_name="mr", package_name="mr")
|
|
34
|
+
@pass_env
|
|
35
|
+
def cli(
|
|
36
|
+
env: Environment,
|
|
37
|
+
log_level: str,
|
|
38
|
+
log_format: str,
|
|
39
|
+
disable_rich_log: bool,
|
|
40
|
+
):
|
|
41
|
+
env.log_level = LogLevel(log_level)
|
|
42
|
+
|
|
43
|
+
if disable_rich_log:
|
|
44
|
+
logging.basicConfig(
|
|
45
|
+
level=LOG_LEVEL_MAP[env.log_level],
|
|
46
|
+
format=log_format,
|
|
47
|
+
force=True,
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
FORMAT = "%(message)s"
|
|
51
|
+
logging.basicConfig(
|
|
52
|
+
level=LOG_LEVEL_MAP[env.log_level],
|
|
53
|
+
format=FORMAT,
|
|
54
|
+
datefmt="[%X]",
|
|
55
|
+
handlers=[RichHandler()],
|
|
56
|
+
force=True,
|
|
57
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import enum
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@enum.unique
|
|
9
|
+
class LogLevel(enum.Enum):
|
|
10
|
+
VERBOSE = "verbose"
|
|
11
|
+
DEBUG = "debug"
|
|
12
|
+
INFO = "info"
|
|
13
|
+
WARNING = "warning"
|
|
14
|
+
ERROR = "error"
|
|
15
|
+
FATAL = "fatal"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
LOG_LEVEL_MAP = {
|
|
19
|
+
LogLevel.DEBUG: logging.DEBUG,
|
|
20
|
+
LogLevel.INFO: logging.INFO,
|
|
21
|
+
LogLevel.WARNING: logging.WARNING,
|
|
22
|
+
LogLevel.ERROR: logging.ERROR,
|
|
23
|
+
LogLevel.FATAL: logging.FATAL,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclasses.dataclass
|
|
28
|
+
class Environment:
|
|
29
|
+
log_level: LogLevel = LogLevel.INFO
|
|
30
|
+
logger: logging.Logger = logging.getLogger("mr")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
pass_env = click.make_pass_decorator(Environment, ensure=True)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: makerrepo-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: click>=8.3.1
|
|
9
|
+
Requires-Dist: makerrepo>=0.2.0
|
|
10
|
+
Requires-Dist: ocp-vscode>=3.0.1
|
|
11
|
+
Requires-Dist: playwright>=1.58.0
|
|
12
|
+
Requires-Dist: rich>=14.3.2
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# makerrepo-cli
|
|
16
|
+
|
|
17
|
+
Command line tool for MakerRepo
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
makerrepo_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
makerrepo_cli/cmds/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
makerrepo_cli/cmds/aliase.py,sha256=OQ-E171TpBfRoX9D93DqBqXJkAWdtikprJsjK_UyXyA,863
|
|
4
|
+
makerrepo_cli/cmds/cli.py,sha256=iIGRynFXvebDIQ8_qGPc9tyMkUUvlIE9C_YtxABLhRg,1457
|
|
5
|
+
makerrepo_cli/cmds/environment.py,sha256=Bdg7bU7AoUBA_podtv7Vse2zxvq7QoFHEH184Fj7SfU,662
|
|
6
|
+
makerrepo_cli/cmds/main.py,sha256=mw9s-fWcQtobXUZdXJtbWVEvgx4-tdof3Cjb3o96ry0,120
|
|
7
|
+
makerrepo_cli/cmds/artifacts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
makerrepo_cli/cmds/artifacts/capture_image.py,sha256=Ioww0R6hmXGxpFFuZbXT-wLEdXtlBnDKXOVDWlM28kU,7172
|
|
9
|
+
makerrepo_cli/cmds/artifacts/cli.py,sha256=yutaVOJkT6NMMewYIcf-h9ytzKFGItXtHpcrrFABnro,202
|
|
10
|
+
makerrepo_cli/cmds/artifacts/main.py,sha256=rYO8M3ouhVC7n8RRYaC7CrZHQVsOGDUwMjzQK3AUQMo,5050
|
|
11
|
+
makerrepo_cli/cmds/artifacts/ocp_data_types.py,sha256=-YeAio5godIYcJPzoDIdu67MbqiJDMBkq8QcJJXQvMA,1175
|
|
12
|
+
makerrepo_cli/cmds/artifacts/utils.py,sha256=ZSLfm-S2uYk7Y7mq2fhK2Ki4OC2C6O0bNCZ5nDFLLEo,943
|
|
13
|
+
makerrepo_cli-0.1.0.dist-info/licenses/LICENSE,sha256=e1FWoucPP7MUG3AOBwb3R6PhN4N3KomjFLvu7CWar5Q,1072
|
|
14
|
+
makerrepo_cli-0.1.0.dist-info/METADATA,sha256=hM9A_2yXPgyOPBUr1CgrJHjdagGUWxWz9htm4-GwA6U,405
|
|
15
|
+
makerrepo_cli-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
16
|
+
makerrepo_cli-0.1.0.dist-info/entry_points.txt,sha256=Wcv5tkqI8NIiXZXJAZxTlVwy__9nT0ZfIktwNpPYjWE,95
|
|
17
|
+
makerrepo_cli-0.1.0.dist-info/top_level.txt,sha256=0sONY8MHgWTad63qvsjR1KXe7OOCwF66pDWm6iITrNM,14
|
|
18
|
+
makerrepo_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Launch Platform
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
makerrepo_cli
|