Skip to content

Commit 2895341

Browse files
authored
Merge pull request #118 from linksplatform/issue-117-e66fa934
Python implementation of Lino protocol parser
2 parents b788d69 + 81beda6 commit 2895341

File tree

15 files changed

+1466
-1
lines changed

15 files changed

+1466
-1
lines changed

.github/workflows/python.yml

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
name: python
2+
3+
on:
4+
push:
5+
branches: main
6+
paths:
7+
- 'python/**'
8+
- '.github/workflows/python.yml'
9+
pull_request:
10+
paths:
11+
- 'python/**'
12+
- '.github/workflows/python.yml'
13+
14+
env:
15+
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
16+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
17+
18+
defaults:
19+
run:
20+
working-directory: python
21+
22+
jobs:
23+
findChangedPythonFiles:
24+
runs-on: ubuntu-latest
25+
outputs:
26+
isPythonFilesChanged: ${{ steps.setIsPythonFilesChangedOutput.outputs.isPythonFilesChanged }}
27+
steps:
28+
- uses: actions/checkout@v3
29+
with:
30+
fetch-depth: 0
31+
- name: Get changed files
32+
id: changed-files
33+
uses: tj-actions/changed-files@v46
34+
- name: Set output isPythonFilesChanged
35+
id: setIsPythonFilesChangedOutput
36+
run: |
37+
isPythonFilesChanged='false'
38+
echo "Changed files: ${{ steps.changed-files.outputs.all_changed_files }}"
39+
for changedFile in ${{ steps.changed-files.outputs.all_changed_files }}; do
40+
if [[ $changedFile == python/*.py ]] || [[ $changedFile == python/pyproject.toml ]] || [[ $changedFile == python/setup.py ]] || [[ $changedFile == python/* ]] || [[ $changedFile == .github/workflows/python.yml ]]; then
41+
echo "isPythonFilesChanged='true'"
42+
isPythonFilesChanged='true'
43+
break
44+
fi
45+
done
46+
echo "isPythonFilesChanged=${isPythonFilesChanged}" >> $GITHUB_OUTPUT
47+
echo "isPythonFilesChanged: ${isPythonFilesChanged}"
48+
49+
test:
50+
needs: [findChangedPythonFiles]
51+
if: ${{ needs.findChangedPythonFiles.outputs.isPythonFilesChanged == 'true' }}
52+
runs-on: ubuntu-latest
53+
strategy:
54+
matrix:
55+
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
56+
steps:
57+
- uses: actions/checkout@v3
58+
with:
59+
submodules: true
60+
- name: Set up Python ${{ matrix.python-version }}
61+
uses: actions/setup-python@v4
62+
with:
63+
python-version: ${{ matrix.python-version }}
64+
- name: Install dependencies
65+
run: |
66+
python -m pip install --upgrade pip
67+
pip install pytest build twine
68+
- name: Build package
69+
run: python -m build
70+
- name: Run tests
71+
run: pytest -v
72+
73+
publishToPyPI:
74+
needs: [test, findChangedPythonFiles]
75+
if: ${{ needs.findChangedPythonFiles.outputs.isPythonFilesChanged == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
76+
runs-on: ubuntu-latest
77+
steps:
78+
- uses: actions/checkout@v3
79+
with:
80+
submodules: true
81+
- name: Set up Python
82+
uses: actions/setup-python@v4
83+
with:
84+
python-version: '3.11'
85+
- name: Install dependencies
86+
run: |
87+
python -m pip install --upgrade pip
88+
pip install build twine
89+
- name: Build package
90+
run: python -m build
91+
- name: Check if version already published
92+
id: version-check
93+
run: |
94+
PACKAGE_VERSION=$(python -c "import tomli; f = open('pyproject.toml', 'rb'); data = tomli.load(f); print(data['project']['version'])" 2>/dev/null || grep "^version" pyproject.toml | sed 's/version = "\(.*\)"/\1/')
95+
PACKAGE_NAME=$(python -c "import tomli; f = open('pyproject.toml', 'rb'); data = tomli.load(f); print(data['project']['name'])" 2>/dev/null || grep "^name" pyproject.toml | sed 's/name = "\(.*\)"/\1/')
96+
echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION"
97+
98+
# Check if version exists on PyPI
99+
if pip index versions $PACKAGE_NAME 2>/dev/null | grep -q "$PACKAGE_VERSION"; then
100+
echo "Version $PACKAGE_VERSION already exists on PyPI"
101+
echo "should_publish=false" >> $GITHUB_OUTPUT
102+
else
103+
echo "Version $PACKAGE_VERSION does not exist on PyPI"
104+
echo "should_publish=true" >> $GITHUB_OUTPUT
105+
fi
106+
- name: Publish to PyPI
107+
if: steps.version-check.outputs.should_publish == 'true'
108+
env:
109+
TWINE_USERNAME: __token__
110+
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
111+
run: |
112+
twine upload dist/*
113+
114+
publishRelease:
115+
runs-on: ubuntu-latest
116+
needs: [publishToPyPI]
117+
if: ${{ needs.findChangedPythonFiles.outputs.isPythonFilesChanged == 'true' && needs.publishToPyPI.result == 'success' && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
118+
steps:
119+
- uses: actions/checkout@v3
120+
with:
121+
submodules: true
122+
- name: Set up Python
123+
uses: actions/setup-python@v4
124+
with:
125+
python-version: '3.11'
126+
- name: Check if GitHub release already exists
127+
id: release-check
128+
run: |
129+
PACKAGE_VERSION=$(grep "^version" pyproject.toml | sed 's/version = "\(.*\)"/\1/')
130+
TAG_NAME="python_$PACKAGE_VERSION"
131+
echo "Checking if release $TAG_NAME already exists"
132+
133+
# Check if release exists
134+
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
135+
echo "Release $TAG_NAME already exists"
136+
echo "should_create_release=false" >> $GITHUB_OUTPUT
137+
else
138+
echo "Release $TAG_NAME does not exist"
139+
echo "should_create_release=true" >> $GITHUB_OUTPUT
140+
fi
141+
env:
142+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
143+
- name: Create GitHub release
144+
if: steps.release-check.outputs.should_create_release == 'true'
145+
run: |
146+
PACKAGE_VERSION=$(grep "^version" pyproject.toml | sed 's/version = "\(.*\)"/\1/')
147+
PACKAGE_NAME=$(grep "^name" pyproject.toml | sed 's/name = "\(.*\)"/\1/')
148+
149+
# Create release
150+
gh release create "${PACKAGE_VERSION}_python" \
151+
--title "[Python] $PACKAGE_VERSION" \
152+
--notes "https://pypi.org/project/$PACKAGE_NAME/"
153+
env:
154+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,4 +333,5 @@ ASALocalRun/
333333
.DS_Store
334334

335335
# rust
336-
target/
336+
target/venv/
337+
.venv/

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
|:-|-:|:-|
55
| [![Actions Status](https://github.com/linksplatform/Protocols.Lino/workflows/rust/badge.svg)](https://github.com/linksplatform/Protocols.Lino/actions?workflow=rust) | [![Crates.io Version and Downloads count](https://img.shields.io/crates/v/platform-lino?label=crates.io&style=flat)](https://crates.io/crates/platform-lino) | **[Rust](rust/README.md)** |
66
| [![Actions Status](https://github.com/linksplatform/Protocols.Lino/workflows/csharp/badge.svg)](https://github.com/linksplatform/Protocols.Lino/actions?workflow=csharp) | [![NuGet Version and Downloads count](https://img.shields.io/nuget/v/Platform.Protocols.Lino?label=nuget&style=flat)](https://www.nuget.org/packages/Platform.Protocols.Lino) | **[C#](csharp/README.md)** |
7+
| [![Actions Status](https://github.com/linksplatform/Protocols.Lino/workflows/python/badge.svg)](https://github.com/linksplatform/Protocols.Lino/actions?workflow=python) | [![PyPI Version and Downloads count](https://img.shields.io/pypi/v/platform-lino?label=pypi&style=flat)](https://pypi.org/project/platform-lino/) | **[Python](python/README.md)** |
78

89
[![Gitpod](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/linksplatform/Protocols.Lino)
910
[![Open in GitHub Codespaces](https://img.shields.io/badge/GitHub%20Codespaces-Open-181717?logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=linksplatform/Protocols.Lino)
@@ -47,6 +48,14 @@ use lino::parse_lino;
4748
let links = parse_lino("papa (lovesMama: loves mama)").unwrap();
4849
```
4950

51+
### Python
52+
53+
```python
54+
from platform_lino import Parser
55+
parser = Parser()
56+
links = parser.parse("papa (lovesMama: loves mama)")
57+
```
58+
5059
## Examples
5160

5261
### Links notation (lino)
@@ -127,6 +136,7 @@ language-specific documentation:
127136
- **[C# README](csharp/README.md)** - Installation and usage guide
128137
- **[JavaScript README](js/README.md)** - Modern web development guide
129138
- **[Rust README](rust/README.md)** - High-performance parsing guide
139+
- **[Python README](python/README.md)** - Python package guide
130140

131141
Additional resources:
132142

python/MANIFEST.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
include README.md
2+
include LICENSE
3+
recursive-include platform_lino *.py

python/README.md

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Platform.Protocols.Lino - Python
2+
3+
[![PyPI version](https://img.shields.io/pypi/v/platform-lino.svg)](https://pypi.org/project/platform-lino/)
4+
[![Python versions](https://img.shields.io/pypi/pyversions/platform-lino.svg)](https://pypi.org/project/platform-lino/)
5+
[![License](https://img.shields.io/badge/license-Unlicense-blue.svg)](../LICENSE)
6+
7+
Python implementation of the Lino (Links Notation) protocol parser.
8+
9+
## Installation
10+
11+
```bash
12+
pip install platform-lino
13+
```
14+
15+
## Quick Start
16+
17+
```python
18+
from platform_lino import Parser
19+
20+
parser = Parser()
21+
links = parser.parse("papa (lovesMama: loves mama)")
22+
23+
# Access parsed links
24+
for link in links:
25+
print(link)
26+
```
27+
28+
## Usage
29+
30+
### Basic Parsing
31+
32+
```python
33+
from platform_lino import Parser, format_links
34+
35+
parser = Parser()
36+
37+
# Parse simple links
38+
links = parser.parse("(papa: loves mama)")
39+
print(links[0].id) # 'papa'
40+
print(len(links[0].values)) # 2
41+
42+
# Format links back to string
43+
output = format_links(links)
44+
print(output) # (papa: loves mama)
45+
```
46+
47+
### Working with Link Objects
48+
49+
```python
50+
from platform_lino import Link
51+
52+
# Create links programmatically
53+
link = Link('parent', [Link('child1'), Link('child2')])
54+
print(str(link)) # (parent: child1 child2)
55+
56+
# Access link properties
57+
print(link.id) # 'parent'
58+
print(link.values[0].id) # 'child1'
59+
60+
# Combine links
61+
combined = link.combine(Link('another'))
62+
print(str(combined)) # ((parent: child1 child2) another)
63+
```
64+
65+
### Indented Syntax
66+
67+
```python
68+
parser = Parser()
69+
70+
# Parse indented notation
71+
text = """3:
72+
papa
73+
loves
74+
mama"""
75+
76+
links = parser.parse(text)
77+
# Produces: (3: papa loves mama)
78+
```
79+
80+
## API Reference
81+
82+
### Parser
83+
84+
The main parser class for Lino notation.
85+
86+
- `parse(input_text: str) -> List[Link]`: Parse Lino text into Link objects
87+
88+
### Link
89+
90+
Represents a link in Lino notation.
91+
92+
- `__init__(id: Optional[str] = None, values: Optional[List[Link]] = None)`
93+
- `format(less_parentheses: bool = False) -> str`: Format as string
94+
- `simplify() -> Link`: Simplify link structure
95+
- `combine(other: Link) -> Link`: Combine with another link
96+
97+
### format_links
98+
99+
Format a list of links into Lino notation.
100+
101+
- `format_links(links: List[Link], less_parentheses: bool = False) -> str`
102+
103+
## Examples
104+
105+
### Doublets (2-tuple)
106+
107+
```python
108+
parser = Parser()
109+
text = """
110+
papa (lovesMama: loves mama)
111+
son lovesMama
112+
daughter lovesMama
113+
"""
114+
links = parser.parse(text)
115+
```
116+
117+
### Triplets (3-tuple)
118+
119+
```python
120+
text = """
121+
papa has car
122+
mama has house
123+
(papa and mama) are happy
124+
"""
125+
links = parser.parse(text)
126+
```
127+
128+
### Quoted References
129+
130+
```python
131+
# References with special characters need quotes
132+
text = '("has space": "value with: colon")'
133+
links = parser.parse(text)
134+
```
135+
136+
## Development
137+
138+
### Running Tests
139+
140+
```bash
141+
# Install development dependencies
142+
pip install pytest
143+
144+
# Run tests
145+
pytest
146+
```
147+
148+
### Building
149+
150+
```bash
151+
pip install build
152+
python -m build
153+
```
154+
155+
## License
156+
157+
This project is released into the public domain under the [Unlicense](../LICENSE).
158+
159+
## Links
160+
161+
- [Main Repository](https://github.com/linksplatform/Protocols.Lino)
162+
- [PyPI Package](https://pypi.org/project/platform-lino/)
163+
- [Documentation](https://linksplatform.github.io/Protocols.Lino/)

python/platform_lino/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""
2+
Platform.Protocols.Lino - Python implementation
3+
4+
Lino (Links Notation) is a simple, intuitive format for representing
5+
structured data as links between references.
6+
"""
7+
8+
from .link import Link
9+
from .parser import Parser
10+
from .formatter import format_links
11+
12+
__version__ = "0.7.0"
13+
14+
__all__ = ["Link", "Parser", "format_links"]

0 commit comments

Comments
 (0)