ai-parrot 0.17.2__cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (535) hide show
  1. agentui/.prettierrc +15 -0
  2. agentui/QUICKSTART.md +272 -0
  3. agentui/README.md +59 -0
  4. agentui/env.example +16 -0
  5. agentui/jsconfig.json +14 -0
  6. agentui/package-lock.json +4242 -0
  7. agentui/package.json +34 -0
  8. agentui/scripts/postinstall/apply-patches.mjs +260 -0
  9. agentui/src/app.css +61 -0
  10. agentui/src/app.d.ts +13 -0
  11. agentui/src/app.html +12 -0
  12. agentui/src/components/LoadingSpinner.svelte +64 -0
  13. agentui/src/components/ThemeSwitcher.svelte +159 -0
  14. agentui/src/components/index.js +4 -0
  15. agentui/src/lib/api/bots.ts +60 -0
  16. agentui/src/lib/api/chat.ts +22 -0
  17. agentui/src/lib/api/http.ts +25 -0
  18. agentui/src/lib/components/BotCard.svelte +33 -0
  19. agentui/src/lib/components/ChatBubble.svelte +63 -0
  20. agentui/src/lib/components/Toast.svelte +21 -0
  21. agentui/src/lib/config.ts +20 -0
  22. agentui/src/lib/stores/auth.svelte.ts +73 -0
  23. agentui/src/lib/stores/theme.svelte.js +64 -0
  24. agentui/src/lib/stores/toast.svelte.ts +31 -0
  25. agentui/src/lib/utils/conversation.ts +39 -0
  26. agentui/src/routes/+layout.svelte +20 -0
  27. agentui/src/routes/+page.svelte +232 -0
  28. agentui/src/routes/login/+page.svelte +200 -0
  29. agentui/src/routes/talk/[agentId]/+page.svelte +297 -0
  30. agentui/src/routes/talk/[agentId]/+page.ts +7 -0
  31. agentui/static/README.md +1 -0
  32. agentui/svelte.config.js +11 -0
  33. agentui/tailwind.config.ts +53 -0
  34. agentui/tsconfig.json +3 -0
  35. agentui/vite.config.ts +10 -0
  36. ai_parrot-0.17.2.dist-info/METADATA +472 -0
  37. ai_parrot-0.17.2.dist-info/RECORD +535 -0
  38. ai_parrot-0.17.2.dist-info/WHEEL +6 -0
  39. ai_parrot-0.17.2.dist-info/entry_points.txt +2 -0
  40. ai_parrot-0.17.2.dist-info/licenses/LICENSE +21 -0
  41. ai_parrot-0.17.2.dist-info/top_level.txt +6 -0
  42. crew-builder/.prettierrc +15 -0
  43. crew-builder/QUICKSTART.md +259 -0
  44. crew-builder/README.md +113 -0
  45. crew-builder/env.example +17 -0
  46. crew-builder/jsconfig.json +14 -0
  47. crew-builder/package-lock.json +4182 -0
  48. crew-builder/package.json +37 -0
  49. crew-builder/scripts/postinstall/apply-patches.mjs +260 -0
  50. crew-builder/src/app.css +62 -0
  51. crew-builder/src/app.d.ts +13 -0
  52. crew-builder/src/app.html +12 -0
  53. crew-builder/src/components/LoadingSpinner.svelte +64 -0
  54. crew-builder/src/components/ThemeSwitcher.svelte +149 -0
  55. crew-builder/src/components/index.js +9 -0
  56. crew-builder/src/lib/api/bots.ts +60 -0
  57. crew-builder/src/lib/api/chat.ts +80 -0
  58. crew-builder/src/lib/api/client.ts +56 -0
  59. crew-builder/src/lib/api/crew/crew.ts +136 -0
  60. crew-builder/src/lib/api/index.ts +5 -0
  61. crew-builder/src/lib/api/o365/auth.ts +65 -0
  62. crew-builder/src/lib/auth/auth.ts +54 -0
  63. crew-builder/src/lib/components/AgentNode.svelte +43 -0
  64. crew-builder/src/lib/components/BotCard.svelte +33 -0
  65. crew-builder/src/lib/components/ChatBubble.svelte +67 -0
  66. crew-builder/src/lib/components/ConfigPanel.svelte +278 -0
  67. crew-builder/src/lib/components/JsonTreeNode.svelte +76 -0
  68. crew-builder/src/lib/components/JsonViewer.svelte +24 -0
  69. crew-builder/src/lib/components/MarkdownEditor.svelte +48 -0
  70. crew-builder/src/lib/components/ThemeToggle.svelte +36 -0
  71. crew-builder/src/lib/components/Toast.svelte +67 -0
  72. crew-builder/src/lib/components/Toolbar.svelte +157 -0
  73. crew-builder/src/lib/components/index.ts +10 -0
  74. crew-builder/src/lib/config.ts +8 -0
  75. crew-builder/src/lib/stores/auth.svelte.ts +228 -0
  76. crew-builder/src/lib/stores/crewStore.ts +369 -0
  77. crew-builder/src/lib/stores/theme.svelte.js +145 -0
  78. crew-builder/src/lib/stores/toast.svelte.ts +69 -0
  79. crew-builder/src/lib/utils/conversation.ts +39 -0
  80. crew-builder/src/lib/utils/markdown.ts +122 -0
  81. crew-builder/src/lib/utils/talkHistory.ts +47 -0
  82. crew-builder/src/routes/+layout.svelte +20 -0
  83. crew-builder/src/routes/+page.svelte +539 -0
  84. crew-builder/src/routes/agents/+page.svelte +247 -0
  85. crew-builder/src/routes/agents/[agentId]/+page.svelte +288 -0
  86. crew-builder/src/routes/agents/[agentId]/+page.ts +7 -0
  87. crew-builder/src/routes/builder/+page.svelte +204 -0
  88. crew-builder/src/routes/crew/ask/+page.svelte +1052 -0
  89. crew-builder/src/routes/crew/ask/+page.ts +1 -0
  90. crew-builder/src/routes/integrations/o365/+page.svelte +304 -0
  91. crew-builder/src/routes/login/+page.svelte +197 -0
  92. crew-builder/src/routes/talk/[agentId]/+page.svelte +487 -0
  93. crew-builder/src/routes/talk/[agentId]/+page.ts +7 -0
  94. crew-builder/static/README.md +1 -0
  95. crew-builder/svelte.config.js +11 -0
  96. crew-builder/tailwind.config.ts +53 -0
  97. crew-builder/tsconfig.json +3 -0
  98. crew-builder/vite.config.ts +10 -0
  99. mcp_servers/calculator_server.py +309 -0
  100. parrot/__init__.py +27 -0
  101. parrot/__pycache__/__init__.cpython-310.pyc +0 -0
  102. parrot/__pycache__/version.cpython-310.pyc +0 -0
  103. parrot/_version.py +34 -0
  104. parrot/a2a/__init__.py +48 -0
  105. parrot/a2a/client.py +658 -0
  106. parrot/a2a/discovery.py +89 -0
  107. parrot/a2a/mixin.py +257 -0
  108. parrot/a2a/models.py +376 -0
  109. parrot/a2a/server.py +770 -0
  110. parrot/agents/__init__.py +29 -0
  111. parrot/bots/__init__.py +12 -0
  112. parrot/bots/a2a_agent.py +19 -0
  113. parrot/bots/abstract.py +3139 -0
  114. parrot/bots/agent.py +1129 -0
  115. parrot/bots/basic.py +9 -0
  116. parrot/bots/chatbot.py +669 -0
  117. parrot/bots/data.py +1618 -0
  118. parrot/bots/database/__init__.py +5 -0
  119. parrot/bots/database/abstract.py +3071 -0
  120. parrot/bots/database/cache.py +286 -0
  121. parrot/bots/database/models.py +468 -0
  122. parrot/bots/database/prompts.py +154 -0
  123. parrot/bots/database/retries.py +98 -0
  124. parrot/bots/database/router.py +269 -0
  125. parrot/bots/database/sql.py +41 -0
  126. parrot/bots/db/__init__.py +6 -0
  127. parrot/bots/db/abstract.py +556 -0
  128. parrot/bots/db/bigquery.py +602 -0
  129. parrot/bots/db/cache.py +85 -0
  130. parrot/bots/db/documentdb.py +668 -0
  131. parrot/bots/db/elastic.py +1014 -0
  132. parrot/bots/db/influx.py +898 -0
  133. parrot/bots/db/mock.py +96 -0
  134. parrot/bots/db/multi.py +783 -0
  135. parrot/bots/db/prompts.py +185 -0
  136. parrot/bots/db/sql.py +1255 -0
  137. parrot/bots/db/tools.py +212 -0
  138. parrot/bots/document.py +680 -0
  139. parrot/bots/hrbot.py +15 -0
  140. parrot/bots/kb.py +170 -0
  141. parrot/bots/mcp.py +36 -0
  142. parrot/bots/orchestration/README.md +463 -0
  143. parrot/bots/orchestration/__init__.py +1 -0
  144. parrot/bots/orchestration/agent.py +155 -0
  145. parrot/bots/orchestration/crew.py +3330 -0
  146. parrot/bots/orchestration/fsm.py +1179 -0
  147. parrot/bots/orchestration/hr.py +434 -0
  148. parrot/bots/orchestration/storage/__init__.py +4 -0
  149. parrot/bots/orchestration/storage/memory.py +100 -0
  150. parrot/bots/orchestration/storage/mixin.py +119 -0
  151. parrot/bots/orchestration/verify.py +202 -0
  152. parrot/bots/product.py +204 -0
  153. parrot/bots/prompts/__init__.py +96 -0
  154. parrot/bots/prompts/agents.py +155 -0
  155. parrot/bots/prompts/data.py +216 -0
  156. parrot/bots/prompts/output_generation.py +8 -0
  157. parrot/bots/scraper/__init__.py +3 -0
  158. parrot/bots/scraper/models.py +122 -0
  159. parrot/bots/scraper/scraper.py +1173 -0
  160. parrot/bots/scraper/templates.py +115 -0
  161. parrot/bots/stores/__init__.py +5 -0
  162. parrot/bots/stores/local.py +172 -0
  163. parrot/bots/webdev.py +81 -0
  164. parrot/cli.py +17 -0
  165. parrot/clients/__init__.py +16 -0
  166. parrot/clients/base.py +1491 -0
  167. parrot/clients/claude.py +1191 -0
  168. parrot/clients/factory.py +129 -0
  169. parrot/clients/google.py +4567 -0
  170. parrot/clients/gpt.py +1975 -0
  171. parrot/clients/grok.py +432 -0
  172. parrot/clients/groq.py +986 -0
  173. parrot/clients/hf.py +582 -0
  174. parrot/clients/models.py +18 -0
  175. parrot/conf.py +395 -0
  176. parrot/embeddings/__init__.py +9 -0
  177. parrot/embeddings/base.py +157 -0
  178. parrot/embeddings/google.py +98 -0
  179. parrot/embeddings/huggingface.py +74 -0
  180. parrot/embeddings/openai.py +84 -0
  181. parrot/embeddings/processor.py +88 -0
  182. parrot/exceptions.c +13868 -0
  183. parrot/exceptions.cpython-310-x86_64-linux-gnu.so +0 -0
  184. parrot/exceptions.pxd +22 -0
  185. parrot/exceptions.pxi +15 -0
  186. parrot/exceptions.pyx +44 -0
  187. parrot/generators/__init__.py +29 -0
  188. parrot/generators/base.py +200 -0
  189. parrot/generators/html.py +293 -0
  190. parrot/generators/react.py +205 -0
  191. parrot/generators/streamlit.py +203 -0
  192. parrot/generators/template.py +105 -0
  193. parrot/handlers/__init__.py +4 -0
  194. parrot/handlers/agent.py +861 -0
  195. parrot/handlers/agents/__init__.py +1 -0
  196. parrot/handlers/agents/abstract.py +900 -0
  197. parrot/handlers/bots.py +338 -0
  198. parrot/handlers/chat.py +915 -0
  199. parrot/handlers/creation.sql +192 -0
  200. parrot/handlers/crew/ARCHITECTURE.md +362 -0
  201. parrot/handlers/crew/README_BOTMANAGER_PERSISTENCE.md +303 -0
  202. parrot/handlers/crew/README_REDIS_PERSISTENCE.md +366 -0
  203. parrot/handlers/crew/__init__.py +0 -0
  204. parrot/handlers/crew/handler.py +801 -0
  205. parrot/handlers/crew/models.py +229 -0
  206. parrot/handlers/crew/redis_persistence.py +523 -0
  207. parrot/handlers/jobs/__init__.py +10 -0
  208. parrot/handlers/jobs/job.py +384 -0
  209. parrot/handlers/jobs/mixin.py +627 -0
  210. parrot/handlers/jobs/models.py +115 -0
  211. parrot/handlers/jobs/worker.py +31 -0
  212. parrot/handlers/models.py +596 -0
  213. parrot/handlers/o365_auth.py +105 -0
  214. parrot/handlers/stream.py +337 -0
  215. parrot/interfaces/__init__.py +6 -0
  216. parrot/interfaces/aws.py +143 -0
  217. parrot/interfaces/credentials.py +113 -0
  218. parrot/interfaces/database.py +27 -0
  219. parrot/interfaces/google.py +1123 -0
  220. parrot/interfaces/hierarchy.py +1227 -0
  221. parrot/interfaces/http.py +651 -0
  222. parrot/interfaces/images/__init__.py +0 -0
  223. parrot/interfaces/images/plugins/__init__.py +24 -0
  224. parrot/interfaces/images/plugins/abstract.py +58 -0
  225. parrot/interfaces/images/plugins/analisys.py +148 -0
  226. parrot/interfaces/images/plugins/classify.py +150 -0
  227. parrot/interfaces/images/plugins/classifybase.py +182 -0
  228. parrot/interfaces/images/plugins/detect.py +150 -0
  229. parrot/interfaces/images/plugins/exif.py +1103 -0
  230. parrot/interfaces/images/plugins/hash.py +52 -0
  231. parrot/interfaces/images/plugins/vision.py +104 -0
  232. parrot/interfaces/images/plugins/yolo.py +66 -0
  233. parrot/interfaces/images/plugins/zerodetect.py +197 -0
  234. parrot/interfaces/o365.py +978 -0
  235. parrot/interfaces/onedrive.py +822 -0
  236. parrot/interfaces/sharepoint.py +1435 -0
  237. parrot/interfaces/soap.py +257 -0
  238. parrot/loaders/__init__.py +8 -0
  239. parrot/loaders/abstract.py +1131 -0
  240. parrot/loaders/audio.py +199 -0
  241. parrot/loaders/basepdf.py +53 -0
  242. parrot/loaders/basevideo.py +1568 -0
  243. parrot/loaders/csv.py +409 -0
  244. parrot/loaders/docx.py +116 -0
  245. parrot/loaders/epubloader.py +316 -0
  246. parrot/loaders/excel.py +199 -0
  247. parrot/loaders/factory.py +55 -0
  248. parrot/loaders/files/__init__.py +0 -0
  249. parrot/loaders/files/abstract.py +39 -0
  250. parrot/loaders/files/html.py +26 -0
  251. parrot/loaders/files/text.py +63 -0
  252. parrot/loaders/html.py +152 -0
  253. parrot/loaders/markdown.py +442 -0
  254. parrot/loaders/pdf.py +373 -0
  255. parrot/loaders/pdfmark.py +320 -0
  256. parrot/loaders/pdftables.py +506 -0
  257. parrot/loaders/ppt.py +476 -0
  258. parrot/loaders/qa.py +63 -0
  259. parrot/loaders/splitters/__init__.py +10 -0
  260. parrot/loaders/splitters/base.py +138 -0
  261. parrot/loaders/splitters/md.py +228 -0
  262. parrot/loaders/splitters/token.py +143 -0
  263. parrot/loaders/txt.py +26 -0
  264. parrot/loaders/video.py +89 -0
  265. parrot/loaders/videolocal.py +218 -0
  266. parrot/loaders/videounderstanding.py +377 -0
  267. parrot/loaders/vimeo.py +167 -0
  268. parrot/loaders/web.py +599 -0
  269. parrot/loaders/youtube.py +504 -0
  270. parrot/manager/__init__.py +5 -0
  271. parrot/manager/manager.py +1030 -0
  272. parrot/mcp/__init__.py +28 -0
  273. parrot/mcp/adapter.py +105 -0
  274. parrot/mcp/cli.py +174 -0
  275. parrot/mcp/client.py +119 -0
  276. parrot/mcp/config.py +75 -0
  277. parrot/mcp/integration.py +842 -0
  278. parrot/mcp/oauth.py +933 -0
  279. parrot/mcp/server.py +225 -0
  280. parrot/mcp/transports/__init__.py +3 -0
  281. parrot/mcp/transports/base.py +279 -0
  282. parrot/mcp/transports/grpc_session.py +163 -0
  283. parrot/mcp/transports/http.py +312 -0
  284. parrot/mcp/transports/mcp.proto +108 -0
  285. parrot/mcp/transports/quic.py +1082 -0
  286. parrot/mcp/transports/sse.py +330 -0
  287. parrot/mcp/transports/stdio.py +309 -0
  288. parrot/mcp/transports/unix.py +395 -0
  289. parrot/mcp/transports/websocket.py +547 -0
  290. parrot/memory/__init__.py +16 -0
  291. parrot/memory/abstract.py +209 -0
  292. parrot/memory/agent.py +32 -0
  293. parrot/memory/cache.py +175 -0
  294. parrot/memory/core.py +555 -0
  295. parrot/memory/file.py +153 -0
  296. parrot/memory/mem.py +131 -0
  297. parrot/memory/redis.py +613 -0
  298. parrot/models/__init__.py +46 -0
  299. parrot/models/basic.py +118 -0
  300. parrot/models/compliance.py +208 -0
  301. parrot/models/crew.py +395 -0
  302. parrot/models/detections.py +654 -0
  303. parrot/models/generation.py +85 -0
  304. parrot/models/google.py +223 -0
  305. parrot/models/groq.py +23 -0
  306. parrot/models/openai.py +30 -0
  307. parrot/models/outputs.py +285 -0
  308. parrot/models/responses.py +938 -0
  309. parrot/notifications/__init__.py +743 -0
  310. parrot/openapi/__init__.py +3 -0
  311. parrot/openapi/components.yaml +641 -0
  312. parrot/openapi/config.py +322 -0
  313. parrot/outputs/__init__.py +32 -0
  314. parrot/outputs/formats/__init__.py +108 -0
  315. parrot/outputs/formats/altair.py +359 -0
  316. parrot/outputs/formats/application.py +122 -0
  317. parrot/outputs/formats/base.py +351 -0
  318. parrot/outputs/formats/bokeh.py +356 -0
  319. parrot/outputs/formats/card.py +424 -0
  320. parrot/outputs/formats/chart.py +436 -0
  321. parrot/outputs/formats/d3.py +255 -0
  322. parrot/outputs/formats/echarts.py +310 -0
  323. parrot/outputs/formats/generators/__init__.py +0 -0
  324. parrot/outputs/formats/generators/abstract.py +61 -0
  325. parrot/outputs/formats/generators/panel.py +145 -0
  326. parrot/outputs/formats/generators/streamlit.py +86 -0
  327. parrot/outputs/formats/generators/terminal.py +63 -0
  328. parrot/outputs/formats/holoviews.py +310 -0
  329. parrot/outputs/formats/html.py +147 -0
  330. parrot/outputs/formats/jinja2.py +46 -0
  331. parrot/outputs/formats/json.py +87 -0
  332. parrot/outputs/formats/map.py +933 -0
  333. parrot/outputs/formats/markdown.py +172 -0
  334. parrot/outputs/formats/matplotlib.py +237 -0
  335. parrot/outputs/formats/mixins/__init__.py +0 -0
  336. parrot/outputs/formats/mixins/emaps.py +855 -0
  337. parrot/outputs/formats/plotly.py +341 -0
  338. parrot/outputs/formats/seaborn.py +310 -0
  339. parrot/outputs/formats/table.py +397 -0
  340. parrot/outputs/formats/template_report.py +138 -0
  341. parrot/outputs/formats/yaml.py +125 -0
  342. parrot/outputs/formatter.py +152 -0
  343. parrot/outputs/templates/__init__.py +95 -0
  344. parrot/pipelines/__init__.py +0 -0
  345. parrot/pipelines/abstract.py +210 -0
  346. parrot/pipelines/detector.py +124 -0
  347. parrot/pipelines/models.py +90 -0
  348. parrot/pipelines/planogram.py +3002 -0
  349. parrot/pipelines/table.sql +97 -0
  350. parrot/plugins/__init__.py +106 -0
  351. parrot/plugins/importer.py +80 -0
  352. parrot/py.typed +0 -0
  353. parrot/registry/__init__.py +18 -0
  354. parrot/registry/registry.py +594 -0
  355. parrot/scheduler/__init__.py +1189 -0
  356. parrot/scheduler/models.py +60 -0
  357. parrot/security/__init__.py +16 -0
  358. parrot/security/prompt_injection.py +268 -0
  359. parrot/security/security_events.sql +25 -0
  360. parrot/services/__init__.py +1 -0
  361. parrot/services/mcp/__init__.py +8 -0
  362. parrot/services/mcp/config.py +13 -0
  363. parrot/services/mcp/server.py +295 -0
  364. parrot/services/o365_remote_auth.py +235 -0
  365. parrot/stores/__init__.py +7 -0
  366. parrot/stores/abstract.py +352 -0
  367. parrot/stores/arango.py +1090 -0
  368. parrot/stores/bigquery.py +1377 -0
  369. parrot/stores/cache.py +106 -0
  370. parrot/stores/empty.py +10 -0
  371. parrot/stores/faiss_store.py +1157 -0
  372. parrot/stores/kb/__init__.py +9 -0
  373. parrot/stores/kb/abstract.py +68 -0
  374. parrot/stores/kb/cache.py +165 -0
  375. parrot/stores/kb/doc.py +325 -0
  376. parrot/stores/kb/hierarchy.py +346 -0
  377. parrot/stores/kb/local.py +457 -0
  378. parrot/stores/kb/prompt.py +28 -0
  379. parrot/stores/kb/redis.py +659 -0
  380. parrot/stores/kb/store.py +115 -0
  381. parrot/stores/kb/user.py +374 -0
  382. parrot/stores/models.py +59 -0
  383. parrot/stores/pgvector.py +3 -0
  384. parrot/stores/postgres.py +2853 -0
  385. parrot/stores/utils/__init__.py +0 -0
  386. parrot/stores/utils/chunking.py +197 -0
  387. parrot/telemetry/__init__.py +3 -0
  388. parrot/telemetry/mixin.py +111 -0
  389. parrot/template/__init__.py +3 -0
  390. parrot/template/engine.py +259 -0
  391. parrot/tools/__init__.py +23 -0
  392. parrot/tools/abstract.py +644 -0
  393. parrot/tools/agent.py +363 -0
  394. parrot/tools/arangodbsearch.py +537 -0
  395. parrot/tools/arxiv_tool.py +188 -0
  396. parrot/tools/calculator/__init__.py +3 -0
  397. parrot/tools/calculator/operations/__init__.py +38 -0
  398. parrot/tools/calculator/operations/calculus.py +80 -0
  399. parrot/tools/calculator/operations/statistics.py +76 -0
  400. parrot/tools/calculator/tool.py +150 -0
  401. parrot/tools/cloudwatch.py +988 -0
  402. parrot/tools/codeinterpreter/__init__.py +127 -0
  403. parrot/tools/codeinterpreter/executor.py +371 -0
  404. parrot/tools/codeinterpreter/internals.py +473 -0
  405. parrot/tools/codeinterpreter/models.py +643 -0
  406. parrot/tools/codeinterpreter/prompts.py +224 -0
  407. parrot/tools/codeinterpreter/tool.py +664 -0
  408. parrot/tools/company_info/__init__.py +6 -0
  409. parrot/tools/company_info/tool.py +1138 -0
  410. parrot/tools/correlationanalysis.py +437 -0
  411. parrot/tools/database/abstract.py +286 -0
  412. parrot/tools/database/bq.py +115 -0
  413. parrot/tools/database/cache.py +284 -0
  414. parrot/tools/database/models.py +95 -0
  415. parrot/tools/database/pg.py +343 -0
  416. parrot/tools/databasequery.py +1159 -0
  417. parrot/tools/db.py +1800 -0
  418. parrot/tools/ddgo.py +370 -0
  419. parrot/tools/decorators.py +271 -0
  420. parrot/tools/dftohtml.py +282 -0
  421. parrot/tools/document.py +549 -0
  422. parrot/tools/ecs.py +819 -0
  423. parrot/tools/edareport.py +368 -0
  424. parrot/tools/elasticsearch.py +1049 -0
  425. parrot/tools/employees.py +462 -0
  426. parrot/tools/epson/__init__.py +96 -0
  427. parrot/tools/excel.py +683 -0
  428. parrot/tools/file/__init__.py +13 -0
  429. parrot/tools/file/abstract.py +76 -0
  430. parrot/tools/file/gcs.py +378 -0
  431. parrot/tools/file/local.py +284 -0
  432. parrot/tools/file/s3.py +511 -0
  433. parrot/tools/file/tmp.py +309 -0
  434. parrot/tools/file/tool.py +501 -0
  435. parrot/tools/file_reader.py +129 -0
  436. parrot/tools/flowtask/__init__.py +19 -0
  437. parrot/tools/flowtask/tool.py +761 -0
  438. parrot/tools/gittoolkit.py +508 -0
  439. parrot/tools/google/__init__.py +18 -0
  440. parrot/tools/google/base.py +169 -0
  441. parrot/tools/google/tools.py +1251 -0
  442. parrot/tools/googlelocation.py +5 -0
  443. parrot/tools/googleroutes.py +5 -0
  444. parrot/tools/googlesearch.py +5 -0
  445. parrot/tools/googlesitesearch.py +5 -0
  446. parrot/tools/googlevoice.py +2 -0
  447. parrot/tools/gvoice.py +695 -0
  448. parrot/tools/ibisworld/README.md +225 -0
  449. parrot/tools/ibisworld/__init__.py +11 -0
  450. parrot/tools/ibisworld/tool.py +366 -0
  451. parrot/tools/jiratoolkit.py +1718 -0
  452. parrot/tools/manager.py +1098 -0
  453. parrot/tools/math.py +152 -0
  454. parrot/tools/metadata.py +476 -0
  455. parrot/tools/msteams.py +1621 -0
  456. parrot/tools/msword.py +635 -0
  457. parrot/tools/multidb.py +580 -0
  458. parrot/tools/multistoresearch.py +369 -0
  459. parrot/tools/networkninja.py +167 -0
  460. parrot/tools/nextstop/__init__.py +4 -0
  461. parrot/tools/nextstop/base.py +286 -0
  462. parrot/tools/nextstop/employee.py +733 -0
  463. parrot/tools/nextstop/store.py +462 -0
  464. parrot/tools/notification.py +435 -0
  465. parrot/tools/o365/__init__.py +42 -0
  466. parrot/tools/o365/base.py +295 -0
  467. parrot/tools/o365/bundle.py +522 -0
  468. parrot/tools/o365/events.py +554 -0
  469. parrot/tools/o365/mail.py +992 -0
  470. parrot/tools/o365/onedrive.py +497 -0
  471. parrot/tools/o365/sharepoint.py +641 -0
  472. parrot/tools/openapi_toolkit.py +904 -0
  473. parrot/tools/openweather.py +527 -0
  474. parrot/tools/pdfprint.py +1001 -0
  475. parrot/tools/powerbi.py +518 -0
  476. parrot/tools/powerpoint.py +1113 -0
  477. parrot/tools/pricestool.py +146 -0
  478. parrot/tools/products/__init__.py +246 -0
  479. parrot/tools/prophet_tool.py +171 -0
  480. parrot/tools/pythonpandas.py +630 -0
  481. parrot/tools/pythonrepl.py +910 -0
  482. parrot/tools/qsource.py +436 -0
  483. parrot/tools/querytoolkit.py +395 -0
  484. parrot/tools/quickeda.py +827 -0
  485. parrot/tools/resttool.py +553 -0
  486. parrot/tools/retail/__init__.py +0 -0
  487. parrot/tools/retail/bby.py +528 -0
  488. parrot/tools/sandboxtool.py +703 -0
  489. parrot/tools/sassie/__init__.py +352 -0
  490. parrot/tools/scraping/__init__.py +7 -0
  491. parrot/tools/scraping/docs/select.md +466 -0
  492. parrot/tools/scraping/documentation.md +1278 -0
  493. parrot/tools/scraping/driver.py +436 -0
  494. parrot/tools/scraping/models.py +576 -0
  495. parrot/tools/scraping/options.py +85 -0
  496. parrot/tools/scraping/orchestrator.py +517 -0
  497. parrot/tools/scraping/readme.md +740 -0
  498. parrot/tools/scraping/tool.py +3115 -0
  499. parrot/tools/seasonaldetection.py +642 -0
  500. parrot/tools/shell_tool/__init__.py +5 -0
  501. parrot/tools/shell_tool/actions.py +408 -0
  502. parrot/tools/shell_tool/engine.py +155 -0
  503. parrot/tools/shell_tool/models.py +322 -0
  504. parrot/tools/shell_tool/tool.py +442 -0
  505. parrot/tools/site_search.py +214 -0
  506. parrot/tools/textfile.py +418 -0
  507. parrot/tools/think.py +378 -0
  508. parrot/tools/toolkit.py +298 -0
  509. parrot/tools/webapp_tool.py +187 -0
  510. parrot/tools/whatif.py +1279 -0
  511. parrot/tools/workday/MULTI_WSDL_EXAMPLE.md +249 -0
  512. parrot/tools/workday/__init__.py +6 -0
  513. parrot/tools/workday/models.py +1389 -0
  514. parrot/tools/workday/tool.py +1293 -0
  515. parrot/tools/yfinance_tool.py +306 -0
  516. parrot/tools/zipcode.py +217 -0
  517. parrot/utils/__init__.py +2 -0
  518. parrot/utils/helpers.py +73 -0
  519. parrot/utils/parsers/__init__.py +5 -0
  520. parrot/utils/parsers/toml.c +12078 -0
  521. parrot/utils/parsers/toml.cpython-310-x86_64-linux-gnu.so +0 -0
  522. parrot/utils/parsers/toml.pyx +21 -0
  523. parrot/utils/toml.py +11 -0
  524. parrot/utils/types.cpp +20936 -0
  525. parrot/utils/types.cpython-310-x86_64-linux-gnu.so +0 -0
  526. parrot/utils/types.pyx +213 -0
  527. parrot/utils/uv.py +11 -0
  528. parrot/version.py +10 -0
  529. parrot/yaml-rs/Cargo.lock +350 -0
  530. parrot/yaml-rs/Cargo.toml +19 -0
  531. parrot/yaml-rs/pyproject.toml +19 -0
  532. parrot/yaml-rs/python/yaml_rs/__init__.py +81 -0
  533. parrot/yaml-rs/src/lib.rs +222 -0
  534. requirements/docker-compose.yml +24 -0
  535. requirements/requirements-dev.txt +21 -0
