Skip to content

Commit 17fb901

Browse files
committed
fix: use shared HTTP session and json_from_response across all controls
- Extract duplicate try/except aiohttp.ContentTypeError blocks into a shared json_from_response() helper in api/network/utils.py - Replace all per-request aiohttp.ClientSession() instantiations in control.py, files.py, info.py, and non-upload methods of job_control.py with the client's shared session (get_http_session()), which respects the configured 15s timeout and avoids connection churn - File upload methods in job_control.py intentionally keep their own sessions to avoid the 15s timeout constraint during large uploads - Remove now-unused aiohttp imports from control.py and files.py
1 parent fe623d4 commit 17fb901

6 files changed

Lines changed: 143 additions & 216 deletions

File tree

flashforge/api/controls/control.py

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@
44

55
from typing import TYPE_CHECKING, Any
66

7-
import aiohttp
8-
97
from ...models.responses import FilamentArgs
108
from ..constants.commands import Commands
119
from ..constants.endpoints import Endpoints
12-
from ..network.utils import NetworkUtils
10+
from ..network.utils import NetworkUtils, json_from_response
1311

1412
if TYPE_CHECKING:
1513
from ...client import FlashForgeClient
@@ -239,26 +237,16 @@ async def send_control_command(self, command: str, args: dict[str, Any]) -> bool
239237
try:
240238
await self.client.is_http_client_busy()
241239

242-
async with aiohttp.ClientSession() as session:
243-
async with session.post(
244-
self.client.get_endpoint(Endpoints.CONTROL),
245-
json=payload,
246-
headers={"Content-Type": "application/json"},
247-
) as response:
248-
# Fix for FlashForge printer's malformed Content-Type header
249-
# Some printers return "appliation/json" instead of "application/json"
250-
try:
251-
data = await response.json()
252-
except aiohttp.ContentTypeError:
253-
# Fallback: manually parse as JSON if Content-Type is malformed
254-
text = await response.text()
255-
import json
256-
257-
data = json.loads(text)
258-
259-
print(f"Command reply: {data}")
260-
261-
return NetworkUtils.is_ok(data)
240+
session = await self.client.get_http_session()
241+
async with session.post(
242+
self.client.get_endpoint(Endpoints.CONTROL),
243+
json=payload,
244+
headers={"Content-Type": "application/json"},
245+
) as response:
246+
data = await json_from_response(response)
247+
print(f"Command reply: {data}")
248+
249+
return NetworkUtils.is_ok(data)
262250

263251
except Exception as e:
264252
print(f"Error in send_control_command: {e}")

flashforge/api/controls/files.py

Lines changed: 75 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
import base64
66
from typing import TYPE_CHECKING
77

8-
import aiohttp
98
from pydantic import ValidationError
109

1110
from ...models.machine_info import FFGcodeFileEntry
1211
from ...models.responses import GCodeListResponse, ThumbnailResponse
1312
from ..constants.endpoints import Endpoints
14-
from ..network.utils import NetworkUtils
13+
from ..network.utils import NetworkUtils, json_from_response
1514

1615
if TYPE_CHECKING:
1716
from ...client import FlashForgeClient
@@ -65,74 +64,65 @@ async def get_recent_file_list(self) -> list[FFGcodeFileEntry]:
6564
payload = {"serialNumber": self.client.serial_number, "checkCode": self.client.check_code}
6665

6766
try:
68-
async with aiohttp.ClientSession() as session:
69-
async with session.post(
70-
self.client.get_endpoint(Endpoints.GCODE_LIST),
71-
json=payload,
72-
headers={"Content-Type": "application/json"},
73-
) as response:
74-
if response.status != 200:
75-
return []
76-
77-
# Fix for FlashForge printer's malformed Content-Type header
78-
# Some printers return "appliation/json" instead of "application/json"
79-
try:
80-
data = await response.json()
81-
except aiohttp.ContentTypeError:
82-
# Fallback: manually parse as JSON if Content-Type is malformed
83-
text = await response.text()
84-
import json
85-
86-
data = json.loads(text)
87-
88-
if not NetworkUtils.is_ok(data):
89-
print(f"Error retrieving file list: {NetworkUtils.get_error_message(data)}")
90-
return []
91-
92-
# Parse the response using GCodeListResponse
93-
try:
94-
result = GCodeListResponse(**data)
95-
except ValidationError:
96-
raw_list = data.get("gcodeList", [])
97-
if isinstance(raw_list, list):
98-
entries: list[FFGcodeFileEntry] = []
99-
for file_name in raw_list:
100-
if isinstance(file_name, str):
101-
entries.append(
102-
FFGcodeFileEntry(
103-
gcodeFileName=file_name,
104-
printingTime=0,
105-
)
106-
)
107-
return entries
108-
return []
109-
110-
# AD5X and newer printers provide detailed info in gcodeListDetail
111-
if result.gcode_list_detail and len(result.gcode_list_detail) > 0:
112-
return result.gcode_list_detail
113-
114-
# Fallback for older printers using gcodeList
115-
if result.gcode_list and len(result.gcode_list) > 0:
116-
# Check if it's a list of strings or already FFGcodeFileEntry objects
117-
first_item = result.gcode_list[0]
118-
119-
if isinstance(first_item, str):
120-
# Convert string array to FFGcodeFileEntry objects
121-
return [
122-
FFGcodeFileEntry(gcodeFileName=file_name, printingTime=0)
123-
for file_name in result.gcode_list
124-
if isinstance(file_name, str)
125-
]
126-
elif isinstance(first_item, FFGcodeFileEntry):
127-
# Already FFGcodeFileEntry objects - need explicit type narrowing
128-
return [
129-
item
130-
for item in result.gcode_list
131-
if isinstance(item, FFGcodeFileEntry)
132-
]
67+
session = await self.client.get_http_session()
68+
async with session.post(
69+
self.client.get_endpoint(Endpoints.GCODE_LIST),
70+
json=payload,
71+
headers={"Content-Type": "application/json"},
72+
) as response:
73+
if response.status != 200:
74+
return []
13375

