Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d6a7a87
feat(object): add base class and increment method
0yenga Apr 1, 2026
5ea4a49
feat(object): add base class and increment method
0yenga Apr 1, 2026
35b0bf0
feat(object): add add_to_array method
0yenga Apr 1, 2026
64a3199
feat(object): add add_unique method
0yenga Apr 1, 2026
b191e49
feat(object): add remove_from_array method
0yenga Apr 1, 2026
42ec42d
feat(object): add unset method
0yenga Apr 1, 2026
6fb8328
test(object): add unit tests for ParseObject atomic operations
0yenga Apr 1, 2026
f0976df
test(object): refine unit tests and fix linting issues
0yenga Apr 1, 2026
ee39962
fix(types): rename unused param to _data in DeleteField.from_parse
0yenga Apr 2, 2026
ffa7cc9
chore: remove PDF formation file and add *.pdf to .gitignore
0yenga Apr 2, 2026
6c02636
fix(exceptions): fix raise_parse_error crash for specialized construc…
0yenga Apr 2, 2026
4b3989f
chore(deps): move unused pydantic and websockets to optional extras
0yenga Apr 2, 2026
ecd803c
fix(object): decode parse types in ParseObject.get()
0yenga Apr 2, 2026
1c1faff
chore(client): remove empty TYPE_CHECKING block
0yenga Apr 2, 2026
40aee47
fix(init): update obsolete comment
0yenga Apr 2, 2026
3c8db39
chore: fix CONTRIBUTING.md markdown and add GitHub Actions CI workflow
0yenga Apr 2, 2026
6a7840d
feat(object): add atomic operations, sync CRUD, and special type seri…
0yenga Apr 5, 2026
426bd11
test(object): add unit tests for atomic operations and sync methods
0yenga Apr 5, 2026
fe2f503
docs: update roadmap status for ParseObject
0yenga Apr 5, 2026
7176818
chore: fix formatting and Ruff SIM117 errors in tests
0yenga Apr 6, 2026
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
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: CI

on:
push:
branches: [ "main", "develop" ]
pull_request:
branches: [ "main", "develop" ]

jobs:
test:
name: Test on Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"

- name: Check code style (Black)
run: black --check src/ tests/

- name: Lint (Ruff)
run: ruff check src/ tests/

- name: Type checking (Mypy)
run: mypy src/

- name: Run unit tests
run: pytest tests/unit/
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,8 @@ cython_debug/
marimo/_static/
marimo/_lsp/
__marimo__/

# Documents de formation / présentations — ne pas versionner dans le SDK
*.pdf
*.pptx
*.docx
10 changes: 4 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ git --version # git version 2.x

### 1. Forker et cloner le dépôt

`````bash
```bash
# 1. Forkez le projet depuis GitHub (bouton "Fork" en haut à droite)

# 2. Clonez votre fork localement
Expand All @@ -75,18 +75,16 @@ cd parsepy

# 3. Ajoutez le dépôt original comme remote "upstream"
git remote add upstream https://github.com/Kether-Labs/parsepy.git
```

### 4. Créer un environnement virtuel
### 2. Créer un environnement virtuel

```bash
# Avec venv (standard)
python -m venv .venv
source .venv/bin/activate # Linux / macOS
.venv\Scripts\activate # Windows
````

`````

```
### 3. Installer les dépendances de développement