@@ -0,0 +1,1389 @@
1
+ """
2
+ Workday Response Models and Structured Output Parser
3
+
4
+ Provides clean Pydantic models for Workday objects with:
5
+ 1. Default models per object type (Worker, Organization, etc.)
6
+ 2. Support for custom output formats
7
+ 3. Automatic parsing from verbose Zeep responses
8
+ """
9
+ import contextlib
10
+ from typing import Any, Dict, List, Optional, Type, TypeVar, Union
11
+ from datetime import date, datetime
12
+ from pydantic import BaseModel, Field, field_validator
13
+ from zeep import helpers
14
+
15
+
16
+ # ==========================================
17
+ # Default Pydantic Models for Workday Objects
18
+ # ==========================================
19
+
20
+ class WorkdayReference(BaseModel):
21
+ """Standard Workday reference object."""
22
+ id: str = Field(description="Primary identifier")
23
+ id_type: Optional[str] = Field(default=None, description="Type of identifier")
24
+ descriptor: Optional[str] = Field(default=None, description="Human-readable name")
25
+
26
+
27
+ class EmailAddress(BaseModel):
28
+ """Email address with metadata."""
29
+ email: str = Field(description="Email address")
30
+ type: Optional[str] = Field(default=None, description="Email type (Work, Home, etc.)")
31
+ primary: bool = Field(default=False, description="Is primary email")
32
+ public: bool = Field(default=True, description="Is public")
33
+
34
+
35
+ class PhoneNumber(BaseModel):
36
+ """Phone number with metadata."""
37
+ phone: str = Field(description="Phone number")
38
+ type: Optional[str] = Field(default=None, description="Phone type (Work, Mobile, etc.)")
39
+ primary: bool = Field(default=False, description="Is primary phone")
40
+ country_code: Optional[str] = Field(default=None, description="Country code")
41
+
42
+
43
+ class Address(BaseModel):
44
+ """Physical address."""
45
+ formatted_address: Optional[str] = Field(default=None, description="Complete formatted address")
46
+ address_line_1: Optional[str] = None
47
+ address_line_2: Optional[str] = None
48
+ city: Optional[str] = None
49
+ region: Optional[str] = Field(default=None, description="State/Province")
50
+ postal_code: Optional[str] = None
51
+ country: Optional[str] = None
52
+ type: Optional[str] = Field(default=None, description="Address type (Work, Home, etc.)")
53
+
54
+
55
+ class JobProfile(BaseModel):
56
+ """Job profile information."""
57
+ id: str = Field(description="Job profile ID")
58
+ name: str = Field(description="Job profile name")
59
+ job_family: Optional[str] = None
60
+ management_level: Optional[str] = None
61
+
62
+
63
+ class Position(BaseModel):
64
+ """Worker position information."""
65
+ position_id: str = Field(description="Position ID")
66
+ business_title: str = Field(description="Job title")
67
+ job_profile: Optional[JobProfile] = None
68
+ time_type: Optional[str] = Field(default=None, description="Full-time, Part-time, etc.")
69
+ location: Optional[str] = None
70
+ hire_date: Optional[date] = None
71
+ start_date: Optional[date] = None
72
+
73
+
74
+ class Manager(BaseModel):
75
+ """Manager reference."""
76
+ worker_id: str = Field(description="Manager's worker ID")
77
+ name: str = Field(description="Manager's name")
78
+ email: Optional[str] = None
79
+
80
+
81
+ class Compensation(BaseModel):
82
+ """Compensation information."""
83
+ base_pay: Optional[float] = None
84
+ currency: Optional[str] = Field(default="USD")
85
+ pay_frequency: Optional[str] = Field(default=None, description="Annual, Monthly, etc.")
86
+ effective_date: Optional[date] = None
87
+
88
+ class TimeOffBalance(BaseModel):
89
+ """Individual time off balance for a specific time off type."""
90
+ time_off_type: str = Field(description="Time off type name (e.g., 'Vacation', 'Sick', 'PTO')")
91
+ time_off_type_id: Optional[str] = Field(default=None, description="Time off type ID")
92
+ # Balance information
93
+ balance: float = Field(description="Current balance in hours or days")
94
+ balance_unit: str = Field(default="Hours", description="Unit of measurement (Hours, Days)")
95
+ # Additional balance details
96
+ scheduled: Optional[float] = Field(default=None, description="Scheduled/pending time off")
97
+ available: Optional[float] = Field(default=None, description="Available balance (balance - scheduled)")
98
+ # Accrual information
99
+ accrued_ytd: Optional[float] = Field(default=None, description="Accrued year-to-date")
100
+ used_ytd: Optional[float] = Field(default=None, description="Used year-to-date")
101
+ # Carryover
102
+ carryover: Optional[float] = Field(default=None, description="Carried over from previous period")
103
+ carryover_limit: Optional[float] = Field(default=None, description="Maximum carryover allowed")
104
+ # Effective dates
105
+ as_of_date: Optional[date] = Field(default=None, description="Balance as of this date")
106
+ plan_year_start: Optional[date] = Field(default=None, description="Plan year start date")
107
+ plan_year_end: Optional[date] = Field(default=None, description="Plan year end date")
108
+
109
+
110
+ class TimeOffBalanceModel(BaseModel):
111
+ """
112
+ Clean Time Off Balance model - Default output for time off information.
113
+
114
+ Provides structured view of a worker's time off balances across all types.
115
+ """
116
+ worker_id: str = Field(description="Worker ID")
117
+ as_of_date: date = Field(description="Date these balances are calculated as of")
118
+
119
+ # Time off balances by type
120
+ balances: List[TimeOffBalance] = Field(
121
+ default_factory=list,
122
+ description="List of time off balances by type"
123
+ )
124
+ # Quick access to common types
125
+ vacation_balance: Optional[float] = Field(
126
+ default=None,
127
+ description="Vacation/PTO balance if available"
128
+ )
129
+ sick_balance: Optional[float] = Field(
130
+ default=None,
131
+ description="Sick leave balance if available"
132
+ )
133
+ personal_balance: Optional[float] = Field(
134
+ default=None,
135
+ description="Personal time balance if available"
136
+ )
137
+ # Summary
138
+ total_available_hours: Optional[float] = Field(
139
+ default=None,
140
+ description="Total available time off across all types"
141
+ )
142
+
143
+ class Config:
144
+ json_schema_extra = {
145
+ "example": {
146
+ "worker_id": "12345",
147
+ "as_of_date": "2025-10-24",
148
+ "vacation_balance": 120.0,
149
+ "sick_balance": 80.0,
150
+ "balances": [
151
+ {
152
+ "time_off_type": "Vacation",
153
+ "balance": 120.0,
154
+ "balance_unit": "Hours",
155
+ "available": 112.0,
156
+ "scheduled": 8.0
157
+ }
158
+ ]
159
+ }
160
+ }
161
+
162
+ class WorkerModel(BaseModel):
163
+ """
164
+ Clean, structured Worker model - Default output format.
165
+
166
+ This is a simplified, usable representation of a Workday worker
167
+ instead of the deeply nested SOAP response.
168
+ """
169
+ worker_id: str = Field(description="Primary worker ID")
170
+ employee_id: Optional[str] = Field(default=None, description="Employee ID if applicable")
171
+
172
+ # Personal Information
173
+ first_name: str
174
+ last_name: str
175
+ preferred_name: Optional[str] = None
176
+ full_name: str = Field(description="Formatted full name")
177
+
178
+ # Contact Information
179
+ primary_email: Optional[str] = None
180
+ personal_email: Optional[str] = Field(default=None, description="Personal/HOME email")
181
+ corporate_email: Optional[str] = Field(default=None, description="Corporate/WORK email")
182
+ emails: List[EmailAddress] = Field(default_factory=list)
183
+ primary_phone: Optional[str] = None
184
+ phones: List[PhoneNumber] = Field(default_factory=list)
185
+ addresses: List[Address] = Field(default_factory=list)
186
+
187
+ # Employment Information
188
+ is_active: bool = Field(default=True)
189
+ hire_date: Optional[date] = None
190
+ termination_date: Optional[date] = None
191
+
192
+ # Position Information
193
+ business_title: Optional[str] = Field(default=None, description="Job title")
194
+ job_profile: Optional[JobProfile] = None
195
+ location: Optional[str] = None
196
+ time_type: Optional[str] = Field(default=None, description="Full-time, Part-time")
197
+
198
+ # Organizational Relationships
199
+ manager: Optional[Manager] = None
200
+ organizations: List[str] = Field(default_factory=list, description="Org names")
201
+
202
+ # Compensation (optional, might be sensitive)
203
+ compensation: Optional[Compensation] = None
204
+
205
+ class Config:
206
+ json_schema_extra = {
207
+ "example": {
208
+ "worker_id": "12345",
209
+ "employee_id": "EMP-001",
210
+ "first_name": "John",
211
+ "last_name": "Doe",
212
+ "full_name": "John Doe",
213
+ "primary_email": "john.doe@company.com",
214
+ "business_title": "Senior Software Engineer",
215
+ "is_active": True
216
+ }
217
+ }
218
+
219
+
220
+ class OrganizationModel(BaseModel):
221
+ """Clean Organization model."""
222
+ org_id: str = Field(description="Organization ID")
223
+ name: str = Field(description="Organization name")
224
+ type: Optional[str] = Field(default=None, description="Org type (Cost Center, Department, etc.)")
225
+ manager: Optional[Manager] = None
226
+ parent_org: Optional[str] = Field(default=None, description="Parent org name")
227
+ superior_org: Optional[str] = None
228
+ is_active: bool = Field(default=True)
229
+
230
+
231
+ class ContactModel(BaseModel):
232
+ """
233
+ Clean Contact model - Default output for contact information.
234
+
235
+ Simplified representation of a worker's contact details.
236
+ """
237
+ worker_id: str = Field(description="Worker ID")
238
+
239
+ # Email addresses
240
+ primary_email: Optional[str] = None
241
+ work_email: Optional[str] = None
242
+ personal_email: Optional[str] = None
243
+ emails: List[EmailAddress] = Field(default_factory=list, description="All email addresses")
244
+
245
+ # Phone numbers
246
+ primary_phone: Optional[str] = None
247
+ work_phone: Optional[str] = None
248
+ mobile_phone: Optional[str] = None
249
+ phones: List[PhoneNumber] = Field(default_factory=list, description="All phone numbers")
250
+
251
+ # Addresses
252
+ primary_address: Optional[Address] = None
253
+ work_address: Optional[Address] = None
254
+ home_address: Optional[Address] = None
255
+ addresses: List[Address] = Field(default_factory=list, description="All addresses")
256
+
257
+ # Additional contact info
258
+ instant_messengers: List[Dict[str, str]] = Field(default_factory=list, description="IM handles")
259
+ social_networks: List[Dict[str, str]] = Field(default_factory=list, description="Social media")
260
+
261
+ class Config:
262
+ json_schema_extra = {
263
+ "example": {
264
+ "worker_id": "12345",
265
+ "primary_email": "john.doe@company.com",
266
+ "work_phone": "+1 (555) 123-4567",
267
+ "mobile_phone": "+1 (555) 987-6543"
268
+ }
269
+ }
270
+
271
+
272
+
273
+ # ==========================================
274
+ # Response Parser with Structured Outputs
275
+ # ==========================================
276
+
277
+ T = TypeVar('T', bound=BaseModel)
278
+
279
+
280
+ class WorkdayResponseParser:
281
+ """
282
+ Parser that transforms verbose Zeep responses into clean Pydantic models.
283
+
284
+ Supports:
285
+ - Default models per object type
286
+ - Custom output formats via output_format parameter
287
+ - Graceful handling of missing fields
288
+ """
289
+
290
+ # Map object types to default models
291
+ DEFAULT_MODELS = {
292
+ "worker": WorkerModel,
293
+ "organization": OrganizationModel,
294
+ "contact": ContactModel,
295
+ "time_off_balance": TimeOffBalanceModel,
296
+ }
297
+
298
+ @staticmethod
299
+ def _safe_get(obj: Any, key: str, default: Any = None) -> Any:
300
+ """
301
+ Safely get a value from obj[key], handling both dicts and lists.
302
+
303
+ If obj is a list, takes first element before getting key.
304
+ Returns default if obj is None, key doesn't exist, or obj is empty list.
305
+ """
306
+ if obj is None:
307
+ return default
308
+
309
+ # If it's a list, take first element
310
+ if isinstance(obj, list):
311
+ if not obj:
312
+ return default
313
+ obj = obj[0]
314
+
315
+ # Now try to get the key
316
+ return obj.get(key, default) if isinstance(obj, dict) else default
317
+
318
+ @staticmethod
319
+ def _safe_navigate(obj: Any, *path: str, default: Any = None) -> Any:
320
+ """
321
+ Safely navigate a deeply nested structure with mixed dicts/lists.
322
+
323
+ Example:
324
+ _safe_navigate(data, "Personal_Data", "Contact_Data", "Email_Address_Data")
325
+
326
+ Each step handles both dict keys and list indexing (takes [0] if list).
327
+ """
328
+ current = obj
329
+ for key in path:
330
+ if current is None:
331
+ return default
332
+
333
+ # Handle list - take first element
334
+ if isinstance(current, list):
335
+ if not current:
336
+ return default
337
+ current = current[0]
338
+
339
+ # Handle dict - get key
340
+ if isinstance(current, dict):
341
+ current = current.get(key)
342
+ else:
343
+ return default
344
+
345
+ return current if current is not None else default
346
+
347
+ @classmethod
348
+ def parse_worker_response(
349
+ cls,
350
+ response: Any,
351
+ output_format: Optional[Type[T]] = None
352
+ ) -> Union[WorkerModel, T]:
353
+ """
354
+ Parse a worker response into a structured model.
355
+
356
+ Args:
357
+ response: Raw Zeep response object (Get_Workers_Response)
358
+ output_format: Optional custom Pydantic model. If None, uses WorkerModel.
359
+
360
+ Returns:
361
+ Parsed worker as specified model type
362
+ """
363
+ # Use default if no custom format provided
364
+ model_class = output_format or cls.DEFAULT_MODELS["worker"]
365
+
366
+ # Serialize Zeep object to dict
367
+ raw = helpers.serialize_object(response)
368
+
369
+ # Navigate to first worker in response
370
+ # Structure: Response_Data.Worker[0]
371
+ response_data = raw.get("Response_Data", {})
372
+ workers = response_data.get("Worker", [])
373
+
374
+ if not workers:
375
+ raise ValueError("No worker found in response")
376
+
377
+ # Get first worker
378
+ worker_element = workers[0] if isinstance(workers, list) else workers
379
+
380
+ # Extract data using the extraction logic
381
+ extracted = cls._extract_worker_data(worker_element)
382
+
383
+ # Instantiate the model
384
+ return model_class(**extracted)
385
+
386
+ @classmethod
387
+ def parse_workers_response(
388
+ cls,
389
+ response: Any,
390
+ output_format: Optional[Type[T]] = None
391
+ ) -> List[Union[WorkerModel, T]]:
392
+ """
393
+ Parse multiple workers from Get_Workers response.
394
+
395
+ Args:
396
+ response: Raw Zeep Get_Workers response
397
+ output_format: Optional custom model for each worker
398
+
399
+ Returns:
400
+ List of parsed workers
401
+ """
402
+ model_class = output_format or cls.DEFAULT_MODELS["worker"]
403
+
404
+ raw = helpers.serialize_object(response)
405
+
406
+ # Navigate to worker array
407
+ response_data = raw.get("Response_Data", {})
408
+ worker_data = response_data.get("Worker", [])
409
+
410
+ # Handle single vs array
411
+ if not isinstance(worker_data, list):
412
+ worker_data = [worker_data] if worker_data else []
413
+
414
+ # Parse each worker
415
+ workers = []
416
+ for worker_raw in worker_data:
417
+ extracted = cls._extract_worker_data(worker_raw)
418
+ workers.append(model_class(**extracted))
419
+
420
+ return workers
421
+
422
+ @classmethod
423
+ def parse_contact_response(
424
+ cls,
425
+ response: Any,
426
+ worker_id: str,
427
+ output_format: Optional[Type[T]] = None
428
+ ) -> Union[ContactModel, T]:
429
+ """
430
+ Parse contact information from Get_Workers response.
431
+
432
+ Args:
433
+ response: Raw Zeep Get_Workers response
434
+ worker_id: Worker ID for reference
435
+ output_format: Optional custom model. Defaults to ContactModel.
436
+
437
+ Returns:
438
+ Parsed contact information
439
+ """
440
+ model_class = output_format or cls.DEFAULT_MODELS["contact"]
441
+
442
+ # Get worker element (same navigation as parse_worker_response)
443
+ raw = helpers.serialize_object(response)
444
+ response_data = raw.get("Response_Data", {})
445
+ workers = response_data.get("Worker", [])
446
+
447
+ if not workers:
448
+ raise ValueError("No worker found in response")
449
+
450
+ worker_element = workers[0] if isinstance(workers, list) else workers
451
+
452
+ # Extract contact data
453
+ extracted = cls._extract_contact_data(worker_element, worker_id)
454
+
455
+ # Instantiate the model
456
+ return model_class(**extracted)
457
+
458
+ @classmethod
459
+ def _extract_contact_data(cls, worker_element: Dict[str, Any], worker_id: str) -> Dict[str, Any]:
460
+ """
461
+ Extract contact information from worker element.
462
+
463
+ Args:
464
+ worker_element: Single Worker element
465
+ worker_id: Worker ID for reference
466
+
467
+ Returns:
468
+ Dict with contact data for ContactModel
469
+ """
470
+ worker_data = worker_element.get("Worker_Data", {})
471
+ if not isinstance(worker_data, dict):
472
+ # Some Workday tenants return an explicit null for Worker_Data when
473
+ # the response group omits most sections (e.g. only requesting time
474
+ # off balance data). Treat these the same as an empty payload so
475
+ # downstream parsing logic can continue gracefully.
476
+ worker_data = {}
477
+ personal = worker_data.get("Personal_Data", {})
478
+ contact_data = personal.get("Contact_Data", {})
479
+
480
+ # Extract emails, phones, addresses using existing methods
481
+ emails = cls._extract_emails(contact_data)
482
+ phones = cls._extract_phones(contact_data)
483
+ addresses = cls._extract_addresses(contact_data)
484
+
485
+ # Determine primary email
486
+ primary_email = next((e.email for e in emails if e.primary), None)
487
+ if not primary_email and emails:
488
+ primary_email = emails[0].email
489
+
490
+ # Find work and personal emails
491
+ work_email = None
492
+ personal_email = None
493
+ for email in emails:
494
+ if email.type and "work" in email.type.lower():
495
+ work_email = email.email
496
+ elif email.type and ("home" in email.type.lower() or "personal" in email.type.lower()):
497
+ personal_email = email.email
498
+
499
+ # Determine primary phone
500
+ primary_phone = next((p.phone for p in phones if p.primary), None)
501
+ if not primary_phone and phones:
502
+ primary_phone = phones[0].phone
503
+
504
+ # Find work and mobile phones
505
+ work_phone = None
506
+ mobile_phone = None
507
+ for phone in phones:
508
+ if phone.type:
509
+ phone_type_lower = phone.type.lower()
510
+ if "work" in phone_type_lower:
511
+ work_phone = phone.phone
512
+ elif "mobile" in phone_type_lower or "cell" in phone_type_lower:
513
+ mobile_phone = phone.phone
514
+
515
+ # Determine primary address
516
+ primary_address = next((a for a in addresses if a.type and "work" in a.type.lower()), None)
517
+ if not primary_address and addresses:
518
+ primary_address = addresses[0]
519
+
520
+ # Find work and home addresses
521
+ work_address = None
522
+ home_address = None
523
+ for addr in addresses:
524
+ if addr.type:
525
+ addr_type_lower = addr.type.lower()
526
+ if "work" in addr_type_lower:
527
+ work_address = addr
528
+ elif "home" in addr_type_lower:
529
+ home_address = addr
530
+
531
+ # Extract instant messengers (if present)
532
+ instant_messengers = []
533
+ im_data = contact_data.get("Instant_Messenger_Data", [])
534
+ if not isinstance(im_data, list):
535
+ im_data = [im_data] if im_data else []
536
+
537
+ for im in im_data:
538
+ if isinstance(im, dict):
539
+ im_address = im.get("Instant_Messenger_Address")
540
+ im_type = cls._safe_navigate(im, "Instant_Messenger_Type_Reference", "descriptor")
541
+ if im_address:
542
+ instant_messengers.append({
543
+ "type": im_type or "Unknown",
544
+ "address": im_address
545
+ })
546
+
547
+ # Extract social networks (if present in Web_Address_Data)
548
+ social_networks = []
549
+ web_data = contact_data.get("Web_Address_Data", [])
550
+ if not isinstance(web_data, list):
551
+ web_data = [web_data] if web_data else []
552
+
553
+ for web in web_data:
554
+ if isinstance(web, dict):
555
+ web_address = web.get("Web_Address")
556
+ web_type = cls._safe_navigate(web, "Usage_Data", "Type_Data", "Type_Reference", "descriptor")
557
+ if web_address:
558
+ social_networks.append({
559
+ "type": web_type or "Website",
560
+ "url": web_address
561
+ })
562
+
563
+ return {
564
+ "worker_id": worker_id,
565
+ "primary_email": primary_email,
566
+ "work_email": work_email,
567
+ "personal_email": personal_email,
568
+ "emails": emails,
569
+ "primary_phone": primary_phone,
570
+ "work_phone": work_phone,
571
+ "mobile_phone": mobile_phone,
572
+ "phones": phones,
573
+ "primary_address": primary_address,
574
+ "work_address": work_address,
575
+ "home_address": home_address,
576
+ "addresses": addresses,
577
+ "instant_messengers": instant_messengers,
578
+ "social_networks": social_networks
579
+ }
580
+
581
+ @classmethod
582
+ def _extract_worker_data(cls, worker_element: Dict[str, Any]) -> Dict[str, Any]:
583
+ """
584
+ Extract and flatten worker data from nested SOAP structure.
585
+
586
+ Args:
587
+ worker_element: Single Worker element from Response_Data.Worker array
588
+
589
+ This is where we handle Workday's verbose structure.
590
+ """
591
+ # Worker element structure: { Worker_Reference, Worker_Descriptor, Worker_Data }
592
+ worker_data = worker_element.get("Worker_Data", {})
593
+
594
+ # References are at the worker_element level, not inside Worker_Data
595
+ worker_ref = worker_element.get("Worker_Reference")
596
+
597
+ # Try to extract IDs from Worker_Reference if present
598
+ worker_id = None
599
+ employee_id = None
600
+
601
+ if worker_ref and isinstance(worker_ref, (dict, list)):
602
+ # Handle both single reference and array
603
+ refs = worker_ref if isinstance(worker_ref, list) else [worker_ref]
604
+ for ref in refs:
605
+ if ref:
606
+ worker_id = cls._extract_id(ref, "WID") or worker_id
607
+ employee_id = cls._extract_id(ref, "Employee_ID") or employee_id
608
+
609
+ # Fallback to Worker_ID field in Worker_Data
610
+ if not worker_id and not employee_id:
611
+ worker_id = worker_data.get("Worker_ID")
612
+ employee_id = worker_data.get("Worker_ID")
613
+
614
+ # Personal Data
615
+ personal = worker_data.get("Personal_Data", {})
616
+ name_data = personal.get("Name_Data", {})
617
+
618
+ # Extract names
619
+ legal_name = name_data.get("Legal_Name_Data", {})
620
+ preferred_name_data = name_data.get("Preferred_Name_Data", {})
621
+
622
+ legal_name_detail = legal_name.get("Name_Detail_Data", {})
623
+ preferred_name_detail = preferred_name_data.get("Name_Detail_Data", {})
624
+
625
+ first_name = (
626
+ preferred_name_detail.get("First_Name") or
627
+ legal_name_detail.get("First_Name", "")
628
+ )
629
+ last_name = (
630
+ preferred_name_detail.get("Last_Name") or
631
+ legal_name_detail.get("Last_Name", "")
632
+ )
633
+ full_name = (
634
+ preferred_name_detail.get("Formatted_Name") or
635
+ legal_name_detail.get("Formatted_Name") or
636
+ f"{first_name} {last_name}".strip()
637
+ )
638
+ preferred_name = preferred_name_detail.get("Formatted_Name")
639
+
640
+ # Contact Data
641
+ contact_data = personal.get("Contact_Data", {})
642
+ emails, personal_email, corporate_email = cls._extract_emails(contact_data)
643
+ phones = cls._extract_phones(contact_data)
644
+ addresses = cls._extract_addresses(contact_data)
645
+
646
+ primary_email = next((e.email for e in emails if e.primary), None)
647
+ if not primary_email and emails:
648
+ primary_email = emails[0].email
649
+
650
+ primary_phone = next((p.phone for p in phones if p.primary), None)
651
+ if not primary_phone and phones:
652
+ primary_phone = phones[0].phone
653
+
654
+ # Employment Data
655
+ employment_data = worker_data.get("Employment_Data", {})
656
+ worker_status = employment_data.get("Worker_Status_Data", {})
657
+
658
+ is_active = worker_status.get("Active", True)
659
+ hire_date = worker_status.get("Hire_Date")
660
+ termination_date = worker_status.get("Termination_Date")
661
+
662
+ # Position Data
663
+ position_data = employment_data.get("Worker_Job_Data", [])
664
+ if not isinstance(position_data, list):
665
+ position_data = [position_data] if position_data else []
666
+
667
+ # Get primary position
668
+ business_title = None
669
+ job_profile = None
670
+ location = None
671
+ time_type = None
672
+
673
+ if position_data:
674
+ primary_position = position_data[0].get("Position_Data", {})
675
+ business_title = primary_position.get("Business_Title")
676
+
677
+ # Job Profile
678
+ if job_profile_data := primary_position.get("Job_Profile_Summary_Data", {}):
679
+ # Use safe navigation for potentially list-valued fields
680
+ job_profile_ref = job_profile_data.get("Job_Profile_Reference", {})
681
+ profile_id = cls._extract_id(job_profile_ref)
682
+
683
+ # Job Family - Based on flowtask, Job_Family_Reference is a list
684
+ job_family = None
685
+ job_family_refs = job_profile_data.get("Job_Family_Reference", [])
686
+ if not isinstance(job_family_refs, list):
687
+ job_family_refs = [job_family_refs] if job_family_refs else []
688
+
689
+ # Extract first Job_Family_ID
690
+ for fam_ref in job_family_refs:
691
+ if isinstance(fam_ref, dict):
692
+ job_family = cls._extract_id(fam_ref, "Job_Family_ID")
693
+ if job_family:
694
+ break
695
+
696
+ job_profile = JobProfile(
697
+ id=profile_id or "",
698
+ name=job_profile_data.get("Job_Profile_Name", ""),
699
+ job_family=job_family,
700
+ management_level=cls._safe_navigate(job_profile_data, "Management_Level_Reference", "descriptor")
701
+ )
702
+
703
+ # Location
704
+ location_data = primary_position.get("Business_Site_Summary_Data", {})
705
+ location = location_data.get("Name") if isinstance(location_data, dict) else None
706
+
707
+ # Time type - use safe navigation
708
+ time_type = cls._safe_navigate(primary_position, "Position_Time_Type_Reference", "descriptor")
709
+
710
+ # Manager - Extract from Manager_as_of_last_detected_manager_change_Reference
711
+ # This is the direct manager, not the management chain
712
+ manager = None
713
+ manager_data = employment_data.get("Worker_Job_Data", [])
714
+ if manager_data:
715
+ if not isinstance(manager_data, list):
716
+ manager_data = [manager_data]
717
+
718
+ # Get manager reference from Position_Data
719
+ position_data = manager_data[0].get("Position_Data", {})
720
+ manager_ref = cls._safe_get(position_data, "Manager_as_of_last_detected_manager_change_Reference")
721
+
722
+ if manager_ref and isinstance(manager_ref, dict):
723
+ # Extract Employee_ID specifically (not WID)
724
+ manager_id = cls._extract_id(manager_ref, "Employee_ID")
725
+ # Get Descriptor (manager name) directly
726
+ manager_name = manager_ref.get("Descriptor")
727
+
728
+ # Only create Manager object if we have both ID and name
729
+ if manager_id and manager_name:
730
+ manager = Manager(
731
+ worker_id=manager_id,
732
+ name=manager_name,
733
+ email=None # Would need separate lookup
734
+ )
735
+
736
+ # Organizations - Based on flowtask structure
737
+ # Organization_Data is a dict containing Worker_Organization_Data list
738
+ organization_data = worker_data.get("Organization_Data", {}) or {}
739
+ worker_orgs = organization_data.get("Worker_Organization_Data", []) or []
740
+
741
+ # Ensure worker_orgs is a list
742
+ if not isinstance(worker_orgs, list):
743
+ worker_orgs = [worker_orgs] if worker_orgs else []
744
+
745
+ organizations = [
746
+ org.get("Organization_Data", {}).get("Organization_Name", "")
747
+ for org in worker_orgs
748
+ if org.get("Organization_Data", {}).get("Organization_Name")
749
+ ]
750
+
751
+ # Compensation (optional)
752
+ comp_data = worker_data.get("Compensation_Data", {})
753
+ compensation = None
754
+ if comp_data:
755
+ compensation = cls._extract_compensation(comp_data)
756
+
757
+ return {
758
+ "worker_id": worker_id or employee_id or "",
759
+ "employee_id": employee_id,
760
+ "first_name": first_name,
761
+ "last_name": last_name,
762
+ "preferred_name": preferred_name,
763
+ "full_name": full_name,
764
+ "primary_email": primary_email,
765
+ "personal_email": personal_email,
766
+ "corporate_email": corporate_email,
767
+ "emails": emails,
768
+ "primary_phone": primary_phone,
769
+ "phones": phones,
770
+ "addresses": addresses,
771
+ "is_active": is_active,
772
+ "hire_date": cls._parse_date(hire_date),
773
+ "termination_date": cls._parse_date(termination_date),
774
+ "business_title": business_title,
775
+ "job_profile": job_profile,
776
+ "location": location,
777
+ "time_type": time_type,
778
+ "manager": manager,
779
+ "organizations": organizations,
780
+ "compensation": compensation
781
+ }
782
+
783
+ @staticmethod
784
+ def _extract_id(ref_obj: Any, id_type: Optional[str] = None) -> Optional[str]:
785
+ """
786
+ Extract ID from a Workday reference object.
787
+
788
+ Handles multiple formats:
789
+ - Single reference with ID array
790
+ - Array of references
791
+ - Dict with nested ID structures
792
+ """
793
+ if not ref_obj:
794
+ return None
795
+
796
+ # If ref_obj is a list of references, take the first one
797
+ if isinstance(ref_obj, list):
798
+ if not ref_obj:
799
+ return None
800
+ ref_obj = ref_obj[0]
801
+
802
+ # Get the ID array
803
+ ids = ref_obj.get("ID", []) if isinstance(ref_obj, dict) else []
804
+ if not isinstance(ids, list):
805
+ ids = [ids] if ids else []
806
+
807
+ # If id_type specified, find matching type
808
+ if id_type:
809
+ for id_obj in ids:
810
+ if isinstance(id_obj, dict) and id_obj.get("type") == id_type:
811
+ return id_obj.get("_value_1")
812
+
813
+ # Otherwise return first ID
814
+ return ids[0].get("_value_1") if ids and isinstance(ids[0], dict) else None
815
+
816
+ @staticmethod
817
+ def _extract_emails(contact_data: Dict[str, Any]) -> tuple[List[EmailAddress], Optional[str], Optional[str]]:
818
+ """
819
+ Extract email addresses and separate personal vs corporate emails.
820
+
821
+ Returns:
822
+ Tuple of (emails_list, personal_email, corporate_email)
823
+ """
824
+ emails = []
825
+ personal_email = None
826
+ corporate_email = None
827
+ email_data = contact_data.get("Email_Address_Data", [])
828
+
829
+ if not isinstance(email_data, list):
830
+ email_data = [email_data] if email_data else []
831
+
832
+ for email_obj in email_data:
833
+ if email_addr := email_obj.get("Email_Address"):
834
+ # Safe navigation through Usage_Data -> Type_Data nested lists
835
+ email_type = None
836
+ usage_type_id = None
837
+ is_primary = False
838
+ is_public = True
839
+
840
+ usage_data = email_obj.get("Usage_Data", [])
841
+ if usage_data and isinstance(usage_data, list) and len(usage_data) > 0:
842
+ usage_item = usage_data[0]
843
+ if isinstance(usage_item, dict):
844
+ # Extract Type from Type_Data array
845
+ type_data = usage_item.get("Type_Data", [])
846
+ if type_data and isinstance(type_data, list) and len(type_data) > 0:
847
+ type_item = type_data[0]
848
+ if isinstance(type_item, dict):
849
+ type_ref = type_item.get("Type_Reference", {})
850
+ if isinstance(type_ref, dict):
851
+ email_type = type_ref.get("descriptor") or type_ref.get("Descriptor")
852
+
853
+ # Extract Communication_Usage_Type_ID (HOME/WORK) - Based on flowtask
854
+ type_ids = type_ref.get("ID", [])
855
+ if not isinstance(type_ids, list):
856
+ type_ids = [type_ids] if type_ids else []
857
+
858
+ # Find Communication_Usage_Type_ID
859
+ for id_obj in type_ids:
860
+ if isinstance(id_obj, dict) and id_obj.get("type") == "Communication_Usage_Type_ID":
861
+ usage_type_id = id_obj.get("_value_1")
862
+ break
863
+
864
+ # Extract Primary flag (at usage_item level, not type_data)
865
+ is_primary = usage_item.get("Primary", False)
866
+ is_public = usage_item.get("Public", True)
867
+
868
+ # Separate personal vs corporate emails based on Communication_Usage_Type_ID
869
+ if usage_type_id == "HOME":
870
+ personal_email = email_addr
871
+ elif usage_type_id == "WORK":
872
+ corporate_email = email_addr
873
+
874
+ emails.append(EmailAddress(
875
+ email=email_addr,
876
+ type=email_type,
877
+ primary=is_primary,
878
+ public=is_public
879
+ ))
880
+
881
+ return emails, personal_email, corporate_email
882
+
883
+ @staticmethod
884
+ def _extract_phones(contact_data: Dict[str, Any]) -> List[PhoneNumber]:
885
+ """Extract phone numbers."""
886
+ phones = []
887
+ phone_data = contact_data.get("Phone_Data", [])
888
+
889
+ if not isinstance(phone_data, list):
890
+ phone_data = [phone_data] if phone_data else []
891
+
892
+ for phone_obj in phone_data:
893
+ if formatted_phone := phone_obj.get("Formatted_Phone"):
894
+ # Safe navigation through Usage_Data -> Type_Data
895
+ phone_type = None
896
+ is_primary = False
897
+
898
+ usage_data = phone_obj.get("Usage_Data", [])
899
+ if usage_data and isinstance(usage_data, list) and len(usage_data) > 0:
900
+ usage_item = usage_data[0]
901
+ if isinstance(usage_item, dict):
902
+ type_data = usage_item.get("Type_Data", [])
903
+ if type_data and isinstance(type_data, list) and len(type_data) > 0:
904
+ type_item = type_data[0]
905
+ if isinstance(type_item, dict):
906
+ type_ref = type_item.get("Type_Reference", {})
907
+ if isinstance(type_ref, dict):
908
+ phone_type = type_ref.get("descriptor") or type_ref.get("Descriptor")
909
+
910
+ is_primary = usage_item.get("Primary", False)
911
+
912
+ phones.append(PhoneNumber(
913
+ phone=formatted_phone,
914
+ type=phone_type,
915
+ primary=is_primary,
916
+ country_code=phone_obj.get("Country_ISO_Code")
917
+ ))
918
+
919
+ return phones
920
+
921
+ @staticmethod
922
+ def _extract_addresses(contact_data: Dict[str, Any]) -> List[Address]:
923
+ """Extract addresses."""
924
+ addresses = []
925
+ address_data = contact_data.get("Address_Data", [])
926
+
927
+ if not isinstance(address_data, list):
928
+ address_data = [address_data] if address_data else []
929
+
930
+ for addr_obj in address_data:
931
+ if formatted := addr_obj.get("Formatted_Address"):
932
+ # Extract address lines
933
+ address_line_1 = None
934
+ address_lines = addr_obj.get("Address_Line_Data", [])
935
+ if address_lines and isinstance(address_lines, list) and len(address_lines) > 0:
936
+ line_item = address_lines[0]
937
+ if isinstance(line_item, dict):
938
+ address_line_1 = line_item.get("_value_1")
939
+
940
+ # Safe navigation for Usage_Data
941
+ addr_type = None
942
+ usage_data = addr_obj.get("Usage_Data", [])
943
+ if usage_data and isinstance(usage_data, list) and len(usage_data) > 0:
944
+ usage_item = usage_data[0]
945
+ if isinstance(usage_item, dict):
946
+ type_data = usage_item.get("Type_Data", [])
947
+ if type_data and isinstance(type_data, list) and len(type_data) > 0:
948
+ type_item = type_data[0]
949
+ if isinstance(type_item, dict):
950
+ type_ref = type_item.get("Type_Reference", {})
951
+ if isinstance(type_ref, dict):
952
+ addr_type = type_ref.get("descriptor") or type_ref.get("Descriptor")
953
+
954
+ # Extract country
955
+ country = None
956
+ country_ref = addr_obj.get("Country_Reference", {})
957
+ if isinstance(country_ref, dict):
958
+ country = country_ref.get("descriptor") or country_ref.get("Descriptor")
959
+
960
+ addresses.append(Address(
961
+ formatted_address=formatted,
962
+ address_line_1=address_line_1,
963
+ address_line_2=None, # Would need to check Address_Line_Data[1]
964
+ city=addr_obj.get("Municipality"),
965
+ region=addr_obj.get("Country_Region_Descriptor"),
966
+ postal_code=addr_obj.get("Postal_Code"),
967
+ country=country,
968
+ type=addr_type
969
+ ))
970
+
971
+ return addresses
972
+
973
+ @staticmethod
974
+ def _extract_compensation(comp_data: Dict[str, Any]) -> Optional[Compensation]:
975
+ """Extract compensation data."""
976
+ # This structure varies significantly by configuration
977
+ # Simplified example:
978
+ try:
979
+ return Compensation(
980
+ base_pay=comp_data.get("Total_Base_Pay"),
981
+ currency=comp_data.get("Currency_Reference", {}).get("descriptor", "USD"),
982
+ pay_frequency=comp_data.get("Frequency_Reference", {}).get("descriptor"),
983
+ effective_date=WorkdayResponseParser._parse_date(comp_data.get("Effective_Date"))
984
+ )
985
+ except Exception:
986
+ return None
987
+
988
+ @staticmethod
989
+ def _parse_date(date_value: Any) -> Optional[date]:
990
+ """Parse various date formats."""
991
+ if not date_value:
992
+ return None
993
+
994
+ if isinstance(date_value, date):
995
+ return date_value
996
+
997
+ if isinstance(date_value, datetime):
998
+ return date_value.date()
999
+
1000
+ if isinstance(date_value, str):
1001
+ with contextlib.suppress(Exception):
1002
+ return datetime.fromisoformat(date_value.replace('Z', '+00:00')).date()
1003
+ return None
1004
+
1005
+ @classmethod
1006
+ def parse_time_off_balance_response(
1007
+ cls,
1008
+ response: Any,
1009
+ worker_id: str,
1010
+ output_format: Optional[Type[T]] = None
1011
+ ) -> Union[TimeOffBalanceModel, T]:
1012
+ """
1013
+ Parse time off balance information from Get_Workers response.
1014
+
1015
+ Args:
1016
+ response: Raw Zeep Get_Workers response
1017
+ worker_id: Worker ID for reference
1018
+ output_format: Optional custom model. Defaults to TimeOffBalanceModel.
1019
+
1020
+ Returns:
1021
+ Parsed time off balance information
1022
+ """
1023
+ model_class = output_format or cls.DEFAULT_MODELS["time_off_balance"]
1024
+ # Get worker element (same navigation as other parsers)
1025
+ raw = helpers.serialize_object(response)
1026
+ response_data = raw.get("Response_Data", {})
1027
+ workers = response_data.get("Worker", [])
1028
+
1029
+ if not workers:
1030
+ raise ValueError("No worker found in response")
1031
+
1032
+ worker_element = workers[0] if isinstance(workers, list) else workers
1033
+ # Extract time off balance data
1034
+ extracted = cls._extract_time_off_balance_data(worker_element, worker_id)
1035
+ # Instantiate the model
1036
+ return model_class(**extracted)
1037
+
1038
+ @classmethod
1039
+ def _extract_time_off_balance_data(
1040
+ cls,
1041
+ worker_element: Dict[str, Any],
1042
+ worker_id: str
1043
+ ) -> Dict[str, Any]:
1044
+ """
1045
+ Extract time off balance information from worker element.
1046
+
1047
+ Args:
1048
+ worker_element: Single Worker element
1049
+ worker_id: Worker ID for reference
1050
+
1051
+ Returns:
1052
+ Dict with time off balance data for TimeOffBalanceModel
1053
+ """
1054
+ worker_data = worker_element.get("Worker_Data", {})
1055
+ if not isinstance(worker_data, dict):
1056
+ # Some Workday tenants may explicitly return null for Worker_Data
1057
+ # when only a subset of response groups are requested. Treat this
1058
+ # as an empty payload so balance parsing can continue.
1059
+ worker_data = {}
1060
+
1061
+ # Time off balance data is typically in a dedicated section
1062
+ # The exact structure varies by Workday configuration
1063
+ time_off_data = worker_data.get("Time_Off_Balance_Data", [])
1064
+
1065
+ if not isinstance(time_off_data, list):
1066
+ time_off_data = [time_off_data] if time_off_data else []
1067
+
1068
+ # Parse each time off type balance
1069
+ balances = []
1070
+ vacation_balance = None
1071
+ sick_balance = None
1072
+ personal_balance = None
1073
+ total_available = 0.0
1074
+
1075
+ for balance_item in time_off_data:
1076
+ if not isinstance(balance_item, dict):
1077
+ continue
1078
+
1079
+ # Extract time off type
1080
+ time_off_type_ref = balance_item.get("Time_Off_Type_Reference", {})
1081
+ time_off_type = cls._safe_navigate(time_off_type_ref, "descriptor") or "Unknown"
1082
+ time_off_type_id = cls._extract_id(time_off_type_ref)
1083
+
1084
+ # Extract balance values
1085
+ balance = balance_item.get("Balance", 0.0)
1086
+ if balance and not isinstance(balance, (int, float)):
1087
+ try:
1088
+ balance = float(balance)
1089
+ except (ValueError, TypeError):
1090
+ balance = 0.0
1091
+
1092
+ # Extract unit
1093
+ balance_unit_ref = balance_item.get("Unit_Reference", {})
1094
+ balance_unit = cls._safe_navigate(balance_unit_ref, "descriptor") or "Hours"
1095
+
1096
+ # Extract scheduled/pending
1097
+ scheduled = balance_item.get("Scheduled_Balance", 0.0)
1098
+ if scheduled and not isinstance(scheduled, (int, float)):
1099
+ try:
1100
+ scheduled = float(scheduled)
1101
+ except (ValueError, TypeError):
1102
+ scheduled = 0.0
1103
+
1104
+ # Calculate available
1105
+ available = balance - scheduled if balance and scheduled else balance
1106
+
1107
+ # Extract accrual information
1108
+ accrued_ytd = balance_item.get("Accrued_Year_to_Date")
1109
+ if accrued_ytd and not isinstance(accrued_ytd, (int, float)):
1110
+ try:
1111
+ accrued_ytd = float(accrued_ytd)
1112
+ except (ValueError, TypeError):
1113
+ accrued_ytd = None
1114
+
1115
+ used_ytd = balance_item.get("Used_Year_to_Date")
1116
+ if used_ytd and not isinstance(used_ytd, (int, float)):
1117
+ try:
1118
+ used_ytd = float(used_ytd)
1119
+ except (ValueError, TypeError):
1120
+ used_ytd = None
1121
+
1122
+ # Extract carryover
1123
+ carryover = balance_item.get("Carryover_Balance")
1124
+ if carryover and not isinstance(carryover, (int, float)):
1125
+ try:
1126
+ carryover = float(carryover)
1127
+ except (ValueError, TypeError):
1128
+ carryover = None
1129
+
1130
+ carryover_limit = balance_item.get("Maximum_Carryover_Balance")
1131
+ if carryover_limit and not isinstance(carryover_limit, (int, float)):
1132
+ try:
1133
+ carryover_limit = float(carryover_limit)
1134
+ except (ValueError, TypeError):
1135
+ carryover_limit = None
1136
+
1137
+ # Extract dates
1138
+ as_of_date = cls._parse_date(balance_item.get("As_of_Date"))
1139
+ plan_year_start = cls._parse_date(balance_item.get("Plan_Year_Start_Date"))
1140
+ plan_year_end = cls._parse_date(balance_item.get("Plan_Year_End_Date"))
1141
+
1142
+ # Create TimeOffBalance object
1143
+ time_off_balance = TimeOffBalance(
1144
+ time_off_type=time_off_type,
1145
+ time_off_type_id=time_off_type_id,
1146
+ balance=balance or 0.0,
1147
+ balance_unit=balance_unit,
1148
+ scheduled=scheduled,
1149
+ available=available,
1150
+ accrued_ytd=accrued_ytd,
1151
+ used_ytd=used_ytd,
1152
+ carryover=carryover,
1153
+ carryover_limit=carryover_limit,
1154
+ as_of_date=as_of_date,
1155
+ plan_year_start=plan_year_start,
1156
+ plan_year_end=plan_year_end
1157
+ )
1158
+
1159
+ balances.append(time_off_balance)
1160
+
1161
+ # Track quick-access balances
1162
+ time_off_type_lower = time_off_type.lower()
1163
+ if "vacation" in time_off_type_lower or "pto" in time_off_type_lower:
1164
+ vacation_balance = available or balance
1165
+ elif "sick" in time_off_type_lower:
1166
+ sick_balance = available or balance
1167
+ elif "personal" in time_off_type_lower:
1168
+ personal_balance = available or balance
1169
+
1170
+ # Add to total available
1171
+ if available:
1172
+ total_available += available
1173
+
1174
+ # Determine as_of_date
1175
+ as_of_date = datetime.now().date()
1176
+ if balances and balances[0].as_of_date:
1177
+ as_of_date = balances[0].as_of_date
1178
+
1179
+ return {
1180
+ "worker_id": worker_id,
1181
+ "as_of_date": as_of_date,
1182
+ "balances": balances,
1183
+ "vacation_balance": vacation_balance,
1184
+ "sick_balance": sick_balance,
1185
+ "personal_balance": personal_balance,
1186
+ "total_available_hours": total_available if total_available > 0 else None
1187
+ }
1188
+
1189
+ @classmethod
1190
+ def parse_time_off_plan_balances_response(
1191
+ cls,
1192
+ response: Any,
1193
+ worker_id: str,
1194
+ output_format: Optional[Type[T]] = None
1195
+ ) -> Union[TimeOffBalanceModel, T]:
1196
+ """
1197
+ Parse Get_Time_Off_Plan_Balances response from Absence Management API.
1198
+
1199
+ This parser handles the response from the dedicated Absence Management
1200
+ API which has a different structure than Get_Workers.
1201
+
1202
+ Args:
1203
+ response: Raw Zeep Get_Time_Off_Plan_Balances response
1204
+ worker_id: Worker ID for reference
1205
+ output_format: Optional custom model. Defaults to TimeOffBalanceModel.
1206
+
1207
+ Returns:
1208
+ Parsed time off balance information
1209
+ """
1210
+ model_class = output_format or cls.DEFAULT_MODELS["time_off_balance"]
1211
+
1212
+ # Serialize Zeep object to dict
1213
+ raw = helpers.serialize_object(response)
1214
+
1215
+ # Navigate to Response_Data
1216
+ response_data = raw.get("Response_Data", [])
1217
+
1218
+ # Response_Data is a list of items, each containing Time_Off_Plan_Balance
1219
+ balance_items = []
1220
+ if isinstance(response_data, list):
1221
+ for item in response_data:
1222
+ if isinstance(item, dict):
1223
+ # Extract Time_Off_Plan_Balance from each item
1224
+ tof_balance = item.get("Time_Off_Plan_Balance", [])
1225
+ if isinstance(tof_balance, list):
1226
+ balance_items.extend(tof_balance)
1227
+ elif tof_balance:
1228
+ balance_items.append(tof_balance)
1229
+ elif isinstance(response_data, dict):
1230
+ # Fallback: single dict
1231
+ tof_balance = response_data.get("Time_Off_Plan_Balance", [])
1232
+ if isinstance(tof_balance, list):
1233
+ balance_items.extend(tof_balance)
1234
+ elif tof_balance:
1235
+ balance_items.append(tof_balance)
1236
+
1237
+ # We'll process all balances for this worker
1238
+ all_balances = []
1239
+ vacation_balance = None
1240
+ sick_balance = None
1241
+ personal_balance = None
1242
+ total_available = 0.0
1243
+ as_of_date = datetime.now().date()
1244
+
1245
+ # Process each Time_Off_Plan_Balance
1246
+ for balance_item in balance_items:
1247
+ if not isinstance(balance_item, dict):
1248
+ continue
1249
+
1250
+ # Extract worker information from Employee_Reference
1251
+ employee_ref = balance_item.get("Employee_Reference", {})
1252
+ item_worker_id = None
1253
+ if employee_ref:
1254
+ employee_ids = employee_ref.get("ID", [])
1255
+ if isinstance(employee_ids, list):
1256
+ for emp_id in employee_ids:
1257
+ if isinstance(emp_id, dict) and emp_id.get("type") == "Employee_ID":
1258
+ item_worker_id = emp_id.get("_value_1")
1259
+ break
1260
+
1261
+ # Skip if this balance isn't for our worker
1262
+ if item_worker_id and item_worker_id != worker_id:
1263
+ continue
1264
+
1265
+ # Get the balance data container (note: it's Time_Off_Plan_Balance_Data, not a separate key)
1266
+ balance_data_container = balance_item.get("Time_Off_Plan_Balance_Data", {})
1267
+ if not isinstance(balance_data_container, dict):
1268
+ balance_data_container = {}
1269
+
1270
+ # Get the list of balance records (one per plan)
1271
+ balance_records = balance_data_container.get("Time_Off_Plan_Balance_Record", [])
1272
+ if not isinstance(balance_records, list):
1273
+ balance_records = [balance_records] if balance_records else []
1274
+
1275
+ # Parse each balance record (one per plan)
1276
+ for record in balance_records:
1277
+ if not isinstance(record, dict):
1278
+ continue
1279
+
1280
+ # Time Off Plan information
1281
+ time_off_plan_ref = record.get("Time_Off_Plan_Reference", {})
1282
+
1283
+ # Extract time off plan ID and use it as name if Descriptor is not present
1284
+ time_off_type = None
1285
+ time_off_type_id = None
1286
+ plan_ids = time_off_plan_ref.get("ID", [])
1287
+ if isinstance(plan_ids, list):
1288
+ for plan_id in plan_ids:
1289
+ if isinstance(plan_id, dict):
1290
+ if plan_id.get("type") == "Absence_Plan_ID":
1291
+ time_off_type_id = plan_id.get("_value_1")
1292
+ # Use Absence_Plan_ID as the type name if no Descriptor
1293
+ if not time_off_type:
1294
+ time_off_type = time_off_type_id
1295
+ break
1296
+ elif plan_id.get("type") == "WID" and not time_off_type_id:
1297
+ time_off_type_id = plan_id.get("_value_1")
1298
+
1299
+ # Override with Descriptor if present
1300
+ if time_off_plan_ref.get("Descriptor"):
1301
+ time_off_type = time_off_plan_ref.get("Descriptor")
1302
+
1303
+ # Fallback to "Unknown" if still no type found
1304
+ if not time_off_type:
1305
+ time_off_type = "Unknown"
1306
+
1307
+ # Unit of time
1308
+ unit_ref = record.get("Unit_of_Time_Reference", {})
1309
+ unit_ids = unit_ref.get("ID", [])
1310
+ balance_unit = "Hours"
1311
+ if isinstance(unit_ids, list):
1312
+ for unit_id in unit_ids:
1313
+ if isinstance(unit_id, dict) and unit_id.get("type") == "Unit_of_Time_ID":
1314
+ balance_unit = unit_id.get("_value_1", "Hours")
1315
+ break
1316
+
1317
+ # Balance from position record (can be list or dict)
1318
+ position_records = record.get("Time_Off_Plan_Balance_Position_Record", [])
1319
+
1320
+ balance_value = 0.0
1321
+ if isinstance(position_records, list) and len(position_records) > 0:
1322
+ position_record = position_records[0]
1323
+ if isinstance(position_record, dict):
1324
+ balance_raw = position_record.get("Time_Off_Plan_Balance")
1325
+ balance_value = cls._parse_float(balance_raw) or 0.0
1326
+ elif isinstance(position_records, dict):
1327
+ # Fallback if it's a single dict
1328
+ balance_raw = position_records.get("Time_Off_Plan_Balance")
1329
+ balance_value = cls._parse_float(balance_raw) or 0.0
1330
+
1331
+ # Create TimeOffBalance object
1332
+ time_off_balance = TimeOffBalance(
1333
+ time_off_type=time_off_type,
1334
+ time_off_type_id=time_off_type_id,
1335
+ balance=balance_value,
1336
+ balance_unit=balance_unit,
1337
+ scheduled=None, # Not available in this API
1338
+ available=balance_value, # Assume full balance is available
1339
+ accrued_ytd=None,
1340
+ used_ytd=None,
1341
+ carryover=None,
1342
+ carryover_limit=None,
1343
+ as_of_date=None,
1344
+ plan_year_start=None,
1345
+ plan_year_end=None
1346
+ )
1347
+
1348
+ all_balances.append(time_off_balance)
1349
+
1350
+ # Track quick-access balances
1351
+ time_off_type_lower = time_off_type.lower()
1352
+ if "vacation" in time_off_type_lower or "pto" in time_off_type_lower:
1353
+ vacation_balance = balance_value
1354
+ elif "sick" in time_off_type_lower:
1355
+ sick_balance = balance_value
1356
+ elif "personal" in time_off_type_lower:
1357
+ personal_balance = balance_value
1358
+
1359
+ # Add to total available
1360
+ total_available += balance_value
1361
+
1362
+ return model_class(
1363
+ worker_id=worker_id,
1364
+ as_of_date=as_of_date,
1365
+ balances=all_balances,
1366
+ vacation_balance=vacation_balance,
1367
+ sick_balance=sick_balance,
1368
+ personal_balance=personal_balance,
1369
+ total_available_hours=total_available if total_available > 0 else None
1370
+ )
1371
+
1372
+ @staticmethod
1373
+ def _parse_float(value: Any) -> Optional[float]:
1374
+ """Parse float value from Workday response (similar to flowtask)"""
1375
+ from decimal import Decimal
1376
+
1377
+ if value is None:
1378
+ return None
1379
+
1380
+ try:
1381
+ if isinstance(value, (int, float, Decimal)):
1382
+ return float(value)
1383
+ elif isinstance(value, str):
1384
+ return float(value)
1385
+ elif isinstance(value, dict):
1386
+ return float(value.get("_value_1", 0))
1387
+ return None
1388
+ except (ValueError, TypeError):
1389
+ return None