hip-cargo 0.0.2__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,672 @@
1
+ Metadata-Version: 2.3
2
+ Name: hip-cargo
3
+ Version: 0.0.2
4
+ Summary: Tools for generating Stimela cab definitions from Python functions
5
+ Keywords: stimela,typer,cli,yaml,code-generation,radio-astronomy
6
+ Author: landmanbester
7
+ Author-email: landmanbester <lbester@sarao.ac.za>
8
+ License: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Code Generators
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
20
+ Requires-Dist: typer>=0.12.0
21
+ Requires-Dist: pyyaml>=6.0
22
+ Requires-Dist: typing-extensions>=4.15.0
23
+ Requires-Python: >=3.10
24
+ Project-URL: Bug Tracker, https://github.com/landmanbester/hip-cargo/issues
25
+ Project-URL: Homepage, https://github.com/landmanbester/hip-cargo
26
+ Project-URL: Repository, https://github.com/landmanbester/hip-cargo
27
+ Description-Content-Type: text/markdown
28
+
29
+ # hip-cargo
30
+
31
+ A guide to designing auto-documenting CLI interfaces using Typer + conversion utilities.
32
+ If you are creating a new package the instructions below will guide you on how to structure it.
33
+ The `generate-function` utility is available to assist in converting an existing package to the `hip-cargo` format but there will be some manual steps involved.
34
+ The philosophy behind this design is to allow having a lightweight version of the package that only installs the bits required to generate `--help` from the CLI and the cab definitions that can then be used with `stimela`.
35
+ The full package should be available as a container image that can be used with `stimela`.
36
+ The image should be tagged with the package version so that `stimela` will automatically pull the image that matches the cab configuration.
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install hip-cargo
42
+ ```
43
+
44
+ Or for development:
45
+
46
+ ```bash
47
+ git clone https://github.com/landmanbester/hip-cargo.git
48
+ cd hip-cargo
49
+ uv sync
50
+ ```
51
+
52
+ ## Quick Start
53
+
54
+ ### 1. Decorate your Python CLI
55
+
56
+ Something like the following goes in `src/mypackage/cli/process.py`
57
+ ```python
58
+ import typer
59
+ from pathlib import Path
60
+ from typing import NewType
61
+ from typing_extensions import Annotated
62
+ from hip_cargo import stimela_cab, stimela_output
63
+
64
+ # custom types (stimela has e.g. File, URI, MS and Directory)
65
+ File = NewType("File", Path)
66
+ URI = NewType("URI", Path)
67
+ MS = NewType("MS", Path)
68
+ Directory = NewType("Directory", Path)
69
+
70
+ @stimela_cab(
71
+ name="my_processor",
72
+ info="Process data files",
73
+ )
74
+ @stimela_output(
75
+ name="output_file",
76
+ dtype="File",
77
+ info="{input_file}.processed",
78
+ required=True,
79
+ )
80
+ def process(
81
+ input_ms: Annotated[MS, typer.Argument(parser=MS, help="Input MS to process")], # note the parser=MS bit. This is required for non-standard types
82
+ output_dir: Annotated[Directory, typer.Option(parser=Directory, help="Output Directory for results")] = Path("./output"),
83
+ threshold: Annotated[float, typer.Option(help="Threshold value")] = 0.5,
84
+ ):
85
+ """
86
+ Process a data file.
87
+ """
88
+ # All your manual parameter wrangling here
89
+ from mypackage.core.process import process as process_core
90
+ return process_core(*args, **kwargs)
91
+ ```
92
+ Note that `*args` and `**kwargs` need to passed explicitly.
93
+ Then register the command in the `src/mypackage/cli/__init__.py` with something like the following
94
+ ```python
95
+ """Lightweight CLI for mypackage."""
96
+
97
+ import typer
98
+
99
+ app = typer.Typer(
100
+ name="mypackage",
101
+ help="Scientific computing package",
102
+ no_args_is_help=True,
103
+ )
104
+
105
+ # Register commands
106
+ from mypackage.cli.process import process
107
+
108
+ app.command(name="process")(process)
109
+
110
+ __all__ = ["app"]
111
+ ```
112
+ That's it, if you have something like the following
113
+ ```toml
114
+ [project.scripts]
115
+ mypackage = "mypackage.cli:app"
116
+ ```
117
+ in your `pyproject.toml` you should now be able to run
118
+ ```bash
119
+ app --help
120
+ ```
121
+ and
122
+ ```bash
123
+ app process --help
124
+ ```
125
+ from the command line and have a beautifully formatted CLI for your package.
126
+ Note that you can register multiple commands under `app`.
127
+
128
+ ### 2. Generate the Stimela cab definition
129
+
130
+ If you have the CLI definition you can convert it to a can using e.g.
131
+
132
+ ```bash
133
+ cargo generate-cab mypackage.process src/mypackage/cabs/process.yaml
134
+ ```
135
+
136
+ This should be automated using `scrips/generate_cabs.py`, but the above command is useful for testing.
137
+
138
+ ### 3. Generate Python function from existing cab (reverse)
139
+
140
+ If you are converting an existing package to the `hip-cargo` format there is a utility function available viz.
141
+
142
+ ```bash
143
+ cargo generate-function /path/to/existing_cab.yaml -o myfunction.py
144
+ ```
145
+
146
+ Currently, this won't add things like `rich_output_panel`, but it should help to get you started.
147
+ The program should recognize custom types and add the
148
+ ```
149
+ from pathlib import Path
150
+ from typing import NewType
151
+
152
+ MS = NewType("MS", Path)
153
+ ```
154
+ bit for you. It should also add the `parser=MS` in the `typer.Option()` bit for you.
155
+
156
+ ## Project Structure for hip-cargo Packages
157
+
158
+ Packages following the hip-cargo pattern should be structured to enable both lightweight cab definitions and full execution environments:
159
+
160
+ ```
161
+ my-scientific-package/
162
+ ├── src/
163
+ │ └── mypackage/
164
+ │ ├── __init__.py
165
+ │ ├── utils/ # Utilities used by core algorithms
166
+ │ │ ├── __init__.py
167
+ │ │ └── operator.py
168
+ │ ├── core/ # Core implementations with standard python type hints (no Annotated or custom types)
169
+ │ │ ├── __init__.py
170
+ │ │ ├── process.py
171
+ │ │ └── analyze.py
172
+ │ ├── cli/ # Lightweight CLI layer
173
+ │ │ ├── __init__.py # Main Typer app
174
+ │ │ ├── process.py # Individual commands
175
+ │ │ └── analyze.py
176
+ │ └── cabs/ # Generated cab definitions (inside mypackage)
177
+ │ ├── __init__.py
178
+ │ ├── process.yaml
179
+ │ └── analyze.yaml
180
+ ├── scripts/
181
+ │ └── generate_cabs.py # Automation script
182
+ ├── Dockerfile # For containerization
183
+ ├── pyproject.toml
184
+ └── README.md
185
+ ```
186
+
187
+ ### Key Principles
188
+
189
+ 1. **Separate CLI from implementation**: Keep CLI modules lightweight with lazy imports. Keep them all in the `src/mypackage/cli` directory and define the CLI for each command in a separate file. Construct the main Typer app in `src/mypackage/cli/__init__.py` and register commands there.
190
+ 2. **Separate cabs directory at same level as `cli`**: Use `hip-cargo` to auto-generate cabs into in `src/mypackage/cabs/` directory with the `generate_cabs.py` script. There should be a separate file for each cab.
191
+ 3. **Single app, multiple commands**: Use one Typer app that registers all commands. If you need a separate app you might as well create a separate repository for it.
192
+ 4. **Lazy imports**: Import heavy dependencies (NumPy, JAX, Dask) only when executing
193
+ 5. **Linked GitHub package with container image**: Maintain an up to date `Dockerfile` that installs the full package and use **Docker** (or **Podman**) to upload the image to the GitHub Container registry. Link this to your GitHub repository.
194
+
195
+ ### Example Structure
196
+
197
+ **`src/mypackage/cli/__init__.py`:**
198
+ ```python
199
+ """Lightweight CLI for mypackage."""
200
+
201
+ import typer
202
+
203
+ app = typer.Typer(
204
+ name="mypackage",
205
+ help="Scientific computing package",
206
+ no_args_is_help=True,
207
+ )
208
+
209
+ # Register commands
210
+ from mypackage.cli.process import process
211
+ from mypackage.cli.analyze import analyze
212
+
213
+ app.command(name="process")(process)
214
+ app.command(name="analyze")(analyze)
215
+
216
+ __all__ = ["app"]
217
+ ```
218
+
219
+ **`src/mypackage/cli/process.py`:**
220
+ ```python
221
+ """Process command - lightweight wrapper."""
222
+
223
+ from pathlib import Path
224
+ from typing import NewType
225
+ from typing_extensions import Annotated
226
+ import typer
227
+ from hip_cargo import stimela_cab, stimela_output
228
+
229
+ MS = NewType("MS", Path)
230
+
231
+ @stimela_cab(name="mypackage_process", info="Process data")
232
+ @stimela_output(name="output", dtype="File", info="{input_file}.out")
233
+ def process(
234
+ input_ms: Annotated[MS, typer.Argument(parser=MS, help="Input File")],
235
+ param: Annotated[float, typer.Option(help="Parameter")] = 1.0,
236
+ ):
237
+ """Process data files."""
238
+ # Lazy import - only loaded when executing
239
+ from mypackage.operators.core_algorithm import process_data
240
+
241
+ return process_data(input_file, param)
242
+ ```
243
+
244
+ **`pyproject.toml`:**
245
+ ```toml
246
+ [project]
247
+ name = "mypackage"
248
+ dependencies = [
249
+ "typer>=0.12.0",
250
+ "hip-cargo>=0.1.0",
251
+ ]
252
+
253
+ [project.optional-dependencies]
254
+ # Full scientific stack
255
+ full = [
256
+ "numpy>=1.24.0",
257
+ "jax>=0.4.0",
258
+ # ... heavy dependencies
259
+ ]
260
+
261
+ [project.scripts]
262
+ mypackage = "mypackage.cli:app"
263
+ ```
264
+
265
+ **`scripts/generate_cabs.py`:**
266
+ ```python
267
+ """Generate all cab definitions."""
268
+ import subprocess
269
+ from pathlib import Path
270
+
271
+ CLI_MODULES = [
272
+ "mypackage.cli.process",
273
+ "mypackage.cli.analyze",
274
+ ]
275
+
276
+ CABS_DIR = Path("src/mypackage/cabs")
277
+ CABS_DIR.mkdir(exist_ok=True)
278
+
279
+ for module in CLI_MODULES:
280
+ cmd_name = module.split(".")[-1]
281
+ output = CABS_DIR / f"{cmd_name}.yaml"
282
+
283
+ print(f"Generating {output}...")
284
+ subprocess.run([
285
+ "cargo", "generate-cab",
286
+ module,
287
+ str(output)
288
+ ], check=True)
289
+
290
+ print("✓ All cabs generated")
291
+ ```
292
+
293
+ ### Installation Modes
294
+
295
+ Users can install your package in different ways:
296
+
297
+ ```bash
298
+ # Lightweight (just CLI and cab definitions)
299
+ pip install mypackage
300
+
301
+ # Full (with all scientific dependencies)
302
+ pip install mypackage[full]
303
+
304
+ # Development
305
+ pip install -e "mypackage[full,dev]"
306
+ ```
307
+
308
+ ### Integration with cult-cargo
309
+
310
+ For integration with Stimela's cult-cargo:
311
+
312
+ 1. **Make cabs discoverable:**
313
+ ```python
314
+ # src/mypackage/cabs/__init__.py
315
+ from pathlib import Path
316
+
317
+ CAB_DIR = Path(__file__).parent
318
+ AVAILABLE_CABS = [p.stem for p in CAB_DIR.glob("*.yml")]
319
+
320
+ def get_cab_path(name: str) -> Path:
321
+ """Get path to a cab definition."""
322
+ return CAB_DIR / f"{name}.yml"
323
+ ```
324
+
325
+ 2. **cult-cargo imports lightweight version:**
326
+
327
+ We have to decide whether we want to add this kind of thing to `cult-cargo`:
328
+
329
+ ```toml
330
+ # In cult-cargo's pyproject.toml
331
+ [tool.poetry.dependencies]
332
+ mypackage = "^1.0.0" # Not mypackage[full]
333
+ ```
334
+
335
+ However, it should be possible to just
336
+ ```bash
337
+ uv pip install mypackage==x.x.x
338
+ ```
339
+ without any dependency conflicts. If not we have to think about ephemeral virtual environments.
340
+
341
+ 3. **Users run with Stimela:**
342
+ ```bash
343
+ # Native: requires full installation
344
+ pip install mypackage[full]
345
+ stimela run recipe.yml
346
+
347
+ # Singularity: uses container (lightweight install sufficient)
348
+ pip install mypackage
349
+ stimela run recipe.yml -S
350
+ ```
351
+
352
+ ## Container Images and GitHub Actions
353
+
354
+ For Stimela to use your package in containerized environments, you should publish OCI container images to GitHub Container Registry (ghcr.io). This section shows how to automate this with GitHub Actions.
355
+
356
+ ### 1. Create a Dockerfile
357
+
358
+ Add a `Dockerfile` at the root of your repository:
359
+
360
+ ```dockerfile
361
+ FROM python:3.11-slim
362
+
363
+ WORKDIR /app
364
+
365
+ # Install uv for fast package installation
366
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
367
+
368
+ # Copy package files
369
+ COPY pyproject.toml README.md ./
370
+ COPY src/ src/
371
+
372
+ # Install package with full dependencies using uv (much faster than pip)
373
+ RUN uv pip install --system --no-cache .
374
+
375
+ # Make CLI available
376
+ ENTRYPOINT ["mypackage"]
377
+ CMD ["--help"]
378
+ ```
379
+
380
+ ### 2. Set up GitHub Actions Workflow
381
+
382
+ Create `.github/workflows/publish-container.yml`:
383
+
384
+ ```yaml
385
+ name: Build and Publish Container
386
+
387
+ on:
388
+ push:
389
+ tags:
390
+ - 'v*.*.*' # Trigger on version tags (e.g., v1.0.0)
391
+ workflow_dispatch: # Allow manual triggering
392
+
393
+ env:
394
+ REGISTRY: ghcr.io
395
+ IMAGE_NAME: ${{ github.repository }}
396
+
397
+ jobs:
398
+ build-and-push:
399
+ runs-on: ubuntu-latest
400
+ permissions:
401
+ contents: read
402
+ packages: write
403
+
404
+ steps:
405
+ - name: Checkout repository
406
+ uses: actions/checkout@v5
407
+
408
+ - name: Log in to Container Registry
409
+ uses: docker/login-action@v3
410
+ with:
411
+ registry: ${{ env.REGISTRY }}
412
+ username: ${{ github.actor }}
413
+ password: ${{ secrets.GITHUB_TOKEN }}
414
+
415
+ - name: Extract metadata (tags, labels)
416
+ id: meta
417
+ uses: docker/metadata-action@v5
418
+ with:
419
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
420
+ tags: |
421
+ type=semver,pattern={{version}}
422
+ type=semver,pattern={{major}}.{{minor}}
423
+ type=semver,pattern={{major}}
424
+ type=sha,prefix={{branch}}-
425
+
426
+ - name: Build and push Docker image
427
+ uses: docker/build-push-action@v5
428
+ with:
429
+ context: .
430
+ push: true
431
+ tags: ${{ steps.meta.outputs.tags }}
432
+ labels: ${{ steps.meta.outputs.labels }}
433
+ ```
434
+
435
+ ### 3. Link Container to GitHub Package
436
+
437
+ To associate the container image with your repository:
438
+
439
+ 1. **Automatic linking**: If your workflow pushes to `ghcr.io/username/repository-name`, GitHub automatically creates a package linked to the repository.
440
+
441
+ 2. **Manual linking** (if needed):
442
+ - Go to your repository on GitHub
443
+ - Navigate to the "Packages" section
444
+ - Click on your container package
445
+ - Click "Connect repository" in the sidebar
446
+ - Select your repository from the dropdown
447
+
448
+ 3. **Set package visibility**:
449
+ - In the package settings, set visibility to "Public" for open-source projects
450
+ - This allows Stimela to pull images without authentication
451
+
452
+ ### 4. Version Tagging Best Practices
453
+
454
+ The workflow above creates multiple tags for each release:
455
+
456
+ ```bash
457
+ # For release v1.2.3, creates:
458
+ ghcr.io/username/mypackage:1.2.3 # Full version
459
+ ghcr.io/username/mypackage:1.2 # Minor version
460
+ ghcr.io/username/mypackage:1 # Major version
461
+ ghcr.io/username/mypackage:main-sha123456 # Branch + commit SHA
462
+ ```
463
+
464
+ This allows users to pin to specific versions or track latest minor/major releases.
465
+
466
+ ### 5. Triggering a Build
467
+
468
+ **Automated (recommended):**
469
+ ```bash
470
+ # Create and push a version tag
471
+ git tag v1.0.0
472
+ git push origin v1.0.0
473
+ ```
474
+
475
+ The GitHub Action will automatically build and publish the container.
476
+
477
+ **Manual:**
478
+ - Go to "Actions" tab in GitHub
479
+ - Select "Build and Publish Container"
480
+ - Click "Run workflow"
481
+
482
+ ### 6. Using the Container with Stimela
483
+
484
+ Once published, users can reference your container in Stimela recipes:
485
+
486
+ ```yaml
487
+ cabs:
488
+ - name: mypackage
489
+ image: ghcr.io/username/mypackage:1.0.0
490
+ ```
491
+
492
+ Stimela will automatically pull the matching version based on the cab configuration.
493
+
494
+ ### 7. Local Testing
495
+
496
+ Test your container locally before pushing:
497
+
498
+ ```bash
499
+ # Build
500
+ docker build -t mypackage:test .
501
+
502
+ # Run
503
+ docker run --rm mypackage:test --help
504
+ docker run --rm mypackage:test process --help
505
+
506
+ # Test with mounted data
507
+ docker run --rm -v $(pwd)/data:/data mypackage:test process /data/input.ms
508
+ ```
509
+
510
+ ## Type Inference
511
+
512
+ `hip-cargo` automatically recognizes custom `stimela` types. The `generate-cab` command should add
513
+ ```python
514
+ from pathlib import Path
515
+ from typing import NewType
516
+
517
+ MS = NewType("MS", Path)
518
+ Directory = NewType("Directory", Path)
519
+ URI = NewType("URI", Path)
520
+ File = NewType("File", Path)
521
+ ```
522
+
523
+ to the preamble of functions generated from cabs that use these types.
524
+ It should also add the `parser` bit to the type hint Annotation e.g. for the custom `MS` dtype we need
525
+ ```
526
+ def process(input_ms: Annotated[MS, typer.Option(parser=MS)]):
527
+ pass
528
+ ```
529
+ One quirk of this approach is that parameters which have `None` as the default need to be defined as e.g.
530
+ ```
531
+ def process(input_ms: Annotated[MS | None, typer.Option(parser=MS)]) = None:
532
+ pass
533
+ ```
534
+ Python then parses this as `Optional[MS]` which is just an alias for `Union[MS | None]`. This should be handled correctly such that the `generate-cab` command places `dtype: MS` in the cab definition and the `generate-function` command correctly generates the function signature above. These custom types are currently limited to only two possible types in the `Union` and should be specified using the newer `dtype1 | dtype2` format in the function definition (one of which may be `None`). All standard python types should just work.
535
+
536
+ ## Decorators
537
+
538
+ ### `@stimela_cab`
539
+
540
+ Marks a function as a Stimela cab.
541
+
542
+ - `name`: Cab name
543
+ - `info`: Description
544
+ - `policies`: Optional dict of cab-level policies
545
+
546
+ ### `@stimela_output`
547
+
548
+ Defines a `stimela` output. When defining functions from cabs the `generate-function` command should check for the following parameter fields
549
+
550
+ - `name`: Output name (top level, one below `cabs`)
551
+ - `dtype`: Data type (File, Directory, MS, etc.)
552
+ - `info`: Help string
553
+ - `required`: Whether output is required (default: False)
554
+ - `implicit`: If implicit is `True` the parameter should not be placed in the function definition. If implicit is `False` (the default), the parameter needs to be added to the function signature.
555
+
556
+ ## Features
557
+
558
+ - ✅ Automatic type inference from Python type hints
559
+ - ✅ Support for Typer Arguments (positional) and Options
560
+ - ✅ Multiple outputs automatically added to function signature if they are not implicit
561
+ - ✅ List types with automatic `repeat: list` policy
562
+ - ✅ Proper handling of default values and required parameters
563
+
564
+ ## Development
565
+
566
+ This project uses:
567
+ - [uv](https://github.com/astral-sh/uv) for dependency management
568
+ - [ruff](https://github.com/astral-sh/ruff) for linting and formatting
569
+ - [typer](https://typer.tiangolo.com/) for the CLI
570
+
571
+ ### Setting Up Development Environment
572
+
573
+ ```bash
574
+ # Clone the repository
575
+ git clone https://github.com/landmanbester/hip-cargo.git
576
+ cd hip-cargo
577
+
578
+ # Install dependencies with development tools
579
+ uv sync --group dev
580
+
581
+ # Install pre-commit hooks (recommended)
582
+ uv run pre-commit install
583
+ ```
584
+
585
+ ### Pre-commit Hooks
586
+
587
+ This project uses [pre-commit](https://pre-commit.com/) to automatically check code quality before commits. The hooks run:
588
+
589
+ - **ruff linting**: Checks code style and catches common errors
590
+ - **ruff formatting**: Ensures consistent code formatting
591
+ - **trailing whitespace**: Removes trailing whitespace
592
+ - **end-of-file-fixer**: Ensures files end with a newline
593
+ - **check-yaml**: Validates YAML syntax
594
+ - **check-toml**: Validates TOML syntax
595
+ - **check-merge-conflict**: Prevents committing merge conflict markers
596
+ - **check-added-large-files**: Prevents accidentally committing large files
597
+
598
+ #### Installing Pre-commit Hooks
599
+
600
+ After cloning the repository, install the pre-commit hooks:
601
+
602
+ ```bash
603
+ uv run pre-commit install
604
+ ```
605
+
606
+ This will automatically run the hooks before each commit. If any checks fail, the commit will be blocked until you fix the issues.
607
+
608
+ #### Running Hooks Manually
609
+
610
+ You can run the hooks manually on all files:
611
+
612
+ ```bash
613
+ # Run on all files
614
+ uv run pre-commit run --all-files
615
+
616
+ # Run on staged files only
617
+ uv run pre-commit run
618
+ ```
619
+
620
+ #### Updating Hook Versions
621
+
622
+ To update hook versions to the latest:
623
+
624
+ ```bash
625
+ uv run pre-commit autoupdate
626
+ ```
627
+
628
+ ### Manual Code Quality Checks
629
+
630
+ If you prefer to run checks manually without pre-commit:
631
+
632
+ ```bash
633
+ # Format code
634
+ uv run ruff format .
635
+
636
+ # Check and auto-fix linting issues
637
+ uv run ruff check . --fix
638
+
639
+ # Run tests
640
+ uv run pytest -v
641
+
642
+ # Run tests with coverage
643
+ uv run pytest --cov=hip_cargo --cov-report=term-missing
644
+ ```
645
+
646
+ ### Contributing Workflow
647
+
648
+ 1. **Create a feature branch**:
649
+ ```bash
650
+ git checkout -b feature/your-feature-name
651
+ ```
652
+
653
+ 2. **Make your changes** and ensure tests pass:
654
+ ```bash
655
+ uv run pytest -v
656
+ ```
657
+
658
+ 3. **Format and lint** (automatically done by pre-commit):
659
+ ```bash
660
+ git add .
661
+ git commit -m "feat: your feature description"
662
+ # Pre-commit hooks run automatically
663
+ ```
664
+
665
+ 4. **Push and create a pull request**:
666
+ ```bash
667
+ git push origin feature/your-feature-name
668
+ ```
669
+
670
+ ## License
671
+
672
+ MIT License