Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 59 additions & 23 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,90 @@ name: Python application
on: [push, pull_request]

jobs:
build:

test:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for versioning

- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: '3.x'

- name: Install Poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -

- name: Install dependencies with Poetry
- name: Install UV
run: |
poetry install --with dev
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH

- name: Install pre-commit
- name: Install dependencies with UV
run: |
poetry run pip install pre-commit
uv sync

- name: Run pre-commit
run: |
poetry run pre-commit run --all-files
uv run pre-commit run --all-files

- name: Install Playwright browsers
run: |
poetry run playwright install
uv run playwright install

- name: Run tests
run: |
poetry run pytest --reruns 3 # githubs playwright sometimes fails
uv run pytest --reruns 3 # githubs playwright sometimes fails

publish:
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: write # Allows pushing tags

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for versioning
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'

- name: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH

- name: Install dependencies
run: |
uv sync

- name: Bump version and push tag
id: tag_version
uses: mathieudutour/github-tag-action@v6.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
default_bump: patch
tag_prefix: v

- name: Build package
if: github.ref == 'refs/heads/main'
run: |
python -m pip install --upgrade build
python -m build
uv build

- name: Publish package to PyPI
if: github.ref == 'refs/heads/main'
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: |
python -m pip install --upgrade twine
python -m twine upload dist/*
uv publish

- name: Create GitHub Release
uses: ncipollo/release-action@v1
with:
tag: ${{ steps.tag_version.outputs.new_tag }}
name: Release ${{ steps.tag_version.outputs.new_tag }}
body: ${{ steps.tag_version.outputs.changelog }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
Dockerfile
example.py

# Auto-generated version file
hstream/_version.py

.*pyc
# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
47 changes: 43 additions & 4 deletions CONTRIBUTE.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,54 @@
## Deploy

1. increment version in `setup.py`
We use automatic versioning and deployment with hatch-vcs and GitHub Actions.

2. `rm -rf build/ hstream.egg-info/ dist/`
### How It Works:

3. `pip install twine build`
```
Push to main → Tests pass → Auto-create tag → Build with version → Publish to PyPI → Create GitHub release
```

4. `python -m build`
Every push to the main branch will:
1. Run all tests
2. Automatically create a new git tag (patch version bump: v0.1.58 → v0.1.59)
3. Build the package with the version from the tag
4. Publish to PyPI automatically
5. Create a GitHub release with changelog

### Important: Initial Setup Needed

Before the auto-versioning works, you need to create an initial tag:

```bash
# Switch to main branch and create initial tag
git checkout main
git tag v0.1.58 # Use the next version number
git push origin v0.1.58
```

After that, every push to main will auto-increment the patch version.

### Manual Version Bumps

The default is to bump the patch version (0.1.X), but you can control the version bump with commit messages:

- **Patch bump** (default): `git commit -m "fix: bug fix"`
- **Minor bump**: `git commit -m "feat: new feature"`
- **Major bump**: `git commit -m "feat!: breaking change"`

### Old Manual Process (Deprecated)

<details>
<summary>Click to see the old manual deployment process (no longer used)</summary>

1. increment version in `setup.py`
2. `rm -rf build/ hstream.egg-info/ dist/`
3. `pip install twine build`
4. `python -m build`
5. `twine upload dist/*`

</details>

## Kill orphaned uvicorn processes

`kill -9 $(lsof -t -i:8000)` (or whatever port they we're using)
2 changes: 1 addition & 1 deletion agents.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
@architecture.md
@architecture.md
7 changes: 6 additions & 1 deletion architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This flow covers the process from running the command to the user seeing the ini
```mermaid
stateDiagram-v2
direction LR

state "CLI & Server Startup" as Startup {
[*] --> RunCommand: hstream run <script>
RunCommand --> DjangoBoot: run_server()
Expand Down Expand Up @@ -67,6 +67,11 @@ stateDiagram-v2
> **Swap vs Refresh Caveats**:
> * **Full Replace**: Used when the component structure changes significantly. This is the safest fallback but resets the DOM state (e.g., focus, scroll position) unless carefully managed.
> * **Partial Replace/Append**: HStream attempts to preserve the DOM by only updating changed elements (identified by consistent IDs). This maintains user focus and input state better than a full refresh.
>
> ```python
> strategy = pick_a_strategy(prev_html, new_html, hs_script_running)
> # Returns: "1_full_replace", "2_nothing", "3_partial_replace", or "4_partial_append"
> ```


## Key File Descriptions
Expand Down
7 changes: 4 additions & 3 deletions demo/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@
"""
)


hs.markdown("""Or create forms like:""")
with hs.html("form"):
user_number = hs.number_input(
"Input a number",
default_value=0,
)
hs.markdown(f"Your number is {'*even*' if int(user_number) % 2 == 0 else '*odd*'}")
hs.markdown(
f"Your number is {'*even*' if int(user_number) % 2 == 0 else '*odd*'}"
)
with hs.html("header"):
hs.markdown("## HStream also supports displaying plots")
with hs.html("section"):
Expand Down Expand Up @@ -72,7 +73,7 @@
fig, ax = plt.subplots()
ax.plot(x, y)
hs.pyplot(fig, key="myplot")
except Exception as e:
except Exception:
hs.markdown(
"hmmm seems you don't have matplotlib installed, please install it with `pip install matplotlib`"
)
Expand Down
5 changes: 3 additions & 2 deletions demo/file upload.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from hstream import hs

hs.markdown("# upload")
uploaded_file = hs.file_upload("Upload your file")

if uploaded_file and hs.button("Process and Download"):
hs.markdown('uploaded')
hs.markdown("uploaded")

hs.markdown(uploaded_file)
2 changes: 1 addition & 1 deletion hstream/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from hstream.hs import hs as hstream
from hstream.components.components import component
from hstream.components.components import component as component


hs = hstream()
16 changes: 9 additions & 7 deletions hstream/components/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ def component(component_function):
return wrapped_func



class ComponentsGeneric:
def get_key_based_on_call(self, message):
"""Displays a navigation bar with a list of items.
Expand Down Expand Up @@ -525,7 +524,7 @@ def write_dataframe(self, df, key: str = None, striped=False, **kwargs) -> None:
.replace('class="dataframe"', f'class="{striped}"')
)
self.doc.asis(html)

@component_wrapper
def file_upload(
self,
Expand All @@ -546,13 +545,16 @@ def file_upload(
"""
with self.tag("label"):
self.text(label)

# Check if we have a filename
has_file = kwargs.get("value") and isinstance(kwargs["value"], str)

if has_file:
# Show current file info with option to clear
with self.tag("div", style="margin: 5px 0; padding: 10px; background-color: #f0f0f0; border-radius: 3px; display: flex; justify-content: space-between; align-items: center;"):
with self.tag(
"div",
style="margin: 5px 0; padding: 10px; background-color: #f0f0f0; border-radius: 3px; display: flex; justify-content: space-between; align-items: center;",
):
with self.tag("span"):
self.text(f"Current file: {kwargs['value']}")
with self.tag(
Expand All @@ -561,7 +563,7 @@ def file_upload(
("hx-vals", '{"new_value": null}'),
("hx-swap", "none"),
("type", "button"),
style="background-color: #ff4444; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer;"
style="background-color: #ff4444; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer;",
):
self.text("Remove")
else:
Expand All @@ -577,6 +579,6 @@ def file_upload(
("hx-encoding", "multipart/form-data"),
):
pass

# Return the filename directly
return lambda x: str(x) if x else None
28 changes: 13 additions & 15 deletions hstream/django_server/hs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def partial_or_full_html_content(request):
update_strategy = pick_a_strategy(
prev_html, html, get_session_var(request, "hs_script_running", False)
)
# print(f"update strategy: {update_strategy}")
print(f"[DIFF_STRATEGY] Selected strategy: {update_strategy}")
response = HttpResponse()
if get_session_var(request, "hs_script_running", False):
response.headers["HX-Trigger"] = "update_content_event"
Expand Down Expand Up @@ -173,44 +173,42 @@ def run_user_code_and_return_hs_instance(file: Path, request: HttpRequest) -> hs
finally:
set_session_var(request, "hs_script_should_stop", False)
return users_hs_instance
import os
import tempfile
from pathlib import Path


def set_component_value(
request: HttpRequest,
):
component_id = request.GET.get("component_id")

request_server_stop_running_user_script(request, wait=True)

# Handle file uploads
if request.FILES and "new_value" in request.FILES:
uploaded_file = request.FILES["new_value"]

# Create tmp directory if it doesn't exist
tmp_dir = Path("tmp")
tmp_dir.mkdir(exist_ok=True)

# Save file to tmp directory
file_path = tmp_dir / uploaded_file.name
with open(file_path, 'wb') as f:
with open(file_path, "wb") as f:
for chunk in uploaded_file.chunks():
f.write(chunk)

# Set the filename as the value
new_value = uploaded_file.name
print(f'File saved to: {file_path}')
print(f"File saved to: {file_path}")

else:
# Handle regular form data
new_value = request.POST.get("new_value")
if new_value is None:
new_value = request.GET.get("new_value")

set_session_var(request, component_id, new_value)
print('Component value set to:', new_value)
print("Component value set to:", new_value)

response = HttpResponse(f"suc: {request.session[component_id]}", status=200)
response.headers["HX-Reswap"] = "none"
response.headers["HX-Trigger"] = "trigger_run_hs_event"
Expand Down
Loading
Loading