Skip to content

Commit 8b64e02

Browse files
committed
Add file saving and URL posting methods to vCon
- Implement `save_to_file()` method for easy JSON file serialization - Add `post_to_url()` method to send vCons to API endpoints - Update documentation and examples in GUIDE.md and README.md - Include comprehensive error handling and logging - Add corresponding test cases for new methods
1 parent 8224f5f commit 8b64e02

File tree

6 files changed

+218
-3
lines changed

6 files changed

+218
-3
lines changed

GUIDE.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,33 @@ json_str = vcon.dumps()
9999

100100
# To dictionary
101101
dict_data = vcon.to_dict()
102+
103+
# Save to file
104+
vcon.save_to_file("conversation.json")
105+
106+
# Post to URL with custom headers
107+
response = vcon.post_to_url(
108+
'https://api.example.com/vcons',
109+
headers={
110+
'x-conserver-api-token': 'your-token-here',
111+
'x-custom-header': 'custom-value'
112+
}
113+
)
114+
if response.status_code == 200:
115+
print("Successfully posted vCon")
102116
```
103117

118+
The `save_to_file` method allows you to save a vCon directly to a JSON file:
119+
- Takes a file path as argument
120+
- Automatically handles JSON serialization
121+
- Raises IOError if there are file permission issues
122+
123+
The `post_to_url` method enables sending a vCon to a URL endpoint:
124+
- Automatically sets Content-Type to application/json
125+
- Supports custom headers for authentication and other purposes
126+
- Returns a requests.Response object for handling the server response
127+
- Raises requests.RequestException for network/server errors
128+
104129
### Tags
105130
```python
106131
# Add a tag

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ else:
9595

9696
# Serialize to JSON
9797
json_data = vcon.to_json()
98+
99+
# Save to file
100+
vcon.save_to_file("conversation.json")
101+
102+
# Post to URL with authentication
103+
response = vcon.post_to_url(
104+
'https://api.example.com/vcons',
105+
headers={'x-conserver-api-token': 'your-token-here'}
106+
)
98107
```
99108

100109
## File Validation
@@ -134,6 +143,17 @@ except requests.RequestException:
134143
print("Error fetching from URL")
135144
except json.JSONDecodeError:
136145
print("Invalid JSON format")
146+
147+
# Save a vCon to file
148+
vcon.save_to_file("conversation.json")
149+
150+
# Post a vCon to a URL with custom headers
151+
response = vcon.post_to_url(
152+
'https://api.example.com/vcons',
153+
headers={'x-conserver-api-token': 'your-token-here'}
154+
)
155+
if response.status_code == 200:
156+
print("Successfully posted vCon")
137157
```
138158

139159
The validation checks include:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "vcon"
3-
version = "0.4.1"
3+
version = "0.5.0"
44
description = "The vCon library"
55
authors = ["Thomas McCarthy-Howe <ghostofbasho@gmail.com>"]
66
license = "MIT"

samples/example.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from vcon import Vcon
1212
from vcon.party import Party
1313
from vcon.dialog import Dialog
14+
import requests
1415

1516

1617
def main():
@@ -123,8 +124,37 @@ def main():
123124

124125
# Save the vCon to a file
125126
output_filename = "example.vcon.json"
126-
with open(output_filename, "w") as file:
127-
file.write(vcon.to_json())
127+
try:
128+
vcon.save_to_file(output_filename)
129+
print(f"Successfully saved vCon to {output_filename}")
130+
except IOError as e:
131+
print(f"Error saving vCon to file: {str(e)}")
132+
133+
# Post the vCon to a server (example using httpbin.org as a test endpoint)
134+
try:
135+
# Example with authentication and custom headers
136+
headers = {
137+
'x-conserver-api-token': 'your-token-here',
138+
'x-custom-header': 'test-value'
139+
}
140+
response = vcon.post_to_url(
141+
'https://httpbin.org/post', # Test endpoint that echoes back the request
142+
headers=headers
143+
)
144+
145+
if response.status_code == 200:
146+
print("Successfully posted vCon to server")
147+
# The response from httpbin.org includes the sent data and headers
148+
response_data = response.json()
149+
print("Server received our custom headers:")
150+
for header, value in response_data['headers'].items():
151+
if header.lower().startswith('x-'):
152+
print(f" {header}: {value}")
153+
else:
154+
print(f"Server returned status code: {response.status_code}")
155+
156+
except requests.RequestException as e:
157+
print(f"Error posting vCon to server: {str(e)}")
128158

129159

130160
if __name__ == "__main__":

