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.
- agentui/.prettierrc +15 -0
- agentui/QUICKSTART.md +272 -0
- agentui/README.md +59 -0
- agentui/env.example +16 -0
- agentui/jsconfig.json +14 -0
- agentui/package-lock.json +4242 -0
- agentui/package.json +34 -0
- agentui/scripts/postinstall/apply-patches.mjs +260 -0
- agentui/src/app.css +61 -0
- agentui/src/app.d.ts +13 -0
- agentui/src/app.html +12 -0
- agentui/src/components/LoadingSpinner.svelte +64 -0
- agentui/src/components/ThemeSwitcher.svelte +159 -0
- agentui/src/components/index.js +4 -0
- agentui/src/lib/api/bots.ts +60 -0
- agentui/src/lib/api/chat.ts +22 -0
- agentui/src/lib/api/http.ts +25 -0
- agentui/src/lib/components/BotCard.svelte +33 -0
- agentui/src/lib/components/ChatBubble.svelte +63 -0
- agentui/src/lib/components/Toast.svelte +21 -0
- agentui/src/lib/config.ts +20 -0
- agentui/src/lib/stores/auth.svelte.ts +73 -0
- agentui/src/lib/stores/theme.svelte.js +64 -0
- agentui/src/lib/stores/toast.svelte.ts +31 -0
- agentui/src/lib/utils/conversation.ts +39 -0
- agentui/src/routes/+layout.svelte +20 -0
- agentui/src/routes/+page.svelte +232 -0
- agentui/src/routes/login/+page.svelte +200 -0
- agentui/src/routes/talk/[agentId]/+page.svelte +297 -0
- agentui/src/routes/talk/[agentId]/+page.ts +7 -0
- agentui/static/README.md +1 -0
- agentui/svelte.config.js +11 -0
- agentui/tailwind.config.ts +53 -0
- agentui/tsconfig.json +3 -0
- agentui/vite.config.ts +10 -0
- ai_parrot-0.17.2.dist-info/METADATA +472 -0
- ai_parrot-0.17.2.dist-info/RECORD +535 -0
- ai_parrot-0.17.2.dist-info/WHEEL +6 -0
- ai_parrot-0.17.2.dist-info/entry_points.txt +2 -0
- ai_parrot-0.17.2.dist-info/licenses/LICENSE +21 -0
- ai_parrot-0.17.2.dist-info/top_level.txt +6 -0
- crew-builder/.prettierrc +15 -0
- crew-builder/QUICKSTART.md +259 -0
- crew-builder/README.md +113 -0
- crew-builder/env.example +17 -0
- crew-builder/jsconfig.json +14 -0
- crew-builder/package-lock.json +4182 -0
- crew-builder/package.json +37 -0
- crew-builder/scripts/postinstall/apply-patches.mjs +260 -0
- crew-builder/src/app.css +62 -0
- crew-builder/src/app.d.ts +13 -0
- crew-builder/src/app.html +12 -0
- crew-builder/src/components/LoadingSpinner.svelte +64 -0
- crew-builder/src/components/ThemeSwitcher.svelte +149 -0
- crew-builder/src/components/index.js +9 -0
- crew-builder/src/lib/api/bots.ts +60 -0
- crew-builder/src/lib/api/chat.ts +80 -0
- crew-builder/src/lib/api/client.ts +56 -0
- crew-builder/src/lib/api/crew/crew.ts +136 -0
- crew-builder/src/lib/api/index.ts +5 -0
- crew-builder/src/lib/api/o365/auth.ts +65 -0
- crew-builder/src/lib/auth/auth.ts +54 -0
- crew-builder/src/lib/components/AgentNode.svelte +43 -0
- crew-builder/src/lib/components/BotCard.svelte +33 -0
- crew-builder/src/lib/components/ChatBubble.svelte +67 -0
- crew-builder/src/lib/components/ConfigPanel.svelte +278 -0
- crew-builder/src/lib/components/JsonTreeNode.svelte +76 -0
- crew-builder/src/lib/components/JsonViewer.svelte +24 -0
- crew-builder/src/lib/components/MarkdownEditor.svelte +48 -0
- crew-builder/src/lib/components/ThemeToggle.svelte +36 -0
- crew-builder/src/lib/components/Toast.svelte +67 -0
- crew-builder/src/lib/components/Toolbar.svelte +157 -0
- crew-builder/src/lib/components/index.ts +10 -0
- crew-builder/src/lib/config.ts +8 -0
- crew-builder/src/lib/stores/auth.svelte.ts +228 -0
- crew-builder/src/lib/stores/crewStore.ts +369 -0
- crew-builder/src/lib/stores/theme.svelte.js +145 -0
- crew-builder/src/lib/stores/toast.svelte.ts +69 -0
- crew-builder/src/lib/utils/conversation.ts +39 -0
- crew-builder/src/lib/utils/markdown.ts +122 -0
- crew-builder/src/lib/utils/talkHistory.ts +47 -0
- crew-builder/src/routes/+layout.svelte +20 -0
- crew-builder/src/routes/+page.svelte +539 -0
- crew-builder/src/routes/agents/+page.svelte +247 -0
- crew-builder/src/routes/agents/[agentId]/+page.svelte +288 -0
- crew-builder/src/routes/agents/[agentId]/+page.ts +7 -0
- crew-builder/src/routes/builder/+page.svelte +204 -0
- crew-builder/src/routes/crew/ask/+page.svelte +1052 -0
- crew-builder/src/routes/crew/ask/+page.ts +1 -0
- crew-builder/src/routes/integrations/o365/+page.svelte +304 -0
- crew-builder/src/routes/login/+page.svelte +197 -0
- crew-builder/src/routes/talk/[agentId]/+page.svelte +487 -0
- crew-builder/src/routes/talk/[agentId]/+page.ts +7 -0
- crew-builder/static/README.md +1 -0
- crew-builder/svelte.config.js +11 -0
- crew-builder/tailwind.config.ts +53 -0
- crew-builder/tsconfig.json +3 -0
- crew-builder/vite.config.ts +10 -0
- mcp_servers/calculator_server.py +309 -0
- parrot/__init__.py +27 -0
- parrot/__pycache__/__init__.cpython-310.pyc +0 -0
- parrot/__pycache__/version.cpython-310.pyc +0 -0
- parrot/_version.py +34 -0
- parrot/a2a/__init__.py +48 -0
- parrot/a2a/client.py +658 -0
- parrot/a2a/discovery.py +89 -0
- parrot/a2a/mixin.py +257 -0
- parrot/a2a/models.py +376 -0
- parrot/a2a/server.py +770 -0
- parrot/agents/__init__.py +29 -0
- parrot/bots/__init__.py +12 -0
- parrot/bots/a2a_agent.py +19 -0
- parrot/bots/abstract.py +3139 -0
- parrot/bots/agent.py +1129 -0
- parrot/bots/basic.py +9 -0
- parrot/bots/chatbot.py +669 -0
- parrot/bots/data.py +1618 -0
- parrot/bots/database/__init__.py +5 -0
- parrot/bots/database/abstract.py +3071 -0
- parrot/bots/database/cache.py +286 -0
- parrot/bots/database/models.py +468 -0
- parrot/bots/database/prompts.py +154 -0
- parrot/bots/database/retries.py +98 -0
- parrot/bots/database/router.py +269 -0
- parrot/bots/database/sql.py +41 -0
- parrot/bots/db/__init__.py +6 -0
- parrot/bots/db/abstract.py +556 -0
- parrot/bots/db/bigquery.py +602 -0
- parrot/bots/db/cache.py +85 -0
- parrot/bots/db/documentdb.py +668 -0
- parrot/bots/db/elastic.py +1014 -0
- parrot/bots/db/influx.py +898 -0
- parrot/bots/db/mock.py +96 -0
- parrot/bots/db/multi.py +783 -0
- parrot/bots/db/prompts.py +185 -0
- parrot/bots/db/sql.py +1255 -0
- parrot/bots/db/tools.py +212 -0
- parrot/bots/document.py +680 -0
- parrot/bots/hrbot.py +15 -0
- parrot/bots/kb.py +170 -0
- parrot/bots/mcp.py +36 -0
- parrot/bots/orchestration/README.md +463 -0
- parrot/bots/orchestration/__init__.py +1 -0
- parrot/bots/orchestration/agent.py +155 -0
- parrot/bots/orchestration/crew.py +3330 -0
- parrot/bots/orchestration/fsm.py +1179 -0
- parrot/bots/orchestration/hr.py +434 -0
- parrot/bots/orchestration/storage/__init__.py +4 -0
- parrot/bots/orchestration/storage/memory.py +100 -0
- parrot/bots/orchestration/storage/mixin.py +119 -0
- parrot/bots/orchestration/verify.py +202 -0
- parrot/bots/product.py +204 -0
- parrot/bots/prompts/__init__.py +96 -0
- parrot/bots/prompts/agents.py +155 -0
- parrot/bots/prompts/data.py +216 -0
- parrot/bots/prompts/output_generation.py +8 -0
- parrot/bots/scraper/__init__.py +3 -0
- parrot/bots/scraper/models.py +122 -0
- parrot/bots/scraper/scraper.py +1173 -0
- parrot/bots/scraper/templates.py +115 -0
- parrot/bots/stores/__init__.py +5 -0
- parrot/bots/stores/local.py +172 -0
- parrot/bots/webdev.py +81 -0
- parrot/cli.py +17 -0
- parrot/clients/__init__.py +16 -0
- parrot/clients/base.py +1491 -0
- parrot/clients/claude.py +1191 -0
- parrot/clients/factory.py +129 -0
- parrot/clients/google.py +4567 -0
- parrot/clients/gpt.py +1975 -0
- parrot/clients/grok.py +432 -0
- parrot/clients/groq.py +986 -0
- parrot/clients/hf.py +582 -0
- parrot/clients/models.py +18 -0
- parrot/conf.py +395 -0
- parrot/embeddings/__init__.py +9 -0
- parrot/embeddings/base.py +157 -0
- parrot/embeddings/google.py +98 -0
- parrot/embeddings/huggingface.py +74 -0
- parrot/embeddings/openai.py +84 -0
- parrot/embeddings/processor.py +88 -0
- parrot/exceptions.c +13868 -0
- parrot/exceptions.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/exceptions.pxd +22 -0
- parrot/exceptions.pxi +15 -0
- parrot/exceptions.pyx +44 -0
- parrot/generators/__init__.py +29 -0
- parrot/generators/base.py +200 -0
- parrot/generators/html.py +293 -0
- parrot/generators/react.py +205 -0
- parrot/generators/streamlit.py +203 -0
- parrot/generators/template.py +105 -0
- parrot/handlers/__init__.py +4 -0
- parrot/handlers/agent.py +861 -0
- parrot/handlers/agents/__init__.py +1 -0
- parrot/handlers/agents/abstract.py +900 -0
- parrot/handlers/bots.py +338 -0
- parrot/handlers/chat.py +915 -0
- parrot/handlers/creation.sql +192 -0
- parrot/handlers/crew/ARCHITECTURE.md +362 -0
- parrot/handlers/crew/README_BOTMANAGER_PERSISTENCE.md +303 -0
- parrot/handlers/crew/README_REDIS_PERSISTENCE.md +366 -0
- parrot/handlers/crew/__init__.py +0 -0
- parrot/handlers/crew/handler.py +801 -0
- parrot/handlers/crew/models.py +229 -0
- parrot/handlers/crew/redis_persistence.py +523 -0
- parrot/handlers/jobs/__init__.py +10 -0
- parrot/handlers/jobs/job.py +384 -0
- parrot/handlers/jobs/mixin.py +627 -0
- parrot/handlers/jobs/models.py +115 -0
- parrot/handlers/jobs/worker.py +31 -0
- parrot/handlers/models.py +596 -0
- parrot/handlers/o365_auth.py +105 -0
- parrot/handlers/stream.py +337 -0
- parrot/interfaces/__init__.py +6 -0
- parrot/interfaces/aws.py +143 -0
- parrot/interfaces/credentials.py +113 -0
- parrot/interfaces/database.py +27 -0
- parrot/interfaces/google.py +1123 -0
- parrot/interfaces/hierarchy.py +1227 -0
- parrot/interfaces/http.py +651 -0
- parrot/interfaces/images/__init__.py +0 -0
- parrot/interfaces/images/plugins/__init__.py +24 -0
- parrot/interfaces/images/plugins/abstract.py +58 -0
- parrot/interfaces/images/plugins/analisys.py +148 -0
- parrot/interfaces/images/plugins/classify.py +150 -0
- parrot/interfaces/images/plugins/classifybase.py +182 -0
- parrot/interfaces/images/plugins/detect.py +150 -0
- parrot/interfaces/images/plugins/exif.py +1103 -0
- parrot/interfaces/images/plugins/hash.py +52 -0
- parrot/interfaces/images/plugins/vision.py +104 -0
- parrot/interfaces/images/plugins/yolo.py +66 -0
- parrot/interfaces/images/plugins/zerodetect.py +197 -0
- parrot/interfaces/o365.py +978 -0
- parrot/interfaces/onedrive.py +822 -0
- parrot/interfaces/sharepoint.py +1435 -0
- parrot/interfaces/soap.py +257 -0
- parrot/loaders/__init__.py +8 -0
- parrot/loaders/abstract.py +1131 -0
- parrot/loaders/audio.py +199 -0
- parrot/loaders/basepdf.py +53 -0
- parrot/loaders/basevideo.py +1568 -0
- parrot/loaders/csv.py +409 -0
- parrot/loaders/docx.py +116 -0
- parrot/loaders/epubloader.py +316 -0
- parrot/loaders/excel.py +199 -0
- parrot/loaders/factory.py +55 -0
- parrot/loaders/files/__init__.py +0 -0
- parrot/loaders/files/abstract.py +39 -0
- parrot/loaders/files/html.py +26 -0
- parrot/loaders/files/text.py +63 -0
- parrot/loaders/html.py +152 -0
- parrot/loaders/markdown.py +442 -0
- parrot/loaders/pdf.py +373 -0
- parrot/loaders/pdfmark.py +320 -0
- parrot/loaders/pdftables.py +506 -0
- parrot/loaders/ppt.py +476 -0
- parrot/loaders/qa.py +63 -0
- parrot/loaders/splitters/__init__.py +10 -0
- parrot/loaders/splitters/base.py +138 -0
- parrot/loaders/splitters/md.py +228 -0
- parrot/loaders/splitters/token.py +143 -0
- parrot/loaders/txt.py +26 -0
- parrot/loaders/video.py +89 -0
- parrot/loaders/videolocal.py +218 -0
- parrot/loaders/videounderstanding.py +377 -0
- parrot/loaders/vimeo.py +167 -0
- parrot/loaders/web.py +599 -0
- parrot/loaders/youtube.py +504 -0
- parrot/manager/__init__.py +5 -0
- parrot/manager/manager.py +1030 -0
- parrot/mcp/__init__.py +28 -0
- parrot/mcp/adapter.py +105 -0
- parrot/mcp/cli.py +174 -0
- parrot/mcp/client.py +119 -0
- parrot/mcp/config.py +75 -0
- parrot/mcp/integration.py +842 -0
- parrot/mcp/oauth.py +933 -0
- parrot/mcp/server.py +225 -0
- parrot/mcp/transports/__init__.py +3 -0
- parrot/mcp/transports/base.py +279 -0
- parrot/mcp/transports/grpc_session.py +163 -0
- parrot/mcp/transports/http.py +312 -0
- parrot/mcp/transports/mcp.proto +108 -0
- parrot/mcp/transports/quic.py +1082 -0
- parrot/mcp/transports/sse.py +330 -0
- parrot/mcp/transports/stdio.py +309 -0
- parrot/mcp/transports/unix.py +395 -0
- parrot/mcp/transports/websocket.py +547 -0
- parrot/memory/__init__.py +16 -0
- parrot/memory/abstract.py +209 -0
- parrot/memory/agent.py +32 -0
- parrot/memory/cache.py +175 -0
- parrot/memory/core.py +555 -0
- parrot/memory/file.py +153 -0
- parrot/memory/mem.py +131 -0
- parrot/memory/redis.py +613 -0
- parrot/models/__init__.py +46 -0
- parrot/models/basic.py +118 -0
- parrot/models/compliance.py +208 -0
- parrot/models/crew.py +395 -0
- parrot/models/detections.py +654 -0
- parrot/models/generation.py +85 -0
- parrot/models/google.py +223 -0
- parrot/models/groq.py +23 -0
- parrot/models/openai.py +30 -0
- parrot/models/outputs.py +285 -0
- parrot/models/responses.py +938 -0
- parrot/notifications/__init__.py +743 -0
- parrot/openapi/__init__.py +3 -0
- parrot/openapi/components.yaml +641 -0
- parrot/openapi/config.py +322 -0
- parrot/outputs/__init__.py +32 -0
- parrot/outputs/formats/__init__.py +108 -0
- parrot/outputs/formats/altair.py +359 -0
- parrot/outputs/formats/application.py +122 -0
- parrot/outputs/formats/base.py +351 -0
- parrot/outputs/formats/bokeh.py +356 -0
- parrot/outputs/formats/card.py +424 -0
- parrot/outputs/formats/chart.py +436 -0
- parrot/outputs/formats/d3.py +255 -0
- parrot/outputs/formats/echarts.py +310 -0
- parrot/outputs/formats/generators/__init__.py +0 -0
- parrot/outputs/formats/generators/abstract.py +61 -0
- parrot/outputs/formats/generators/panel.py +145 -0
- parrot/outputs/formats/generators/streamlit.py +86 -0
- parrot/outputs/formats/generators/terminal.py +63 -0
- parrot/outputs/formats/holoviews.py +310 -0
- parrot/outputs/formats/html.py +147 -0
- parrot/outputs/formats/jinja2.py +46 -0
- parrot/outputs/formats/json.py +87 -0
- parrot/outputs/formats/map.py +933 -0
- parrot/outputs/formats/markdown.py +172 -0
- parrot/outputs/formats/matplotlib.py +237 -0
- parrot/outputs/formats/mixins/__init__.py +0 -0
- parrot/outputs/formats/mixins/emaps.py +855 -0
- parrot/outputs/formats/plotly.py +341 -0
- parrot/outputs/formats/seaborn.py +310 -0
- parrot/outputs/formats/table.py +397 -0
- parrot/outputs/formats/template_report.py +138 -0
- parrot/outputs/formats/yaml.py +125 -0
- parrot/outputs/formatter.py +152 -0
- parrot/outputs/templates/__init__.py +95 -0
- parrot/pipelines/__init__.py +0 -0
- parrot/pipelines/abstract.py +210 -0
- parrot/pipelines/detector.py +124 -0
- parrot/pipelines/models.py +90 -0
- parrot/pipelines/planogram.py +3002 -0
- parrot/pipelines/table.sql +97 -0
- parrot/plugins/__init__.py +106 -0
- parrot/plugins/importer.py +80 -0
- parrot/py.typed +0 -0
- parrot/registry/__init__.py +18 -0
- parrot/registry/registry.py +594 -0
- parrot/scheduler/__init__.py +1189 -0
- parrot/scheduler/models.py +60 -0
- parrot/security/__init__.py +16 -0
- parrot/security/prompt_injection.py +268 -0
- parrot/security/security_events.sql +25 -0
- parrot/services/__init__.py +1 -0
- parrot/services/mcp/__init__.py +8 -0
- parrot/services/mcp/config.py +13 -0
- parrot/services/mcp/server.py +295 -0
- parrot/services/o365_remote_auth.py +235 -0
- parrot/stores/__init__.py +7 -0
- parrot/stores/abstract.py +352 -0
- parrot/stores/arango.py +1090 -0
- parrot/stores/bigquery.py +1377 -0
- parrot/stores/cache.py +106 -0
- parrot/stores/empty.py +10 -0
- parrot/stores/faiss_store.py +1157 -0
- parrot/stores/kb/__init__.py +9 -0
- parrot/stores/kb/abstract.py +68 -0
- parrot/stores/kb/cache.py +165 -0
- parrot/stores/kb/doc.py +325 -0
- parrot/stores/kb/hierarchy.py +346 -0
- parrot/stores/kb/local.py +457 -0
- parrot/stores/kb/prompt.py +28 -0
- parrot/stores/kb/redis.py +659 -0
- parrot/stores/kb/store.py +115 -0
- parrot/stores/kb/user.py +374 -0
- parrot/stores/models.py +59 -0
- parrot/stores/pgvector.py +3 -0
- parrot/stores/postgres.py +2853 -0
- parrot/stores/utils/__init__.py +0 -0
- parrot/stores/utils/chunking.py +197 -0
- parrot/telemetry/__init__.py +3 -0
- parrot/telemetry/mixin.py +111 -0
- parrot/template/__init__.py +3 -0
- parrot/template/engine.py +259 -0
- parrot/tools/__init__.py +23 -0
- parrot/tools/abstract.py +644 -0
- parrot/tools/agent.py +363 -0
- parrot/tools/arangodbsearch.py +537 -0
- parrot/tools/arxiv_tool.py +188 -0
- parrot/tools/calculator/__init__.py +3 -0
- parrot/tools/calculator/operations/__init__.py +38 -0
- parrot/tools/calculator/operations/calculus.py +80 -0
- parrot/tools/calculator/operations/statistics.py +76 -0
- parrot/tools/calculator/tool.py +150 -0
- parrot/tools/cloudwatch.py +988 -0
- parrot/tools/codeinterpreter/__init__.py +127 -0
- parrot/tools/codeinterpreter/executor.py +371 -0
- parrot/tools/codeinterpreter/internals.py +473 -0
- parrot/tools/codeinterpreter/models.py +643 -0
- parrot/tools/codeinterpreter/prompts.py +224 -0
- parrot/tools/codeinterpreter/tool.py +664 -0
- parrot/tools/company_info/__init__.py +6 -0
- parrot/tools/company_info/tool.py +1138 -0
- parrot/tools/correlationanalysis.py +437 -0
- parrot/tools/database/abstract.py +286 -0
- parrot/tools/database/bq.py +115 -0
- parrot/tools/database/cache.py +284 -0
- parrot/tools/database/models.py +95 -0
- parrot/tools/database/pg.py +343 -0
- parrot/tools/databasequery.py +1159 -0
- parrot/tools/db.py +1800 -0
- parrot/tools/ddgo.py +370 -0
- parrot/tools/decorators.py +271 -0
- parrot/tools/dftohtml.py +282 -0
- parrot/tools/document.py +549 -0
- parrot/tools/ecs.py +819 -0
- parrot/tools/edareport.py +368 -0
- parrot/tools/elasticsearch.py +1049 -0
- parrot/tools/employees.py +462 -0
- parrot/tools/epson/__init__.py +96 -0
- parrot/tools/excel.py +683 -0
- parrot/tools/file/__init__.py +13 -0
- parrot/tools/file/abstract.py +76 -0
- parrot/tools/file/gcs.py +378 -0
- parrot/tools/file/local.py +284 -0
- parrot/tools/file/s3.py +511 -0
- parrot/tools/file/tmp.py +309 -0
- parrot/tools/file/tool.py +501 -0
- parrot/tools/file_reader.py +129 -0
- parrot/tools/flowtask/__init__.py +19 -0
- parrot/tools/flowtask/tool.py +761 -0
- parrot/tools/gittoolkit.py +508 -0
- parrot/tools/google/__init__.py +18 -0
- parrot/tools/google/base.py +169 -0
- parrot/tools/google/tools.py +1251 -0
- parrot/tools/googlelocation.py +5 -0
- parrot/tools/googleroutes.py +5 -0
- parrot/tools/googlesearch.py +5 -0
- parrot/tools/googlesitesearch.py +5 -0
- parrot/tools/googlevoice.py +2 -0
- parrot/tools/gvoice.py +695 -0
- parrot/tools/ibisworld/README.md +225 -0
- parrot/tools/ibisworld/__init__.py +11 -0
- parrot/tools/ibisworld/tool.py +366 -0
- parrot/tools/jiratoolkit.py +1718 -0
- parrot/tools/manager.py +1098 -0
- parrot/tools/math.py +152 -0
- parrot/tools/metadata.py +476 -0
- parrot/tools/msteams.py +1621 -0
- parrot/tools/msword.py +635 -0
- parrot/tools/multidb.py +580 -0
- parrot/tools/multistoresearch.py +369 -0
- parrot/tools/networkninja.py +167 -0
- parrot/tools/nextstop/__init__.py +4 -0
- parrot/tools/nextstop/base.py +286 -0
- parrot/tools/nextstop/employee.py +733 -0
- parrot/tools/nextstop/store.py +462 -0
- parrot/tools/notification.py +435 -0
- parrot/tools/o365/__init__.py +42 -0
- parrot/tools/o365/base.py +295 -0
- parrot/tools/o365/bundle.py +522 -0
- parrot/tools/o365/events.py +554 -0
- parrot/tools/o365/mail.py +992 -0
- parrot/tools/o365/onedrive.py +497 -0
- parrot/tools/o365/sharepoint.py +641 -0
- parrot/tools/openapi_toolkit.py +904 -0
- parrot/tools/openweather.py +527 -0
- parrot/tools/pdfprint.py +1001 -0
- parrot/tools/powerbi.py +518 -0
- parrot/tools/powerpoint.py +1113 -0
- parrot/tools/pricestool.py +146 -0
- parrot/tools/products/__init__.py +246 -0
- parrot/tools/prophet_tool.py +171 -0
- parrot/tools/pythonpandas.py +630 -0
- parrot/tools/pythonrepl.py +910 -0
- parrot/tools/qsource.py +436 -0
- parrot/tools/querytoolkit.py +395 -0
- parrot/tools/quickeda.py +827 -0
- parrot/tools/resttool.py +553 -0
- parrot/tools/retail/__init__.py +0 -0
- parrot/tools/retail/bby.py +528 -0
- parrot/tools/sandboxtool.py +703 -0
- parrot/tools/sassie/__init__.py +352 -0
- parrot/tools/scraping/__init__.py +7 -0
- parrot/tools/scraping/docs/select.md +466 -0
- parrot/tools/scraping/documentation.md +1278 -0
- parrot/tools/scraping/driver.py +436 -0
- parrot/tools/scraping/models.py +576 -0
- parrot/tools/scraping/options.py +85 -0
- parrot/tools/scraping/orchestrator.py +517 -0
- parrot/tools/scraping/readme.md +740 -0
- parrot/tools/scraping/tool.py +3115 -0
- parrot/tools/seasonaldetection.py +642 -0
- parrot/tools/shell_tool/__init__.py +5 -0
- parrot/tools/shell_tool/actions.py +408 -0
- parrot/tools/shell_tool/engine.py +155 -0
- parrot/tools/shell_tool/models.py +322 -0
- parrot/tools/shell_tool/tool.py +442 -0
- parrot/tools/site_search.py +214 -0
- parrot/tools/textfile.py +418 -0
- parrot/tools/think.py +378 -0
- parrot/tools/toolkit.py +298 -0
- parrot/tools/webapp_tool.py +187 -0
- parrot/tools/whatif.py +1279 -0
- parrot/tools/workday/MULTI_WSDL_EXAMPLE.md +249 -0
- parrot/tools/workday/__init__.py +6 -0
- parrot/tools/workday/models.py +1389 -0
- parrot/tools/workday/tool.py +1293 -0
- parrot/tools/yfinance_tool.py +306 -0
- parrot/tools/zipcode.py +217 -0
- parrot/utils/__init__.py +2 -0
- parrot/utils/helpers.py +73 -0
- parrot/utils/parsers/__init__.py +5 -0
- parrot/utils/parsers/toml.c +12078 -0
- parrot/utils/parsers/toml.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/utils/parsers/toml.pyx +21 -0
- parrot/utils/toml.py +11 -0
- parrot/utils/types.cpp +20936 -0
- parrot/utils/types.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/utils/types.pyx +213 -0
- parrot/utils/uv.py +11 -0
- parrot/version.py +10 -0
- parrot/yaml-rs/Cargo.lock +350 -0
- parrot/yaml-rs/Cargo.toml +19 -0
- parrot/yaml-rs/pyproject.toml +19 -0
- parrot/yaml-rs/python/yaml_rs/__init__.py +81 -0
- parrot/yaml-rs/src/lib.rs +222 -0
- requirements/docker-compose.yml +24 -0
- 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
|