```bash
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ Parse Server dispose de SDK officiels pour JavaScript, iOS, Android et .NET —

| Module | Statut |
|---|---|
| `ParseClient` — configuration et HTTP | 🚧 En cours |
| `ParseObject` — CRUD | 📋 Planifié |
| `ParseClient` — configuration et HTTP | ✅ Terminé |
| `ParseObject` — CRUD | ✅ Terminé |
| `ParseQuery` — requêtes | 📋 Planifié |
| `ParseUser` — authentification | 📋 Planifié |
| `ParseFile` — fichiers | 📋 Planifié |
Expand Down
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,23 @@ classifiers = [
requires-python = ">=3.9"
dependencies = [
"httpx>=0.25.0",
"pydantic>=2.0",
"websockets>=12.0",
]

[project.optional-dependencies]
django = ["django>=4.0"]
fastapi = ["fastapi>=0.100", "python-multipart>=0.0.6"]
flask = ["flask>=3.0"]
# Activé automatiquement quand ParseObject utilisera la validation Pydantic
validation = ["pydantic>=2.0"]
# Activé quand le module LiveQuery (WebSocket) sera implémenté
livequery = ["websockets>=12.0"]
all = [
"django>=4.0",
"fastapi>=0.100",
"python-multipart>=0.0.6",
"flask>=3.0",
"pydantic>=2.0",
"websockets>=12.0",
]
dev = [
"pytest>=7.4",
Expand Down
4 changes: 3 additions & 1 deletion src/parse_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@
ParseTimeoutError,
ParseUsernameTakenError,
)
from .object import ParseObject

# NOTE : ParseClient, ParseObject, ParseQuery, ParseUser, ParseFile, etc.
# NOTE : ParseQuery, ParseUser, ParseFile, etc.
# seront ajoutés ici au fur et à mesure de leur implémentation.
# Chaque contributeur qui implémente un module doit aussi l'exporter ici.

Expand All @@ -78,6 +79,7 @@
# Version
"__version__",
"ParseClient",
"ParseObject",
"get_client",
# Types spéciaux
"GeoPoint",
Expand Down
65 changes: 21 additions & 44 deletions src/parse_sdk/_http.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
"""
Couche HTTP interne du SDK Parse Server Python.

Ce module est PRIVÉ. Il ne doit jamais être importé directement par les
utilisateurs du SDK. Tous les modules publics (ParseObject, ParseQuery, etc.)
passent exclusivement par ce module pour leurs requêtes HTTP.

Responsabilités :
- Gérer le client httpx (async + sync)
- Injecter les headers Parse obligatoires
- Retry automatique avec backoff exponentiel
- Convertir les erreurs HTTP en exceptions ParseError
- Logger les requêtes/réponses pour le debug
Couche HTTP interne pour Parse Server.

Usage interne uniquement — ne pas importer manuellement.
Tous les modules (ParseObject, ParseQuery, etc.) passent par ici pour leurs requêtes.
"""

from __future__ import annotations
Expand Down Expand Up @@ -75,25 +67,16 @@ def __init__(
self._max_retries = max_retries
self._session_token: str | None = None

# Client async partagé — évite de rouvrir une connexion à chaque requête
# Client async partagé pour réutiliser les connexions
self._async_client: httpx.AsyncClient | None = None

# ------------------------------------------------------------------
# Gestion du session token (défini par ParseUser après login)
# ------------------------------------------------------------------

def set_session_token(self, token: str | None) -> None:
"""Définit le session token à envoyer dans les requêtes suivantes."""
self._session_token = token

def clear_session_token(self) -> None:
"""Supprime le session token (après logout)."""
self._session_token = None

# ------------------------------------------------------------------
# Construction des headers
# ------------------------------------------------------------------

def _build_headers(
self,
use_master_key: bool = False,
Expand Down Expand Up @@ -125,10 +108,6 @@ def _build_headers(

return headers

# ------------------------------------------------------------------
# Gestion du client async
# ------------------------------------------------------------------

async def _get_async_client(self) -> httpx.AsyncClient:
"""Retourne le client async partagé, en le créant si nécessaire."""
if self._async_client is None or self._async_client.is_closed:
Expand All @@ -139,15 +118,10 @@ async def _get_async_client(self) -> httpx.AsyncClient:
return self._async_client

async def close(self) -> None:
"""Ferme proprement le client HTTP async."""
if self._async_client and not self._async_client.is_closed:
await self._async_client.aclose()
self._async_client = None

# ------------------------------------------------------------------
# Méthode principale : requête async avec retry
# ------------------------------------------------------------------

async def request(
self,
method: str,
Expand Down Expand Up @@ -246,10 +220,6 @@ async def request(

raise last_error or ParseConnectionError(f"Échec de la requête {method} {path}")

# ------------------------------------------------------------------
# Wrapper synchrone
# ------------------------------------------------------------------

def request_sync(
self,
method: str,
Expand Down Expand Up @@ -312,10 +282,6 @@ def request_sync(

raise last_error or ParseConnectionError(f"Échec de la requête {method} {path}")

# ------------------------------------------------------------------
# Traitement de la réponse HTTP
# ------------------------------------------------------------------

def _handle_response(self, response: httpx.Response) -> dict[str, Any]:
"""Parse la réponse HTTP et lève l'exception appropriée si erreur.

Expand All @@ -342,10 +308,6 @@ def _handle_response(self, response: httpx.Response) -> dict[str, Any]:

return body

# ------------------------------------------------------------------
# Helpers HTTP raccourcis
# ------------------------------------------------------------------

async def get(self, path: str, **kwargs: Any) -> dict[str, Any]:
"""GET asynchrone."""
return await self.request("GET", path, **kwargs)
Expand All @@ -359,5 +321,20 @@ async def put(self, path: str, **kwargs: Any) -> dict[str, Any]:
return await self.request("PUT", path, **kwargs)

async def delete(self, path: str, **kwargs: Any) -> dict[str, Any]:
"""DELETE asynchrone."""
return await self.request("DELETE", path, **kwargs)

def get_sync(self, path: str, **kwargs: Any) -> dict[str, Any]:
"""GET synchrone."""
return self.request_sync("GET", path, **kwargs)

def post_sync(self, path: str, **kwargs: Any) -> dict[str, Any]:
"""POST synchrone."""
return self.request_sync("POST", path, **kwargs)

def put_sync(self, path: str, **kwargs: Any) -> dict[str, Any]:
"""PUT synchrone."""
return self.request_sync("PUT", path, **kwargs)

def delete_sync(self, path: str, **kwargs: Any) -> dict[str, Any]:
"""DELETE synchrone."""
return self.request_sync("DELETE", path, **kwargs)
2 changes: 1 addition & 1 deletion src/parse_sdk/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ def to_parse(self) -> dict[str, Any]:
return {"__op": "Delete"}

@classmethod
def from_parse(cls, data: dict[str, Any]) -> DeleteField:
def from_parse(cls, _data: dict[str, Any]) -> DeleteField:
return cls()


Expand Down
16 changes: 3 additions & 13 deletions src/parse_sdk/client.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
"""
ParseClient — Point d'entrée principal du SDK.

Ce module fournit la classe publique ParseClient que chaque utilisateur
installe pour configurer sa connexion à Parse Server.
Point d'entrée principal pour configurer le SDK.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from ._http import ParseHTTPClient

if TYPE_CHECKING:
pass

# Variable de module pour le pattern singleton/global
# Instance globale partagée (singleton)
_current_client: ParseHTTPClient | None = None


Expand Down Expand Up @@ -72,15 +64,14 @@ def __init__(
self._validate_required_param(rest_key, "rest_key")
self._validate_required_param(server_url, "server_url")

# Stocker la configuration (optionnel mais utile pour debugging)
self._app_id = app_id
self._rest_key = rest_key
self._server_url = server_url
self._master_key = master_key
self._timeout = timeout
self._max_retries = max_retries

# Créer le client HTTP interne
# Client HTTP interne
self._http_client = ParseHTTPClient(
app_id=app_id,
rest_key=rest_key,
Expand All @@ -90,7 +81,6 @@ def __init__(
max_retries=max_retries,
)

# Enregistrer comme client global
global _current_client
_current_client = self._http_client

Expand Down
9 changes: 8 additions & 1 deletion src/parse_sdk/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,4 +354,11 @@ def raise_parse_error(code: int, message: str) -> None:
exc_class = PARSE_ERROR_MAP.get(code, ParseError)
if exc_class is ParseError:
raise ParseError(code=code, message=message)
raise exc_class(message=message) # type: ignore[call-arg]

# Les classes spécialisées ont des constructeurs qui ne correspondent pas
# tous à la signature (code, message). On crée l'instance via __new__ et
# on l'initialise avec ParseError.__init__ pour conserver le bon type
# tout en passant les bons arguments — isinstance() fonctionne toujours.
exc = exc_class.__new__(exc_class)
ParseError.__init__(exc, code=code, message=message)
raise exc
Loading
Loading