Metadata-Version: 2.4
Name: alfresco-rest-client
Version: 1.0.0
Summary: Python client for Alfresco Content Services
Author: Alfresco Contributors
License: MIT
Project-URL: Homepage, https://github.com/Alfresco/alfresco-python-client
Classifier: Development Status :: 1 - Planning
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.28.0
Provides-Extra: oauth
Requires-Dist: authlib>=1.2.0; extra == "oauth"
Provides-Extra: dev
Requires-Dist: setuptools>=68.0; extra == "dev"
Requires-Dist: wheel>=0.41.0; extra == "dev"
Requires-Dist: build>=1.0.0; extra == "dev"
Requires-Dist: authlib>=1.2.0; extra == "dev"

# alfresco-rest-client

Python client for Alfresco Content Services REST API, modelled after
[nuxeo-python-client](https://github.com/nuxeo/nuxeo-python-client).

## Quick start

```python
from alfresco import Alfresco, BasicAuth

client = Alfresco(url="http://localhost:8080", auth=BasicAuth("admin", "admin"))

# Nodes (files & folders)
root = client.nodes.get("-root-")
children = client.nodes.list_children("-root-")
folder = client.nodes.create_folder("-root-", "Reports")
uploaded = client.nodes.upload(folder.id, file_path="/tmp/q1.pdf")
content = client.nodes.get_content(uploaded.id)
client.nodes.copy(uploaded.id, root.id, name="q1-copy.pdf")
client.nodes.delete(uploaded.id)

# People (users)
me = client.people.get("-me-")

# Groups
groups = client.groups.list()

# Sites
sites = client.sites.list()
site = client.sites.create("eng", "Engineering", visibility="PRIVATE")

# Search (AFTS or CMIS)
results = client.search.search("cm:name:'budget*'")
results = client.search.search("SELECT * FROM cmis:document", language="cmis")

# Trashcan
deleted = client.trashcan.list()

# Comments
client.comments.create(uploaded.id, "LGTM!")

# Tags
client.tags.add_to_node(uploaded.id, ["reviewed"])

# Favorites
client.favorites.add("-me-", uploaded.id)

# Context-manager support
with Alfresco(url="http://localhost:8080", auth=BasicAuth("admin", "admin")) as c:
    c.nodes.get("-root-")

client.close()
```

## Authentication

| Method | Class | Notes |
|--------|-------|-------|
| HTTP Basic | `alfresco.BasicAuth(user, password)` | Simple, works everywhere |
| Ticket | `alfresco.TicketAuth(user, password)` | Obtains a login ticket automatically |
| OAuth 2.0 | `alfresco.OAuth2Auth(...)` | Bearer-token auth (Keycloak / Alfresco Identity Service) |

### OAuth 2.0 example

Password (Resource Owner Password Credentials) grant:

```python
from alfresco import Alfresco, OAuth2Auth

auth = OAuth2Auth(
    token_url="http://localhost:8081/auth/realms/alfresco/protocol/openid-connect/token",
    client_id="alfresco",
    username="admin",
    password="admin",
)
client = Alfresco("http://localhost:8080", auth=auth)
me = client.people.get("-me-")
```

Client-credentials grant (service account):

```python
auth = OAuth2Auth(
    token_url="https://idp/realms/r/protocol/openid-connect/token",
    client_id="my-service",
    client_secret="…",
    grant_type="client_credentials",
)
```

Pre-fetched access token (e.g. from a browser-based auth code flow):

```python
auth = OAuth2Auth.from_token(
    access_token="eyJhbGciOi…",
    refresh_token="…",
    token_url="…",
    client_id="…",
)
```

The handler caches the token, refreshes automatically before expiry (with a
30 s skew), and falls back to a full re-authentication if the refresh token
is rejected. Call `auth.invalidate()` to force a re-auth.

## Local development

```bash
# Create & activate a virtual environment
python3 -m venv .venv
source .venv/bin/activate

# Install build / dev tools
make install-dev

# Install the package in editable mode
pip install -e .

# Run smoke test
python3 tests/test_smoke.py

# Build wheel + sdist (PEP 517)
make build


# Remove build artifacts
make clean
```

Built packages are placed in `dist/`.

## Releasing

Publishing is automated with GitHub Actions. Both publishing workflows use
**PyPI Trusted Publishing (OIDC)** — there are no API tokens stored in the repo.

### Release → PyPI (`.github/workflows/release.yml`)

Triggered by a **push to `main` whose commit message contains `[release]`**, or
manually via **Run workflow** (`workflow_dispatch`).

> **Before releasing, set the correct release version.** The version that gets
> tagged and published is read from `__version__` in `alfresco/__init__.py`, so
> edit that value to the version you intend to release **before** triggering the
> workflow. The `check` job fails if the matching `v<version>` git tag already
> exists, so you cannot accidentally re-release an existing version.

1. **check** — reads the version from `alfresco/__init__.py` (`__version__`) and
   fails if the `v<version>` git tag already exists.
2. **tests** — runs the full suite (`tests.yml`). Add `[skip test]` to the commit
   message (or set `skip_tests=true` on a manual run) to bypass this step.
3. **build** — builds the wheel + sdist (PEP 517).
4. **publish** — uploads to [PyPI](https://pypi.org/) from the `pypi` environment.
5. **tag** — creates the `v<version>` git tag and a GitHub Release with the
   built artifacts attached.
6. **bump** — after a successful release, advances `__version__` on `main` to the
   next development version (auto-increments the patch, e.g. `0.1.0 → 0.1.1`, or
   uses the `next_version` input on a manual run) and pushes a
   `Bump version to … [skip ci]` commit.

To cut a release: set `__version__` to the release version, then land a commit on
`main`, e.g. `git commit -m "Release 0.1.0 [release]"`. The next development
version is set automatically by the **bump** step.

### Snapshot → TestPyPI (`.github/workflows/snapshot.yml`)

Publishes dev builds versioned `X.Y.Z.dev<run-number>` to
[TestPyPI](https://test.pypi.org/) from the `testpypi` environment. It runs when:

* any push lands on `main` (automatic), **or**
* a push on a working branch (`feature/**`, `fix/**`, `release/**`) has a commit
  message containing `[snapshot]`.

Release commits (`[release]`) are always skipped here to avoid double-publishing,
and the test suite must pass before a snapshot is published.

Install a snapshot with:

```bash
pip install -i https://test.pypi.org/simple/ --pre alfresco-rest-client
```

> All third-party actions are pinned to commit SHAs, and each publishing job runs
> in a dedicated protected GitHub Environment (`pypi` / `testpypi`).