76+
data = await json_from_response(response)
77+
78+
if not NetworkUtils.is_ok(data):
79+
print(f"Error retrieving file list: {NetworkUtils.get_error_message(data)}")
13480
return []
13581

82+
# Parse the response using GCodeListResponse
83+
try:
84+
result = GCodeListResponse(**data)
85+
except ValidationError:
86+
raw_list = data.get("gcodeList", [])
87+
if isinstance(raw_list, list):
88+
entries: list[FFGcodeFileEntry] = []
89+
for file_name in raw_list:
90+
if isinstance(file_name, str):
91+
entries.append(
92+
FFGcodeFileEntry(
93+
gcodeFileName=file_name,
94+
printingTime=0,
95+
)
96+
)
97+
return entries
98+
return []
99+
100+
# AD5X and newer printers provide detailed info in gcodeListDetail
101+
if result.gcode_list_detail and len(result.gcode_list_detail) > 0:
102+
return result.gcode_list_detail
103+
104+
# Fallback for older printers using gcodeList
105+
if result.gcode_list and len(result.gcode_list) > 0:
106+
# Check if it's a list of strings or already FFGcodeFileEntry objects
107+
first_item = result.gcode_list[0]
108+
109+
if isinstance(first_item, str):
110+
# Convert string array to FFGcodeFileEntry objects
111+
return [
112+
FFGcodeFileEntry(gcodeFileName=file_name, printingTime=0)
113+
for file_name in result.gcode_list
114+
if isinstance(file_name, str)
115+
]
116+
elif isinstance(first_item, FFGcodeFileEntry):
117+
# Already FFGcodeFileEntry objects - need explicit type narrowing
118+
return [
119+
item
120+
for item in result.gcode_list
121+
if isinstance(item, FFGcodeFileEntry)
122+
]
123+
124+
return []
125+
136126
except Exception as err:
137127
print(f"GetRecentFileList error: {err}")
138128
return []
@@ -156,33 +146,24 @@ async def get_gcode_thumbnail(self, file_name: str) -> bytes | None:
156146
}
157147

158148
try:
159-
async with aiohttp.ClientSession() as session:
160-
async with session.post(
161-
self.client.get_endpoint(Endpoints.GCODE_THUMB),
162-
json=payload,
163-
headers={"Content-Type": "application/json"},
164-
) as response:
165-
if response.status != 200:
166-
return None
167-
168-
# Fix for FlashForge printer's malformed Content-Type header
169-
# Some printers return "appliation/json" instead of "application/json"
170-
try:
171-
data = await response.json()
172-
except aiohttp.ContentTypeError:
173-
# Fallback: manually parse as JSON if Content-Type is malformed
174-
text = await response.text()
175-
import json
176-
177-
data = json.loads(text)
178-
179-
if NetworkUtils.is_ok(data):
180-
# Parse response and return decoded image bytes
181-
result = ThumbnailResponse(**data)
182-
return base64.b64decode(result.image_data)
183-
else:
184-
print(f"Error retrieving thumbnail: {NetworkUtils.get_error_message(data)}")
185-
return None
149+
session = await self.client.get_http_session()
150+
async with session.post(
151+
self.client.get_endpoint(Endpoints.GCODE_THUMB),
152+
json=payload,
153+
headers={"Content-Type": "application/json"},
154+
) as response:
155+
if response.status != 200:
156+
return None
157+
158+
data = await json_from_response(response)
159+
160+
if NetworkUtils.is_ok(data):
161+
# Parse response and return decoded image bytes
162+
result = ThumbnailResponse(**data)
163+
return base64.b64decode(result.image_data)
164+
else:
165+
print(f"Error retrieving thumbnail: {NetworkUtils.get_error_message(data)}")
166+
return None
186167

187168
except Exception as err:
188169
print(f"GetGcodeThumbnail error: {err}")

flashforge/api/controls/info.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
from datetime import datetime, timedelta
66
from typing import TYPE_CHECKING
77

8-
import aiohttp
9-
108
from ...models.machine_info import FFMachineInfo, MachineState, Temperature
119
from ...models.responses import DetailResponse, FFPrinterDetail
1210
from ..constants.endpoints import Endpoints
11+
from ..network.utils import json_from_response
1312

1413
if TYPE_CHECKING:
1514
from ...client import FlashForgeClient
@@ -272,18 +271,7 @@ async def get_detail_response(self) -> DetailResponse | None:
272271
print(f"Non-200 status from detail endpoint: {response.status}")
273272
return None
274273

275-
# Fix for FlashForge printer's malformed Content-Type header
276-
# Some printers return "appliation/json" instead of "application/json"
277-
# We'll manually parse the text as JSON to bypass Content-Type validation
278-
try:
279-
data = await response.json()
280-
except aiohttp.ContentTypeError:
281-
# Fallback: manually parse as JSON if Content-Type is malformed
282-
text = await response.text()
283-
import json
284-
285-
data = json.loads(text)
286-
274+
data = await json_from_response(response)
287275
return DetailResponse(**data)
288276

289277
except Exception as error:

0 commit comments

Comments
 (0)