feat: Add Advoware History and Watcher services for document synchronization
- Implement AdvowareHistoryService for fetching and creating history entries. - Implement AdvowareWatcherService for file operations including listing, downloading, and uploading with Blake3 hash verification. - Introduce Blake3 utility functions for hash computation and verification. - Create document sync cron step to poll Redis for pending Aktennummern and emit sync events. - Develop document sync event handler to manage 3-way merge synchronization for Akten, including metadata updates and error handling.
This commit is contained in:
@@ -377,7 +377,37 @@ class EspoCRMAPI:
|
||||
self._log(f"Updating {entity_type} with ID: {entity_id}")
|
||||
return await self.api_call(f"/{entity_type}/{entity_id}", method='PUT', json_data=data)
|
||||
|
||||
async def delete_entity(self, entity_type: str, entity_id: str) -> bool:
|
||||
async def link_entities(
|
||||
self,
|
||||
entity_type: str,
|
||||
entity_id: str,
|
||||
link: str,
|
||||
foreign_id: str
|
||||
) -> bool:
|
||||
"""
|
||||
Link two entities together (create relationship).
|
||||
|
||||
Args:
|
||||
entity_type: Parent entity type
|
||||
entity_id: Parent entity ID
|
||||
link: Link name (relationship field)
|
||||
foreign_id: ID of entity to link
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
|
||||
Example:
|
||||
await espocrm.link_entities('CAdvowareAkten', 'akte123', 'dokumente', 'doc456')
|
||||
"""
|
||||
self._log(f"Linking {entity_type}/{entity_id} → {link} → {foreign_id}")
|
||||
await self.api_call(
|
||||
f"/{entity_type}/{entity_id}/{link}",
|
||||
method='POST',
|
||||
json_data={"id": foreign_id}
|
||||
)
|
||||
return True
|
||||
|
||||
async def delete_entity(self, entity_type: str,entity_id: str) -> bool:
|
||||
"""
|
||||
Delete an entity.
|
||||
|
||||
@@ -494,6 +524,99 @@ class EspoCRMAPI:
|
||||
self._log(f"Upload failed: {e}", level='error')
|
||||
raise EspoCRMError(f"Upload request failed: {e}") from e
|
||||
|
||||
async def upload_attachment_for_file_field(
|
||||
self,
|
||||
file_content: bytes,
|
||||
filename: str,
|
||||
related_type: str,
|
||||
field: str,
|
||||
mime_type: str = 'application/octet-stream'
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Upload an attachment for a File field (2-step process per EspoCRM API).
|
||||
|
||||
This is Step 1: Upload the attachment without parent, specifying relatedType and field.
|
||||
Step 2: Create/update the entity with {field}Id set to the attachment ID.
|
||||
|
||||
Args:
|
||||
file_content: File content as bytes
|
||||
filename: Name of the file
|
||||
related_type: Entity type that will contain this attachment (e.g., 'CDokumente')
|
||||
field: Field name in the entity (e.g., 'dokument')
|
||||
mime_type: MIME type of the file
|
||||
|
||||
Returns:
|
||||
Attachment entity data with 'id' field
|
||||
|
||||
Example:
|
||||
# Step 1: Upload attachment
|
||||
attachment = await espocrm.upload_attachment_for_file_field(
|
||||
file_content=file_bytes,
|
||||
filename="document.pdf",
|
||||
related_type="CDokumente",
|
||||
field="dokument",
|
||||
mime_type="application/pdf"
|
||||
)
|
||||
|
||||
# Step 2: Create entity with dokumentId
|
||||
doc = await espocrm.create_entity('CDokumente', {
|
||||
'name': 'document.pdf',
|
||||
'dokumentId': attachment['id']
|
||||
})
|
||||
"""
|
||||
import base64
|
||||
|
||||
self._log(f"Uploading attachment for File field: {filename} ({len(file_content)} bytes) -> {related_type}.{field}")
|
||||
|
||||
# Encode file content to base64
|
||||
file_base64 = base64.b64encode(file_content).decode('utf-8')
|
||||
data_uri = f"data:{mime_type};base64,{file_base64}"
|
||||
|
||||
url = self.api_base_url.rstrip('/') + '/Attachment'
|
||||
headers = {
|
||||
'X-Api-Key': self.api_key,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
payload = {
|
||||
'name': filename,
|
||||
'type': mime_type,
|
||||
'role': 'Attachment',
|
||||
'relatedType': related_type,
|
||||
'field': field,
|
||||
'file': data_uri
|
||||
}
|
||||
|
||||
self._log(f"Upload params: relatedType={related_type}, field={field}, role=Attachment")
|
||||
|
||||
effective_timeout = aiohttp.ClientTimeout(total=self.api_timeout_seconds)
|
||||
|
||||
session = await self._get_session()
|
||||
try:
|
||||
async with session.post(url, headers=headers, json=payload, timeout=effective_timeout) as response:
|
||||
self._log(f"Upload response status: {response.status}")
|
||||
|
||||
if response.status == 401:
|
||||
raise EspoCRMAuthError("Authentication failed - check API key")
|
||||
elif response.status == 403:
|
||||
raise EspoCRMError("Access forbidden")
|
||||
elif response.status == 404:
|
||||
raise EspoCRMError(f"Attachment endpoint not found")
|
||||
elif response.status >= 400:
|
||||
error_text = await response.text()
|
||||
self._log(f"❌ Upload failed with {response.status}. Response: {error_text}", level='error')
|
||||
raise EspoCRMError(f"Upload error {response.status}: {error_text}")
|
||||
|
||||
# Parse response
|
||||
result = await response.json()
|
||||
attachment_id = result.get('id')
|
||||
self._log(f"✅ Attachment uploaded successfully: {attachment_id}")
|
||||
return result
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
self._log(f"Upload failed: {e}", level='error')
|
||||
raise EspoCRMError(f"Upload request failed: {e}") from e
|
||||
|
||||
async def download_attachment(self, attachment_id: str) -> bytes:
|
||||
"""
|
||||
Download an attachment from EspoCRM.
|
||||
|
||||
Reference in New Issue
Block a user