Skip to content

Commit 556d1e0

Browse files
authored
Merge pull request #288 from poissoncorp/skill-csharp-sync
Csharp sync skill
2 parents 48868a8 + c0c77d0 commit 556d1e0

4 files changed

Lines changed: 288 additions & 0 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Naming & Code Conventions
2+
3+
## Naming map
4+
5+
| C# | Python |
6+
|---|---|
7+
| `PascalCase` class | `PascalCase` class |
8+
| `PascalCase` method | `snake_case` method |
9+
| `PascalCase` property | `snake_case` attribute |
10+
| JSON keys in `ToJson()` | `"PascalCase"` in `to_json()` |
11+
| JSON keys in `FromJson()` | `"PascalCase"` in `from_json()` |
12+
| `IOperation<T>` | `IOperation[T]` |
13+
| `IMaintenanceOperation` | `IMaintenanceOperation` |
14+
| `IVoidMaintenanceOperation` | `VoidMaintenanceOperation` |
15+
| `null` | `None` |
16+
| `TimeSpan.FromDays(n)` | `timedelta(days=n)` |
17+
| `DateTime.UtcNow` | `datetime.datetime.now(datetime.timezone.utc)` β€” NEVER `utcnow()` |
18+
19+
## Operation class pattern
20+
21+
```python
22+
class MyNewOperation(IMaintenanceOperation[MyResult]):
23+
def __init__(self, config: MyConfig):
24+
self._config = config
25+
26+
def get_command(self, conventions: "DocumentConventions") -> RavenCommand[MyResult]:
27+
return self._MyCommand(self._config)
28+
29+
class _MyCommand(RavenCommand[MyResult], RaftCommand):
30+
def __init__(self, config: MyConfig):
31+
super().__init__(MyResult)
32+
self._config = config
33+
34+
def is_read_request(self) -> bool:
35+
return False
36+
37+
def create_request(self, node: ServerNode) -> requests.Request:
38+
request = requests.Request(
39+
"PUT", f"{node.url}/databases/{node.database}/my-endpoint"
40+
)
41+
request.data = self._config.to_json()
42+
return request
43+
44+
def set_response(self, response: str, from_cache: bool) -> None:
45+
if response:
46+
self.result = MyResult.from_json(json.loads(response))
47+
48+
def get_raft_unique_request_id(self) -> str:
49+
return RaftIdGenerator.new_id()
50+
```
51+
52+
## Model / DTO pattern
53+
54+
```python
55+
class MyConfig:
56+
def __init__(self, name: str = None, items: Optional[Dict] = None):
57+
self.name = name
58+
self.items = items
59+
60+
def to_json(self) -> dict:
61+
result = {}
62+
if self.name is not None:
63+
result["Name"] = self.name
64+
if self.items is not None:
65+
result["Items"] = {k: v.to_json() for k, v in self.items.items()}
66+
return result
67+
68+
@classmethod
69+
def from_json(cls, json_dict: dict) -> "MyConfig":
70+
obj = cls.__new__(cls)
71+
obj.name = json_dict.get("Name")
72+
# parse nested objects ...
73+
return obj
74+
```
75+
76+
## Exception wiring
77+
78+
1. Create the class inheriting `RavenException` in `ravendb/exceptions/`.
79+
2. Register it in `exception_dispatcher.py` β†’ `_EXCEPTION_MAP` using the **short C# type name** as key (e.g. `"SchemaValidationException"`).
80+
3. `RavenException.__init__` stores message as a plain string in `args[0]` β€” never a tuple.
81+
82+
## Enum pattern
83+
84+
```python
85+
class MyEnum(enum.Enum):
86+
VALUE_ONE = "ValueOne" # match the C# string / JSON value
87+
VALUE_TWO = "ValueTwo"
88+
```
89+
90+
## Import style
91+
92+
- Absolute imports from `ravendb.*`.
93+
- Group: stdlib β†’ third-party β†’ project, separated by blank lines.
94+
- Use `TYPE_CHECKING` guard for circular-import-prone types.
95+
96+
## Verifying against C# source
97+
98+
When unsure about a field name, method signature, or serialization key, fetch the original C# file:
99+
100+
```
101+
https://raw.githubusercontent.com/ravendb/ravendb/refs/heads/v7.2/src/Raven.Client/<path>.cs
102+
```
103+
104+
105+
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
---
2+
name: csharp-sync
3+
description: Migrates RavenDB C# client features to the Python client. Use when porting C# diffs, patches, or test files to Python, batching sync work, or writing migration tests.
4+
---
5+
6+
# C# β†’ Python Client Sync
7+
8+
Migrate features from the RavenDB C# client into this Python client codebase.
9+
Input is a raw `.patch` / `.diff` from the C# repo. Output is working Python code with tests.
10+
11+
## Pipeline
12+
13+
1. **Triage** β€” read the diff, list every changed file, group into batches
14+
2. **Implement** β€” port each batch following [CONVENTIONS.md](CONVENTIONS.md)
15+
3. **Test** β€” migrate or write tests following [TESTING.md](TESTING.md)
16+
17+
## Reference sources
18+
19+
When triaging or implementing, cross-reference with the upstream C# source and RavenDB docs.
20+
21+
### C# source (GitHub raw)
22+
23+
Base URL for the `v7.2` branch:
24+
```
25+
https://raw.githubusercontent.com/ravendb/ravendb/refs/heads/v7.2/src/Raven.Client/
26+
```
27+
28+
To read a C# file, build the URL from its namespace path:
29+
- `Documents/Operations/Attachments/PutAttachmentOperation.cs`
30+
- `Documents/Commands/Batches/PutAttachmentCommandData.cs`
31+
- `Documents/Indexes/IndexDefinition.cs`
32+
- `Exceptions/Documents/DocumentDoesNotExistException.cs`
33+
34+
To discover what's inside a directory, fetch the GitHub API:
35+
```
36+
https://api.github.com/repos/ravendb/ravendb/contents/src/Raven.Client/Documents/Operations?ref=v7.2
37+
```
38+
This returns a JSON array of `{name, path, type}` entries β€” use it to list files before fetching specific ones.
39+
40+
For **tests**, the base path is:
41+
```
42+
https://raw.githubusercontent.com/ravendb/ravendb/refs/heads/v7.2/test/SlowTests/
43+
```
44+
With the API equivalent:
45+
```
46+
https://api.github.com/repos/ravendb/ravendb/contents/test/SlowTests?ref=v7.2
47+
```
48+
49+
### RavenDB documentation
50+
51+
Base URL: `https://docs.ravendb.net/7.2`
52+
53+
Key sections to check during triage:
54+
- `document-extensions/attachments/` β€” attachment operations
55+
- `documents/schema-validation/` β€” JSON schema validation
56+
- `indexes/` β€” index definitions, schema in indexes
57+
- `ai-integration/` β€” AI / embeddings features
58+
59+
Use docs to determine whether a C# change is:
60+
1. **A documented feature** β†’ must be ported
61+
2. **C#-specific plumbing** (e.g. `IDisposable`, `Span<T>`) β†’ skip
62+
3. **Related to a feature not yet synced** β†’ defer to a later batch
63+
64+
## Step 1 β€” Triage the diff
65+
66+
1. Read the full diff. For each changed C# file write a one-line summary.
67+
2. For unfamiliar changes, **fetch the full C# source** from GitHub to understand context.
68+
3. **Check RavenDB docs** to confirm the feature is documented and understand its scope.
69+
4. **Filter out** changes that are C#-specific or relate to features not yet in the Python client.
70+
5. Group remaining changes into **batches** by feature area. Good boundaries:
71+
- New operation class (e.g. `ConfigureRemoteAttachmentsOperation`)
72+
- New model / DTO cluster (e.g. settings + configuration classes)
73+
- New exception type + dispatcher wiring
74+
- Test migration for an already-implemented feature
75+
6. Create a **checklist** β€” one `- [ ]` per batch, ordered by dependency (models β†’ operations β†’ tests).
76+
77+
### Batch sizing
78+
79+
- Target **≀ 300 lines of Python** per batch.
80+
- If a single C# file maps to > 300 lines, split by class or logical section.
81+
- Tests are their own batch, listed after the implementation batch they cover.
82+
83+
## Step 2 β€” Implement each batch
84+
85+
Follow the naming rules and patterns in [CONVENTIONS.md](CONVENTIONS.md).
86+
87+
When porting a C# class, **always fetch the full source from GitHub** β€” diffs alone often lack constructor signatures, base classes, or `ToJson`/`FromJson` methods needed for a correct port.
88+
89+
For every edit:
90+
1. Use `codebase-retrieval` to find ALL downstream callers, implementations, and tests.
91+
2. Update every affected file β€” missing a downstream change is a critical failure.
92+
3. After editing, verify imports resolve and no existing tests are broken.
93+
4. If unsure about a field or method, check the **RavenDB docs** for the canonical behavior.
94+
95+
### Key files to touch per feature
96+
97+
| What | Where |
98+
|---|---|
99+
| New operation / model | `ravendb/documents/operations/<feature>/__init__.py` |
100+
| New exception | `ravendb/exceptions/raven_exceptions.py` or subpackage |
101+
| Exception dispatcher map | `ravendb/exceptions/exception_dispatcher.py` |
102+
| DatabaseRecord fields | `ravendb/serverwide/database_record.py` |
103+
| IndexDefinition fields | `ravendb/documents/indexes/definitions.py` |
104+
| Statistics fields | `ravendb/documents/operations/statistics.py` |
105+
| Session-level changes | `ravendb/documents/session/` submodules |
106+
| Bulk insert changes | `ravendb/documents/bulk_insert_operation.py` |
107+
| Module exports | `ravendb/__init__.py` |
108+
109+
## Step 3 β€” Write tests
110+
111+
Follow the infrastructure and patterns in [TESTING.md](TESTING.md).
112+
113+
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Testing Conventions
2+
3+
## Infrastructure
4+
5+
- All tests extend `TestBase` from `ravendb/tests/test_base.py`.
6+
- `self.store` is pre-configured with a fresh database per test β€” no manual setup needed.
7+
- Run tests: `.venv1\Scripts\python.exe -m unittest <module.path> -v`
8+
9+
## Assertion helpers
10+
11+
| Helper | When to use |
12+
|---|---|
13+
| `self.assertRaisesWithMessageContaining(func, ExType, "substring")` | Server-side exceptions β€” the dispatcher includes the full C# stack trace, so exact match is fragile |
14+
| `self.assertRaisesWithMessage(func, ExType, "exact")` | Client-side exceptions only (e.g. `ValueError`) |
15+
| `self.assertFalse(x)` | Prefer over `assertIsNone` when the server may return `[]` instead of `null` |
16+
| `self.wait_for_indexing(self.store)` | Call after creating indexes, before querying |
17+
18+
## Test file placement
19+
20+
| C# test location | Python target |
21+
|---|---|
22+
| `SlowTests.Server.Documents.*` | `ravendb/tests/operations_tests/` |
23+
| `SlowTests.Client.*` | `ravendb/tests/jvm_migrated_tests/client_tests/` |
24+
| `FastTests.Client.Attachments.*` | `ravendb/tests/jvm_migrated_tests/attachments_tests/` |
25+
| `SlowTests.Issues.RavenDB_XXXXX` | `ravendb/tests/jvm_migrated_tests/issues_tests/` |
26+
27+
## Test structure
28+
29+
```python
30+
class TestMyFeature(TestBase):
31+
def setUp(self):
32+
super().setUp()
33+
34+
def test_should_do_something(self):
35+
# 1. Arrange β€” configure the feature
36+
config = MyConfig(name="test")
37+
self.store.maintenance.send(ConfigureMyFeatureOperation(config))
38+
39+
# 2. Act
40+
with self.store.open_session() as session:
41+
session.store({"field": "value"}, "docs/1")
42+
session.save_changes()
43+
44+
# 3. Assert
45+
with self.store.open_session() as session:
46+
doc = session.load("docs/1")
47+
self.assertEqual("value", doc["field"])
48+
```
49+
50+
## Index query projection pattern
51+
52+
When an index projects custom fields, create a lightweight result class:
53+
54+
```python
55+
class _IndexResult:
56+
def __init__(self, Id=None, Errors=None):
57+
self.Id = Id
58+
self.Errors = Errors
59+
```
60+
61+
Then query with `select_fields(_IndexResult, "Id", "Errors")`.
62+
63+
## Common gotchas
64+
65+
- **Datetime**: always `datetime.datetime.now(datetime.timezone.utc)`, never `utcnow()`.
66+
- **Empty vs None**: server may return `[]` instead of `null` for empty collections β€” use `assertFalse`/`assertTrue` not `assertIsNone`/`assertIsNotNone`.
67+
- **Exception messages**: the dispatcher wraps the full server error (message + stack trace) into the exception string β€” always use `assertRaisesWithMessageContaining` with a meaningful substring.
68+
- **`RavenException.__init__`**: stores message as plain string in `args[0]`, not `(message, cause)` tuple.
69+
- **Async operations**: use `store.maintenance.send_async(op)` β†’ `op.wait_for_completion()` β†’ `op.fetch_operations_status()["Result"]`.
70+
File renamed without changes.

0 commit comments

Comments
Β (0)