Skip to content

Commit 01aa7f5

Browse files
committed
Initial commit
0 parents  commit 01aa7f5

42 files changed

Lines changed: 276106 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Python-generated files
2+
__pycache__/
3+
*.py[oc]
4+
build/
5+
dist/
6+
wheels/
7+
*.egg-info
8+
9+
# Virtual environments
10+
.venv
11+
12+
# Environment variables:
13+
.env
14+
15+
# Exclude local .deb and .zip files from mscl:
16+
mscl_release_assets/*.zip
17+
mscl_release_assets/*.deb

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#python-mscl
2+
3+
Unofficial Python bindings for the [Microstrain Communication Library](https://www.microstrain.com/developers/microstrain-communication-library).
4+
5+
This library just makes it so that we can install the MSCL library using pip. Wheels are not provided. This will fetch the necessary files for your architecture and python
6+
version, and then build the wheel for you.
7+
8+
It is therefore recommended to use a cache for your CI or package manager, unless you're okay with the ~20MB download every time you run your CI.
9+
10+
## Note: NOT PUBLISHED TO PYPI YET - STILL A WIP! CHECK BACK IN A FEW DAYS!
11+
12+
### Installation
13+
14+
```bash
15+
pip install python-mscl
16+
```
17+
18+
### Usage
19+
20+
```python
21+
import mscl
22+
23+
# ... use the MSCL library as you normally would
24+
```
25+
26+
## Local Development:
27+
28+
TODO

build_helpers/__init__.py

Whitespace-only changes.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Downloads the Github release assets for the mscl library."""
2+
3+
import os
4+
from pathlib import Path
5+
6+
import requests
7+
from github import Github
8+
from github.GitRelease import GitRelease
9+
from github.GitReleaseAsset import GitReleaseAsset
10+
11+
12+
class GithubDownloader:
13+
"""Manages downloading the Github release assets for the mscl library, along with the
14+
extracted files from this repository."""
15+
16+
def __init__(self):
17+
self.github = Github(os.getenv("GITHUB_TOKEN"))
18+
self.mscl_repo = "LORD-MicroStrain/MSCL"
19+
self.python_mscl_repo = "harshil21/python-mscl"
20+
self.latest_release = None
21+
22+
def get_latest_release(self) -> GitRelease:
23+
"""Returns the latest stable release for the given repo."""
24+
if self.latest_release:
25+
return self.latest_release
26+
27+
releases = self.github.get_repo(self.mscl_repo).get_releases()
28+
for release in releases:
29+
if release.prerelease:
30+
continue
31+
if release.tag_name.startswith("v"):
32+
self.latest_release = release
33+
break
34+
return self.latest_release
35+
36+
def download_release_assets(self, output_dir: str):
37+
"""Downloads the release assets for the given repo and tag."""
38+
release = self.get_latest_release()
39+
output_path = Path(output_dir)
40+
output_path.mkdir(parents=True, exist_ok=True)
41+
42+
asset: GitReleaseAsset
43+
for asset in release.get_assets():
44+
# Don't download the "Documentation" or "Examples"
45+
if "Documentation" in asset.name or "Examples" in asset.name:
46+
continue
47+
# Don't download anything non-python:
48+
if "Python" not in asset.name:
49+
continue
50+
# Only python 3 and above:
51+
if "3" not in asset.name:
52+
continue
53+
54+
self.download_asset(output_path, asset)
55+
56+
def download_asset(self, output_path: Path, asset: GitReleaseAsset) -> None:
57+
response = requests.get(asset.browser_download_url, timeout=15)
58+
asset_path = output_path / asset.name
59+
asset_path.write_bytes(response.content)
60+
61+
def download_assets_from_folder(self, tag: str, folder_name: str) -> None:
62+
"""Downloads all the files under the `folder_name` for the given tag, from the
63+
root of the repository."""
64+
65+
repo = self.github.get_repo(self.python_mscl_repo)
66+
contents = repo.get_contents(folder_name, ref=tag)
67+
68+
for content in contents:
69+
if content.type == "file":
70+
response = requests.get(content.download_url, timeout=15)
71+
file_path = Path(content.name)
72+
file_path.write_bytes(response.content)

build_helpers/release_extractor.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Extracts the .deb and .zip releases for the mscl library."""
2+
3+
import os
4+
import subprocess
5+
from pathlib import Path
6+
7+
8+
class ReleaseExtractor:
9+
"""Will extract the .deb and .zip releases for the mscl library."""
10+
11+
def __init__(self):
12+
self.asset_dir = Path("mscl_release_assets")
13+
14+
def extract_assets(self):
15+
"""Extracts the .deb and .zip releases into the same directory."""
16+
17+
for file in self.asset_dir.iterdir():
18+
if file.suffix == ".deb":
19+
self.extract_deb(file)
20+
elif file.suffix == ".zip":
21+
self.extract_zip(file)
22+
23+
def extract_deb(self, file: Path):
24+
"""Extracts the .deb release."""
25+
cwd = Path().cwd()
26+
27+
# Create a directory to extract the .deb file. Syntax: mscl-<arch>-<python-ver>-<mscl-ver>
28+
parts = file.stem.split("_")
29+
arch, py_ver, mscl_ver = parts[1], parts[2], parts[3]
30+
mscl_versioned_name = f"mscl-{arch}-{py_ver}-{mscl_ver}"
31+
mscl_versioned_dir = cwd / self.asset_dir / mscl_versioned_name
32+
33+
# If output directory exists, remove it:
34+
if mscl_versioned_dir.exists():
35+
os.system(f"rm -rf {mscl_versioned_dir}") # noqa: S605
36+
37+
mscl_versioned_dir.mkdir(parents=True, exist_ok=True)
38+
file_relative = file.absolute().relative_to(mscl_versioned_dir, walk_up=True)
39+
40+
# Extract the .deb file
41+
subprocess.run(["ar", "x", str(file_relative)], cwd=mscl_versioned_dir, check=True) # noqa: S603, S607
42+
43+
# Extract the data.tar.gz file:
44+
data_tar = "data.tar.gz"
45+
46+
subprocess.run(["tar", "-xzf", data_tar], cwd=mscl_versioned_dir, check=True) # noqa: S603, S607
47+
48+
found_mscl_py = list(mscl_versioned_dir.rglob("mscl.py"))
49+
found_mscl_so = list(mscl_versioned_dir.rglob("_mscl.so"))
50+
51+
if not found_mscl_py or not found_mscl_so:
52+
raise FileNotFoundError(f"Could not find mscl.py or _mscl.so in {mscl_versioned_dir}")
53+
54+
# Move the extracted files to the root of the mscl_versioned_dir:
55+
mscl_py = found_mscl_py[0]
56+
mscl_so = found_mscl_so[0]
57+
58+
mscl_py.rename(mscl_versioned_dir / mscl_py.name)
59+
mscl_so.rename(mscl_versioned_dir / mscl_so.name)
60+
61+
# Delete the remaining files in mscl_versioned_dir:
62+
for f in mscl_versioned_dir.iterdir():
63+
if f.stem in (mscl_py.stem, mscl_so.stem):
64+
continue
65+
if f.is_dir():
66+
os.system(f"rm -rf {f}") # noqa: S605
67+
else:
68+
f.unlink()
69+
70+
def extract_zip(self, file: Path) -> None:
71+
"""Extracts the .zip release."""

hatch_build.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Specifies a hatch build hook to create the wheel for mscl."""
2+
3+
import platform
4+
import sys
5+
from pathlib import Path
6+
7+
sys.path.insert(0, str(Path(__file__).parent))
8+
9+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
10+
11+
from build_helpers.release_downloader import GithubDownloader
12+
13+
14+
class CustomBuildHook(BuildHookInterface):
15+
"""Build hook to build wheels from the extracted .deb/.zip files."""
16+
17+
def _python_tag(self) -> str:
18+
"""Generate the Python tag (e.g., py39 for Python 3.9)."""
19+
major = sys.version_info.major
20+
minor = sys.version_info.minor
21+
return f"cp{major}{minor}"
22+
23+
def _platform_tag(self) -> str:
24+
"""Generate the platform tag (e.g., linux_x86_64)."""
25+
return platform.system().lower() + "_" + platform.machine()
26+
27+
def initialize(self, version, build_data):
28+
"""
29+
Called before building the wheel/sdist.
30+
We can download & extract the .deb here, and place
31+
mscl.py and _mscl.so into src/mscl_pip/.
32+
"""
33+
build_data["pure_python"] = False
34+
self.app.display_info(f"Running on {version=} and {build_data=}")
35+
self.app.display_info(self.target_name)
36+
37+
# --- STEP 1: Determine which python version and arch we are on: ---
38+
# a) Python version:
39+
# syntax: Python<MAJOR>.<MINOR>
40+
41+
py_version = f"Python{sys.version_info.major}.{sys.version_info.minor}"
42+
43+
# b) Architecture:
44+
# possible values: amd64, arm64, armhf.
45+
46+
arch = platform.machine()
47+
if arch == "x86_64":
48+
arch = "amd64"
49+
elif arch == "aarch64":
50+
arch = "arm64"
51+
elif arch == "armv7l":
52+
arch = "armhf"
53+
else:
54+
# TODO: Windows support
55+
raise RuntimeError(f"Unknown architecture: {arch}")
56+
57+
# c) mscl version to download:
58+
mscl_ver = "v67.0.0"
59+
60+
build_data["tag"] = f"{self._python_tag()}-{self._python_tag()}-{self._platform_tag()}"
61+
# --- STEP 2: Download the 2 mscl files (mscl.py and _mscl.so) from the git repo: ---
62+
# Folder name: mscl-<arch>-<python-ver>-<mscl-ver>
63+
64+
# a) Create the folder name:
65+
folder_name = f"mscl-{arch}-{py_version}-{mscl_ver}"
66+
67+
# b) Use PyGithub to download the files from the folder:
68+
self.app.display_info(f"Downloading files for {folder_name}...")
69+
70+
gh = GithubDownloader()
71+
gh.download_assets_from_folder(
72+
tag=mscl_ver,
73+
folder_name=f"mscl_release_assets/{folder_name}",
74+
)
75+
76+
self.display_info("Downloaded files successfully. Building the wheel...")

main.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Entry point for scripts."""
2+
3+
from build_helpers.release_downloader import GithubDownloader
4+
from build_helpers.release_extractor import ReleaseExtractor
5+
6+
7+
def main():
8+
GithubDownloader()
9+
# gh.download_release_assets("mscl_release_assets")
10+
re = ReleaseExtractor()
11+
re.extract_assets()
12+
13+
14+
if __name__ == "__main__":
15+
main()
17.1 MB
Binary file not shown.

0 commit comments

Comments
 (0)