src/vcon/vcon.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,3 +1078,62 @@ def load_from_url(cls, url: str) -> Vcon:
10781078
response = requests.get(url)
10791079
response.raise_for_status() # Raise an exception for bad status codes
10801080
return cls.build_from_json(response.text)
1081+
1082+
def save_to_file(self, file_path: str) -> None:
1083+
"""
1084+
Save the vCon to a JSON file.
1085+
1086+
:param file_path: Path where the vCon JSON should be saved
1087+
:type file_path: str
1088+
:raises IOError: If there is an error writing to the file
1089+
"""
1090+
logger.debug(f"Saving vCon to file: {file_path}")
1091+
try:
1092+
with open(file_path, 'w') as f:
1093+
f.write(self.to_json())
1094+
logger.info(f"Successfully saved vCon to {file_path}")
1095+
except IOError as e:
1096+
logger.error(f"Failed to save vCon to file: {str(e)}")
1097+
raise
1098+
1099+
def post_to_url(self, url: str, headers: Optional[Dict[str, str]] = None) -> requests.Response:
1100+
"""
1101+
Post the vCon as JSON to a URL with optional headers.
1102+
1103+
:param url: The URL to post the vCon to
1104+
:type url: str
1105+
:param headers: Optional dictionary of HTTP headers (e.g., {'x-conserver-api-token': 'token123'})
1106+
:type headers: Optional[Dict[str, str]]
1107+
:return: The HTTP response from the server
1108+
:rtype: requests.Response
1109+
:raises requests.RequestException: If there is an error making the HTTP request
1110+
1111+
Example:
1112+
>>> vcon = Vcon.build_new()
1113+
>>> response = vcon.post_to_url(
1114+
... 'https://api.example.com/vcons',
1115+
... headers={'x-conserver-api-token': 'your-token-here'}
1116+
... )
1117+
>>> print(response.status_code) # Prints HTTP status code (e.g., 200 for success)
1118+
"""
1119+
logger.debug(f"Posting vCon to URL: {url}")
1120+
1121+
# Prepare headers
1122+
request_headers = {
1123+
'Content-Type': 'application/json'
1124+
}
1125+
if headers:
1126+
request_headers.update(headers)
1127+
1128+
try:
1129+
response = requests.post(
1130+
url,
1131+
data=self.to_json(),
1132+
headers=request_headers
1133+
)
1134+
response.raise_for_status()
1135+
logger.info(f"Successfully posted vCon to {url}")
1136+
return response
1137+
except requests.RequestException as e:
1138+
logger.error(f"Failed to post vCon to URL: {str(e)}")
1139+
raise

tests/test_vcon.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,3 +770,84 @@ def test_load_detects_file_vs_url() -> None:
770770
# Restore original methods
771771
Vcon.load_from_file = original_load_from_file
772772
Vcon.load_from_url = original_load_from_url
773+
774+
775+
def test_save_to_file(tmp_path):
776+
"""Test saving a vCon to a file"""
777+
# Create a vCon with known content
778+
vcon = Vcon.build_from_json(test_vcon_string)
779+
780+
# Save to a temporary file
781+
file_path = tmp_path / "saved_vcon.json"
782+
vcon.save_to_file(str(file_path))
783+
784+
# Verify the file exists and contains correct content
785+
assert file_path.exists()
786+
with open(file_path, 'r') as f:
787+
saved_content = f.read()
788+
assert json.loads(saved_content) == json.loads(vcon.to_json())
789+
790+
791+
def test_save_to_file_permission_error(tmp_path):
792+
"""Test saving to a file with no write permissions raises IOError"""
793+
vcon = Vcon.build_new()
794+
file_path = tmp_path / "readonly.json"
795+
796+
# Create a read-only directory
797+
file_path.parent.chmod(0o444)
798+
799+
with pytest.raises(IOError):
800+
vcon.save_to_file(str(file_path))
801+
802+
803+
@pytest.mark.vcr()
804+
def test_post_to_url():
805+
"""Test posting a vCon to a URL"""
806+
vcon = Vcon.build_new()
807+
url = "https://httpbin.org/post" # Test endpoint that echoes back the request
808+
809+
# Test with custom headers
810+
headers = {
811+
'x-conserver-api-token': 'test-token',
812+
'x-custom-header': 'test-value'
813+
}
814+
815+
response = vcon.post_to_url(url, headers=headers)
816+
817+
# Verify the response
818+
assert response.status_code == 200
819+
response_data = response.json()
820+
821+
# Verify the sent data matches our vCon
822+
assert json.loads(response_data['data']) == json.loads(vcon.to_json())
823+
824+
# Verify headers were sent correctly
825+
assert response_data['headers']['Content-Type'] == 'application/json'
826+
assert response_data['headers']['X-Conserver-Api-Token'] == 'test-token'
827+
assert response_data['headers']['X-Custom-Header'] == 'test-value'
828+
829+
830+
@pytest.mark.vcr()
831+
def test_post_to_url_no_headers():
832+
"""Test posting a vCon to a URL without custom headers"""
833+
vcon = Vcon.build_new()
834+
url = "https://httpbin.org/post"
835+
836+
response = vcon.post_to_url(url)
837+
838+
assert response.status_code == 200
839+
response_data = response.json()
840+
841+
# Verify only default headers were sent
842+
assert response_data['headers']['Content-Type'] == 'application/json'
843+
assert 'X-Conserver-Api-Token' not in response_data['headers']
844+
845+
846+
@pytest.mark.vcr()
847+
def test_post_to_url_error():
848+
"""Test posting to an invalid URL raises RequestException"""
849+
vcon = Vcon.build_new()
850+
url = "https://nonexistent.example.com"
851+
852+
with pytest.raises(requests.RequestException):
853+
vcon.post_to_url(url)

0 commit comments

Comments
 (0)