outhad-integrations 0.32.0

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 (275) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +43 -0
  4. data/.ruby-version +1 -0
  5. data/.vscode/settings.json +5 -0
  6. data/README.md +76 -0
  7. data/Rakefile +12 -0
  8. data/lib/outhad/integrations/config.rb +14 -0
  9. data/lib/outhad/integrations/core/base_connector.rb +79 -0
  10. data/lib/outhad/integrations/core/constants.rb +103 -0
  11. data/lib/outhad/integrations/core/destination_connector.rb +20 -0
  12. data/lib/outhad/integrations/core/fullrefresher.rb +19 -0
  13. data/lib/outhad/integrations/core/http_client.rb +17 -0
  14. data/lib/outhad/integrations/core/http_helper.rb +36 -0
  15. data/lib/outhad/integrations/core/query_builder.rb +33 -0
  16. data/lib/outhad/integrations/core/rate_limiter.rb +19 -0
  17. data/lib/outhad/integrations/core/source_connector.rb +66 -0
  18. data/lib/outhad/integrations/core/streaming_http_client.rb +21 -0
  19. data/lib/outhad/integrations/core/unstructured_source_connector.rb +52 -0
  20. data/lib/outhad/integrations/core/utils.rb +123 -0
  21. data/lib/outhad/integrations/core/vector_source_connector.rb +14 -0
  22. data/lib/outhad/integrations/destination/airtable/client.rb +157 -0
  23. data/lib/outhad/integrations/destination/airtable/config/catalog.json +6 -0
  24. data/lib/outhad/integrations/destination/airtable/config/meta.json +15 -0
  25. data/lib/outhad/integrations/destination/airtable/config/spec.json +23 -0
  26. data/lib/outhad/integrations/destination/airtable/icon.svg +6 -0
  27. data/lib/outhad/integrations/destination/airtable/schema_helper.rb +141 -0
  28. data/lib/outhad/integrations/destination/ais_data_store/client.rb +130 -0
  29. data/lib/outhad/integrations/destination/ais_data_store/config/meta.json +15 -0
  30. data/lib/outhad/integrations/destination/ais_data_store/config/spec.json +68 -0
  31. data/lib/outhad/integrations/destination/ais_data_store/icon.svg +4 -0
  32. data/lib/outhad/integrations/destination/amazon_s3/client.rb +92 -0
  33. data/lib/outhad/integrations/destination/amazon_s3/config/catalog.json +16 -0
  34. data/lib/outhad/integrations/destination/amazon_s3/config/meta.json +15 -0
  35. data/lib/outhad/integrations/destination/amazon_s3/config/spec.json +56 -0
  36. data/lib/outhad/integrations/destination/amazon_s3/icon.svg +34 -0
  37. data/lib/outhad/integrations/destination/databricks_lakehouse/client.rb +147 -0
  38. data/lib/outhad/integrations/destination/databricks_lakehouse/config/meta.json +15 -0
  39. data/lib/outhad/integrations/destination/databricks_lakehouse/config/spec.json +44 -0
  40. data/lib/outhad/integrations/destination/databricks_lakehouse/icon.svg +65 -0
  41. data/lib/outhad/integrations/destination/facebook_custom_audience/client.rb +125 -0
  42. data/lib/outhad/integrations/destination/facebook_custom_audience/config/catalog.json +42 -0
  43. data/lib/outhad/integrations/destination/facebook_custom_audience/config/meta.json +15 -0
  44. data/lib/outhad/integrations/destination/facebook_custom_audience/config/spec.json +28 -0
  45. data/lib/outhad/integrations/destination/facebook_custom_audience/icon.svg +23 -0
  46. data/lib/outhad/integrations/destination/google_sheets/client.rb +240 -0
  47. data/lib/outhad/integrations/destination/google_sheets/config/catalog.json +6 -0
  48. data/lib/outhad/integrations/destination/google_sheets/config/meta.json +15 -0
  49. data/lib/outhad/integrations/destination/google_sheets/config/spec.json +75 -0
  50. data/lib/outhad/integrations/destination/google_sheets/icon.svg +1 -0
  51. data/lib/outhad/integrations/destination/http/client.rb +106 -0
  52. data/lib/outhad/integrations/destination/http/config/catalog.json +16 -0
  53. data/lib/outhad/integrations/destination/http/config/meta.json +15 -0
  54. data/lib/outhad/integrations/destination/http/config/spec.json +24 -0
  55. data/lib/outhad/integrations/destination/http/icon.svg +9 -0
  56. data/lib/outhad/integrations/destination/hubspot/client.rb +122 -0
  57. data/lib/outhad/integrations/destination/hubspot/config/catalog.json +351 -0
  58. data/lib/outhad/integrations/destination/hubspot/config/meta.json +15 -0
  59. data/lib/outhad/integrations/destination/hubspot/config/spec.json +18 -0
  60. data/lib/outhad/integrations/destination/hubspot/icon.svg +5 -0
  61. data/lib/outhad/integrations/destination/iterable/client.rb +111 -0
  62. data/lib/outhad/integrations/destination/iterable/config/catalog.json +47 -0
  63. data/lib/outhad/integrations/destination/iterable/config/meta.json +15 -0
  64. data/lib/outhad/integrations/destination/iterable/config/spec.json +19 -0
  65. data/lib/outhad/integrations/destination/iterable/icon.svg +71 -0
  66. data/lib/outhad/integrations/destination/klaviyo/client.rb +119 -0
  67. data/lib/outhad/integrations/destination/klaviyo/config/catalog.json +103 -0
  68. data/lib/outhad/integrations/destination/klaviyo/config/meta.json +15 -0
  69. data/lib/outhad/integrations/destination/klaviyo/config/spec.json +24 -0
  70. data/lib/outhad/integrations/destination/klaviyo/icon.svg +6 -0
  71. data/lib/outhad/integrations/destination/mailchimp/client.rb +141 -0
  72. data/lib/outhad/integrations/destination/mailchimp/config/catalog.json +142 -0
  73. data/lib/outhad/integrations/destination/mailchimp/config/meta.json +15 -0
  74. data/lib/outhad/integrations/destination/mailchimp/config/spec.json +28 -0
  75. data/lib/outhad/integrations/destination/mailchimp/icon.svg +4 -0
  76. data/lib/outhad/integrations/destination/maria_db/client.rb +114 -0
  77. data/lib/outhad/integrations/destination/maria_db/config/meta.json +15 -0
  78. data/lib/outhad/integrations/destination/maria_db/config/spec.json +48 -0
  79. data/lib/outhad/integrations/destination/maria_db/icon.svg +15 -0
  80. data/lib/outhad/integrations/destination/microsoft_dynamics/client.rb +150 -0
  81. data/lib/outhad/integrations/destination/microsoft_dynamics/config/catalog.json +161 -0
  82. data/lib/outhad/integrations/destination/microsoft_dynamics/config/meta.json +15 -0
  83. data/lib/outhad/integrations/destination/microsoft_dynamics/config/spec.json +35 -0
  84. data/lib/outhad/integrations/destination/microsoft_dynamics/icon.svg +2 -0
  85. data/lib/outhad/integrations/destination/microsoft_excel/client.rb +198 -0
  86. data/lib/outhad/integrations/destination/microsoft_excel/config/catalog.json +7 -0
  87. data/lib/outhad/integrations/destination/microsoft_excel/config/meta.json +15 -0
  88. data/lib/outhad/integrations/destination/microsoft_excel/config/spec.json +19 -0
  89. data/lib/outhad/integrations/destination/microsoft_excel/icon.svg +18 -0
  90. data/lib/outhad/integrations/destination/microsoft_sql/client.rb +137 -0
  91. data/lib/outhad/integrations/destination/microsoft_sql/config/meta.json +15 -0
  92. data/lib/outhad/integrations/destination/microsoft_sql/config/spec.json +68 -0
  93. data/lib/outhad/integrations/destination/microsoft_sql/icon.svg +22 -0
  94. data/lib/outhad/integrations/destination/odoo/client.rb +109 -0
  95. data/lib/outhad/integrations/destination/odoo/config/meta.json +15 -0
  96. data/lib/outhad/integrations/destination/odoo/config/spec.json +39 -0
  97. data/lib/outhad/integrations/destination/odoo/icon.svg +21 -0
  98. data/lib/outhad/integrations/destination/oracle_db/client.rb +112 -0
  99. data/lib/outhad/integrations/destination/oracle_db/config/meta.json +15 -0
  100. data/lib/outhad/integrations/destination/oracle_db/config/spec.json +47 -0
  101. data/lib/outhad/integrations/destination/oracle_db/icon.svg +4 -0
  102. data/lib/outhad/integrations/destination/pinecone_db/client.rb +154 -0
  103. data/lib/outhad/integrations/destination/pinecone_db/config/meta.json +15 -0
  104. data/lib/outhad/integrations/destination/pinecone_db/config/spec.json +32 -0
  105. data/lib/outhad/integrations/destination/pinecone_db/icon.svg +1 -0
  106. data/lib/outhad/integrations/destination/postgresql/client.rb +130 -0
  107. data/lib/outhad/integrations/destination/postgresql/config/meta.json +15 -0
  108. data/lib/outhad/integrations/destination/postgresql/config/spec.json +68 -0
  109. data/lib/outhad/integrations/destination/postgresql/icon.svg +20 -0
  110. data/lib/outhad/integrations/destination/qdrant/client.rb +184 -0
  111. data/lib/outhad/integrations/destination/qdrant/config/meta.json +15 -0
  112. data/lib/outhad/integrations/destination/qdrant/config/spec.json +23 -0
  113. data/lib/outhad/integrations/destination/qdrant/icon.svg +1 -0
  114. data/lib/outhad/integrations/destination/salesforce_consumer_goods_cloud/client.rb +136 -0
  115. data/lib/outhad/integrations/destination/salesforce_consumer_goods_cloud/config/catalog.json +6 -0
  116. data/lib/outhad/integrations/destination/salesforce_consumer_goods_cloud/config/meta.json +16 -0
  117. data/lib/outhad/integrations/destination/salesforce_consumer_goods_cloud/config/spec.json +52 -0
  118. data/lib/outhad/integrations/destination/salesforce_consumer_goods_cloud/icon.svg +16 -0
  119. data/lib/outhad/integrations/destination/salesforce_consumer_goods_cloud/schema_helper.rb +132 -0
  120. data/lib/outhad/integrations/destination/salesforce_crm/client.rb +114 -0
  121. data/lib/outhad/integrations/destination/salesforce_crm/config/catalog.json +320 -0
  122. data/lib/outhad/integrations/destination/salesforce_crm/config/meta.json +15 -0
  123. data/lib/outhad/integrations/destination/salesforce_crm/config/spec.json +46 -0
  124. data/lib/outhad/integrations/destination/salesforce_crm/icon.svg +16 -0
  125. data/lib/outhad/integrations/destination/sftp/client.rb +186 -0
  126. data/lib/outhad/integrations/destination/sftp/config/catalog.json +16 -0
  127. data/lib/outhad/integrations/destination/sftp/config/meta.json +16 -0
  128. data/lib/outhad/integrations/destination/sftp/config/spec.json +73 -0
  129. data/lib/outhad/integrations/destination/sftp/icon.svg +1 -0
  130. data/lib/outhad/integrations/destination/slack/client.rb +125 -0
  131. data/lib/outhad/integrations/destination/slack/config/catalog.json +22 -0
  132. data/lib/outhad/integrations/destination/slack/config/meta.json +15 -0
  133. data/lib/outhad/integrations/destination/slack/config/spec.json +23 -0
  134. data/lib/outhad/integrations/destination/slack/icon.svg +26 -0
  135. data/lib/outhad/integrations/destination/stripe/client.rb +94 -0
  136. data/lib/outhad/integrations/destination/stripe/config/catalog.json +128 -0
  137. data/lib/outhad/integrations/destination/stripe/config/meta.json +15 -0
  138. data/lib/outhad/integrations/destination/stripe/config/spec.json +18 -0
  139. data/lib/outhad/integrations/destination/stripe/icon.svg +10 -0
  140. data/lib/outhad/integrations/destination/zendesk/client.rb +132 -0
  141. data/lib/outhad/integrations/destination/zendesk/config/catalog.json +110 -0
  142. data/lib/outhad/integrations/destination/zendesk/config/meta.json +18 -0
  143. data/lib/outhad/integrations/destination/zendesk/config/spec.json +32 -0
  144. data/lib/outhad/integrations/destination/zendesk/icon.svg +63 -0
  145. data/lib/outhad/integrations/protocol/protocol.json +189 -0
  146. data/lib/outhad/integrations/protocol/protocol.rb +228 -0
  147. data/lib/outhad/integrations/rollout.rb +66 -0
  148. data/lib/outhad/integrations/service.rb +55 -0
  149. data/lib/outhad/integrations/source/amazon_s3/client.rb +235 -0
  150. data/lib/outhad/integrations/source/amazon_s3/config/meta.json +16 -0
  151. data/lib/outhad/integrations/source/amazon_s3/config/spec.json +119 -0
  152. data/lib/outhad/integrations/source/amazon_s3/icon.svg +34 -0
  153. data/lib/outhad/integrations/source/anthropic/client.rb +135 -0
  154. data/lib/outhad/integrations/source/anthropic/config/catalog.json +6 -0
  155. data/lib/outhad/integrations/source/anthropic/config/meta.json +16 -0
  156. data/lib/outhad/integrations/source/anthropic/config/spec.json +56 -0
  157. data/lib/outhad/integrations/source/anthropic/icon.svg +1 -0
  158. data/lib/outhad/integrations/source/aws_athena/client.rb +109 -0
  159. data/lib/outhad/integrations/source/aws_athena/config/meta.json +16 -0
  160. data/lib/outhad/integrations/source/aws_athena/config/spec.json +63 -0
  161. data/lib/outhad/integrations/source/aws_athena/icon.svg +22 -0
  162. data/lib/outhad/integrations/source/aws_bedrock_model/client.rb +91 -0
  163. data/lib/outhad/integrations/source/aws_bedrock_model/config/catalog.json +6 -0
  164. data/lib/outhad/integrations/source/aws_bedrock_model/config/meta.json +16 -0
  165. data/lib/outhad/integrations/source/aws_bedrock_model/config/spec.json +58 -0
  166. data/lib/outhad/integrations/source/aws_bedrock_model/icon.svg +1 -0
  167. data/lib/outhad/integrations/source/aws_sagemaker_model/client.rb +79 -0
  168. data/lib/outhad/integrations/source/aws_sagemaker_model/config/catalog.json +6 -0
  169. data/lib/outhad/integrations/source/aws_sagemaker_model/config/meta.json +16 -0
  170. data/lib/outhad/integrations/source/aws_sagemaker_model/config/spec.json +52 -0
  171. data/lib/outhad/integrations/source/aws_sagemaker_model/icon.svg +7 -0
  172. data/lib/outhad/integrations/source/bigquery/client.rb +98 -0
  173. data/lib/outhad/integrations/source/bigquery/config/meta.json +16 -0
  174. data/lib/outhad/integrations/source/bigquery/config/spec.json +83 -0
  175. data/lib/outhad/integrations/source/bigquery/icon.svg +1 -0
  176. data/lib/outhad/integrations/source/clickhouse/client.rb +102 -0
  177. data/lib/outhad/integrations/source/clickhouse/config/meta.json +16 -0
  178. data/lib/outhad/integrations/source/clickhouse/config/spec.json +42 -0
  179. data/lib/outhad/integrations/source/clickhouse/icon.svg +25 -0
  180. data/lib/outhad/integrations/source/databricks/client.rb +98 -0
  181. data/lib/outhad/integrations/source/databricks/config/meta.json +17 -0
  182. data/lib/outhad/integrations/source/databricks/config/spec.json +56 -0
  183. data/lib/outhad/integrations/source/databricks/icon.svg +19 -0
  184. data/lib/outhad/integrations/source/databrics_model/client.rb +89 -0
  185. data/lib/outhad/integrations/source/databrics_model/config/catalog.json +6 -0
  186. data/lib/outhad/integrations/source/databrics_model/config/meta.json +17 -0
  187. data/lib/outhad/integrations/source/databrics_model/config/spec.json +63 -0
  188. data/lib/outhad/integrations/source/databrics_model/icon.svg +19 -0
  189. data/lib/outhad/integrations/source/firecrawl/client.rb +151 -0
  190. data/lib/outhad/integrations/source/firecrawl/config/catalog.json +29 -0
  191. data/lib/outhad/integrations/source/firecrawl/config/meta.json +17 -0
  192. data/lib/outhad/integrations/source/firecrawl/config/spec.json +31 -0
  193. data/lib/outhad/integrations/source/firecrawl/icon.svg +4 -0
  194. data/lib/outhad/integrations/source/generic_open_ai/client.rb +118 -0
  195. data/lib/outhad/integrations/source/generic_open_ai/config/catalog.json +6 -0
  196. data/lib/outhad/integrations/source/generic_open_ai/config/meta.json +16 -0
  197. data/lib/outhad/integrations/source/generic_open_ai/config/spec.json +63 -0
  198. data/lib/outhad/integrations/source/generic_open_ai/icon.svg +6 -0
  199. data/lib/outhad/integrations/source/google_vertex_model/client.rb +83 -0
  200. data/lib/outhad/integrations/source/google_vertex_model/config/catalog.json +6 -0
  201. data/lib/outhad/integrations/source/google_vertex_model/config/meta.json +17 -0
  202. data/lib/outhad/integrations/source/google_vertex_model/config/spec.json +105 -0
  203. data/lib/outhad/integrations/source/google_vertex_model/icon.svg +2 -0
  204. data/lib/outhad/integrations/source/http_model/client.rb +108 -0
  205. data/lib/outhad/integrations/source/http_model/config/catalog.json +6 -0
  206. data/lib/outhad/integrations/source/http_model/config/meta.json +16 -0
  207. data/lib/outhad/integrations/source/http_model/config/spec.json +70 -0
  208. data/lib/outhad/integrations/source/http_model/icon.svg +9 -0
  209. data/lib/outhad/integrations/source/intuit_quick_books/client.rb +213 -0
  210. data/lib/outhad/integrations/source/intuit_quick_books/config/catalog.json +6 -0
  211. data/lib/outhad/integrations/source/intuit_quick_books/config/meta.json +17 -0
  212. data/lib/outhad/integrations/source/intuit_quick_books/config/spec.json +44 -0
  213. data/lib/outhad/integrations/source/intuit_quick_books/icon.svg +1 -0
  214. data/lib/outhad/integrations/source/maria_db/client.rb +92 -0
  215. data/lib/outhad/integrations/source/maria_db/config/meta.json +16 -0
  216. data/lib/outhad/integrations/source/maria_db/config/spec.json +48 -0
  217. data/lib/outhad/integrations/source/maria_db/icon.svg +15 -0
  218. data/lib/outhad/integrations/source/odoo/client.rb +106 -0
  219. data/lib/outhad/integrations/source/odoo/config/meta.json +15 -0
  220. data/lib/outhad/integrations/source/odoo/config/spec.json +39 -0
  221. data/lib/outhad/integrations/source/odoo/icon.svg +21 -0
  222. data/lib/outhad/integrations/source/open_ai/client.rb +118 -0
  223. data/lib/outhad/integrations/source/open_ai/config/catalog.json +6 -0
  224. data/lib/outhad/integrations/source/open_ai/config/meta.json +16 -0
  225. data/lib/outhad/integrations/source/open_ai/config/spec.json +56 -0
  226. data/lib/outhad/integrations/source/open_ai/icon.svg +1 -0
  227. data/lib/outhad/integrations/source/oracle_db/client.rb +127 -0
  228. data/lib/outhad/integrations/source/oracle_db/config/meta.json +16 -0
  229. data/lib/outhad/integrations/source/oracle_db/config/spec.json +47 -0
  230. data/lib/outhad/integrations/source/oracle_db/icon.svg +4 -0
  231. data/lib/outhad/integrations/source/pinecone_db/client.rb +73 -0
  232. data/lib/outhad/integrations/source/pinecone_db/config/catalog.json +6 -0
  233. data/lib/outhad/integrations/source/pinecone_db/config/meta.json +16 -0
  234. data/lib/outhad/integrations/source/pinecone_db/config/spec.json +34 -0
  235. data/lib/outhad/integrations/source/pinecone_db/icon.svg +2 -0
  236. data/lib/outhad/integrations/source/postgresql/client.rb +112 -0
  237. data/lib/outhad/integrations/source/postgresql/config/meta.json +16 -0
  238. data/lib/outhad/integrations/source/postgresql/config/spec.json +86 -0
  239. data/lib/outhad/integrations/source/postgresql/icon.svg +20 -0
  240. data/lib/outhad/integrations/source/qdrant/client.rb +86 -0
  241. data/lib/outhad/integrations/source/qdrant/config/catalog.json +6 -0
  242. data/lib/outhad/integrations/source/qdrant/config/meta.json +16 -0
  243. data/lib/outhad/integrations/source/qdrant/config/spec.json +29 -0
  244. data/lib/outhad/integrations/source/qdrant/icon.svg +1 -0
  245. data/lib/outhad/integrations/source/redshift/client.rb +109 -0
  246. data/lib/outhad/integrations/source/redshift/config/meta.json +16 -0
  247. data/lib/outhad/integrations/source/redshift/config/spec.json +71 -0
  248. data/lib/outhad/integrations/source/redshift/icon.svg +15 -0
  249. data/lib/outhad/integrations/source/salesforce_consumer_goods_cloud/client.rb +133 -0
  250. data/lib/outhad/integrations/source/salesforce_consumer_goods_cloud/config/catalog.json +6 -0
  251. data/lib/outhad/integrations/source/salesforce_consumer_goods_cloud/config/meta.json +18 -0
  252. data/lib/outhad/integrations/source/salesforce_consumer_goods_cloud/config/spec.json +53 -0
  253. data/lib/outhad/integrations/source/salesforce_consumer_goods_cloud/icon.svg +16 -0
  254. data/lib/outhad/integrations/source/salesforce_consumer_goods_cloud/schema_helper.rb +130 -0
  255. data/lib/outhad/integrations/source/sftp/client.rb +133 -0
  256. data/lib/outhad/integrations/source/sftp/config/meta.json +16 -0
  257. data/lib/outhad/integrations/source/sftp/config/spec.json +59 -0
  258. data/lib/outhad/integrations/source/sftp/icon.svg +1 -0
  259. data/lib/outhad/integrations/source/snowflake/client.rb +92 -0
  260. data/lib/outhad/integrations/source/snowflake/config/meta.json +16 -0
  261. data/lib/outhad/integrations/source/snowflake/config/spec.json +82 -0
  262. data/lib/outhad/integrations/source/snowflake/icon.svg +10 -0
  263. data/lib/outhad/integrations/source/watsonx_ai/client.rb +194 -0
  264. data/lib/outhad/integrations/source/watsonx_ai/config/catalog.json +6 -0
  265. data/lib/outhad/integrations/source/watsonx_ai/config/meta.json +16 -0
  266. data/lib/outhad/integrations/source/watsonx_ai/config/spec.json +74 -0
  267. data/lib/outhad/integrations/source/watsonx_ai/icon.svg +1 -0
  268. data/lib/outhad/integrations/source/watsonx_data/client.rb +146 -0
  269. data/lib/outhad/integrations/source/watsonx_data/config/meta.json +17 -0
  270. data/lib/outhad/integrations/source/watsonx_data/config/spec.json +72 -0
  271. data/lib/outhad/integrations/source/watsonx_data/icon.svg +1 -0
  272. data/lib/outhad/integrations.rb +129 -0
  273. data/outhad-integrations.gemspec +79 -0
  274. data/sig/outhad/integrations.rbs +6 -0
  275. metadata +866 -0
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Outhad::Integrations::Source
4
+ module HttpModel
5
+ include Outhad::Integrations::Core
6
+ class Client < SourceConnector
7
+ def check_connection(connection_config)
8
+ connection_config = prepare_config(connection_config)
9
+ response = send_request(
10
+ url: connection_config[:url_host],
11
+ http_method: connection_config[:http_method],
12
+ payload: JSON.parse(connection_config[:request_format]),
13
+ headers: connection_config[:headers],
14
+ config: connection_config[:config]
15
+ )
16
+ success?(response) ? success_status : failure_status(nil)
17
+ rescue StandardError => e
18
+ handle_exception(e, { context: "HTTP MODEL:CHECK_CONNECTION:EXCEPTION", type: "error" })
19
+ failure_status(e)
20
+ end
21
+
22
+ def discover(_connection_config = nil)
23
+ catalog_json = read_json(CATALOG_SPEC_PATH)
24
+ catalog = build_catalog(catalog_json)
25
+ catalog.to_outhad_message
26
+ rescue StandardError => e
27
+ handle_exception(e, { context: "HTTP MODEL:DISCOVER:EXCEPTION", type: "error" })
28
+ end
29
+
30
+ def read(sync_config)
31
+ connection_config = prepare_config(sync_config.source.connection_specification)
32
+ stream = connection_config[:is_stream] ||= false
33
+ # The server checks the ConnectorQueryType.
34
+ # If it's "ai_ml," the server calculates the payload and passes it as a query in the sync config model protocol.
35
+ # This query is then sent to the AI/ML model.
36
+ payload = parse_json(sync_config.model.query)
37
+
38
+ if stream
39
+ run_model_stream(connection_config, payload) { |message| yield message if block_given? }
40
+ else
41
+ run_model(connection_config, payload)
42
+ end
43
+ rescue StandardError => e
44
+ handle_exception(e, { context: "HTTP MODEL:READ:EXCEPTION", type: "error" })
45
+ end
46
+
47
+ private
48
+
49
+ def prepare_config(config)
50
+ config.with_indifferent_access.tap do |conf|
51
+ conf[:config][:timeout] ||= 30
52
+ end
53
+ end
54
+
55
+ def parse_json(json_string)
56
+ JSON.parse(json_string)
57
+ rescue JSON::ParserError => e
58
+ handle_exception(e, { context: "HTTP MODEL:PARSE_JSON:EXCEPTION", type: "error" })
59
+ {}
60
+ end
61
+
62
+ def run_model(connection_config, payload)
63
+ response = send_request(
64
+ url: connection_config[:url_host],
65
+ http_method: connection_config[:http_method],
66
+ payload: payload,
67
+ headers: connection_config[:headers],
68
+ config: connection_config[:config]
69
+ )
70
+ process_response(response)
71
+ rescue StandardError => e
72
+ handle_exception(e, { context: "HTTP MODEL:RUN_MODEL:EXCEPTION", type: "error" })
73
+ end
74
+
75
+ def run_model_stream(connection_config, payload)
76
+ send_streaming_request(
77
+ url: connection_config[:url_host],
78
+ http_method: connection_config[:http_method],
79
+ payload: payload,
80
+ headers: connection_config[:headers],
81
+ config: connection_config[:config]
82
+ ) do |chunk|
83
+ process_streaming_response(chunk) { |message| yield message if block_given? }
84
+ end
85
+ rescue StandardError => e
86
+ handle_exception(e, { context: "HTTP MODEL:RUN_STREAM_MODEL:EXCEPTION", type: "error" })
87
+ end
88
+
89
+ def process_response(response)
90
+ if success?(response)
91
+ data = JSON.parse(response.body)
92
+ [RecordMessage.new(data: data, emitted_at: Time.now.to_i).to_outhad_message]
93
+ else
94
+ create_log_message("HTTP MODEL:RUN_MODEL", "error", "request failed: #{response.body}")
95
+ end
96
+ rescue StandardError => e
97
+ handle_exception(e, { context: "HTTP MODEL:PROCESS_RESPONSE:EXCEPTION", type: "error" })
98
+ end
99
+
100
+ def process_streaming_response(chunk)
101
+ data = JSON.parse(chunk)
102
+ yield [RecordMessage.new(data: data, emitted_at: Time.now.to_i).to_outhad_message] if block_given?
103
+ rescue StandardError => e
104
+ handle_exception(e, { context: "HTTP MODEL:PROCESS_STREAMING_RESPONSE:EXCEPTION", type: "error" })
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,6 @@
1
+ {
2
+ "request_rate_limit": 600,
3
+ "request_rate_limit_unit": "minute",
4
+ "request_rate_concurrency": 10,
5
+ "streams": []
6
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "data": {
3
+ "name": "HttpModel",
4
+ "title": "HTTP Model Endpoint",
5
+ "connector_type": "source",
6
+ "category": "AI Model",
7
+ "sub_category": "AI_ML Service",
8
+ "documentation_url": "https://docs.squared.ai/activation/ai-ml-sources/http-model-endpoint",
9
+ "github_issue_label": "source-http-model",
10
+ "icon": "icon.svg",
11
+ "license": "MIT",
12
+ "release_stage": "alpha",
13
+ "support_level": "community",
14
+ "tags": ["language:ruby", "outhad"]
15
+ }
16
+ }
@@ -0,0 +1,70 @@
1
+ {
2
+ "documentation_url": "https://docs.squared.ai/activation/ai-ml-sources/http-model-endpoint",
3
+ "stream_type": "user_defined",
4
+ "connector_query_type": "ai_ml",
5
+ "connection_specification": {
6
+ "$schema": "http://json-schema.org/draft-07/schema#",
7
+ "title": "HTTP Model Endpoint",
8
+ "type": "object",
9
+ "required": ["url_host", "http_method"],
10
+ "properties": {
11
+ "http_method": {
12
+ "type": "string",
13
+ "title": "HTTP Method",
14
+ "enum": ["POST", "GET"],
15
+ "order": 0
16
+ },
17
+ "url_host": {
18
+ "type": "string",
19
+ "title": "URL",
20
+ "order": 1
21
+ },
22
+ "is_stream": {
23
+ "type": "boolean",
24
+ "title": "Streaming Enabled",
25
+ "description": "Enables data streaming for such as chat, when supported by the model. When true, messages and model data are processed in chunks for immediate delivery, enhancing responsiveness. Default is false, processing only after the entire response is received.",
26
+ "default": false,
27
+ "order": 2
28
+ },
29
+ "headers": {
30
+ "title": "HTTP Headers",
31
+ "description": "Custom headers to include in the HTTP request. Useful for authentication, content type specifications, and other request metadata.",
32
+ "order": 3,
33
+ "additionalProperties": {
34
+ "type": "string"
35
+ },
36
+ "default": {
37
+ "Content-Type": "application/json"
38
+ }
39
+ },
40
+ "config": {
41
+ "title": "",
42
+ "type": "object",
43
+ "properties": {
44
+ "timeout": {
45
+ "type": "string",
46
+ "default": "30",
47
+ "title": "HTTP Timeout",
48
+ "description": "The maximum time, in seconds, to wait for a response from the server before the request is canceled.",
49
+ "order": 0
50
+ }
51
+ },
52
+ "order": 4
53
+ },
54
+ "request_format": {
55
+ "title": "Request Format",
56
+ "description": "Sample Request Format",
57
+ "type": "string",
58
+ "x-request-format": true,
59
+ "order": 5
60
+ },
61
+ "response_format": {
62
+ "title": "Response Format",
63
+ "description": "Sample Response Format",
64
+ "type": "string",
65
+ "x-response-format": true,
66
+ "order": 6
67
+ }
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+
3
+ <svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
4
+
5
+
6
+
7
+
8
+
9
+
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Outhad::Integrations::Source
4
+ module IntuitQuickBooks
5
+ include Outhad::Integrations::Core
6
+
7
+ QUICKBOOKS_OBJECTS = %w[Account Customer Employee Invoice TimeActivity].freeze
8
+ MAX_PER_PAGE = 1000
9
+
10
+ class Client < SourceConnector
11
+ def check_connection(connection_config)
12
+ connection_config = connection_config.with_indifferent_access
13
+ access_token = create_connection(connection_config)
14
+ query = "SELECT * FROM Customer STARTPOSITION 1 MAXRESULTS 1"
15
+ response = query_quickbooks(access_token, query)
16
+ if success?(response)
17
+ success_status
18
+ else
19
+ failure_status(nil)
20
+ end
21
+ rescue StandardError => e
22
+ handle_exception(e, { context: "INTUIT_QUICKBOOKS:CHECK_CONNECTION:EXCEPTION", type: "error" })
23
+ failure_status(e)
24
+ end
25
+
26
+ def discover(connection_config)
27
+ connection_config = connection_config.with_indifferent_access
28
+ access_token = create_connection(connection_config)
29
+ catalog = build_catalog(load_catalog.with_indifferent_access)
30
+ streams = catalog[:streams]
31
+ QUICKBOOKS_OBJECTS.each do |object|
32
+ query = "SELECT * FROM #{object}"
33
+ response = query_quickbooks(access_token, query)
34
+ streams << create_streams(JSON.parse(response.body)["QueryResponse"])[0]
35
+ rescue StandardError => e
36
+ handle_exception(e, { context: "INTUIT_QUICKBOOKS:DISCOVER:LOOP_EXCEPTION", type: "error" })
37
+ next
38
+ end
39
+ catalog.to_outhad_message
40
+ rescue StandardError => e
41
+ handle_exception(e, { context: "INTUIT_QUICKBOOKS:DISCOVER:EXCEPTION", type: "error" })
42
+ end
43
+
44
+ def read(sync_config)
45
+ connection_config = sync_config.source.connection_specification
46
+ connection_config = connection_config.with_indifferent_access
47
+ @connector_instance = sync_config&.source&.connector_instance
48
+ query = sync_config.model.query
49
+ query = batched_query(query, sync_config.limit, sync_config.offset) unless sync_config.limit.nil? && sync_config.offset.nil?
50
+ access_token = create_connection(connection_config)
51
+ query(access_token, query)
52
+ rescue StandardError => e
53
+ handle_exception(e, {
54
+ context: "INTUIT_QUICKBOOKS:READ:EXCEPTION",
55
+ type: "error",
56
+ sync_id: sync_config.sync_id,
57
+ sync_run_id: sync_config.sync_run_id
58
+ })
59
+ end
60
+
61
+ private
62
+
63
+ def query(access_token, query)
64
+ parsed = batched_query_for_quickbooks(query)
65
+ base_query = parsed[:base_query]
66
+ limit = parsed[:limit]
67
+ offset = parsed[:offset]
68
+ execute_query(access_token, base_query, limit, offset).map do |r|
69
+ flat_data = flatten_hash(r)
70
+ RecordMessage.new(data: flat_data, emitted_at: Time.now.to_i).to_outhad_message
71
+ end
72
+ end
73
+
74
+ def execute_query(access_token, base_query, limit, offset)
75
+ total_fetched = 0
76
+ current_offset = offset
77
+ result = []
78
+
79
+ while total_fetched < limit
80
+ batch_limit = [MAX_PER_PAGE, limit - total_fetched].min
81
+ paginated_query = "#{base_query} STARTPOSITION #{current_offset + 1} MAXRESULTS #{batch_limit}"
82
+
83
+ response = query_quickbooks(access_token, paginated_query)
84
+ records = JSON.parse(response.body)["QueryResponse"] || {}
85
+
86
+ break if records.empty?
87
+
88
+ records.each_value do |rows|
89
+ next unless rows.is_a?(Array)
90
+
91
+ rows.each do |row|
92
+ result << row
93
+ end
94
+ end
95
+
96
+ fetched_count = result.size - total_fetched
97
+ break if fetched_count < batch_limit
98
+
99
+ total_fetched += fetched_count
100
+ current_offset += fetched_count
101
+ end
102
+ result
103
+ end
104
+
105
+ def query_quickbooks(access_token, query)
106
+ encoded_query = URI.encode_www_form_component(query)
107
+ query_url = @environment == "sandbox" ? QUICKBOOKS_SANDBOX_QUERY_URL : QUICKBOOKS_PRODUCTION_QUERY_URL
108
+ send_request(
109
+ url: build_url(query_url, encoded_query),
110
+ http_method: HTTP_GET,
111
+ payload: {},
112
+ headers: auth_headers(access_token),
113
+ config: {}
114
+ )
115
+ end
116
+
117
+ def create_connection(connection_config)
118
+ load_connection_config(connection_config)
119
+ refresh_access_token
120
+ end
121
+
122
+ def load_connection_config(connection_config)
123
+ @client_id = connection_config[:client_id]
124
+ @client_secret = connection_config[:client_secret]
125
+ @realm_id = connection_config[:realm_id]
126
+ @environment = connection_config[:environment]
127
+ @refresh_token = if @connector_instance&.configuration
128
+ @connector_instance.configuration["refresh_token"]
129
+ else
130
+ connection_config[:refresh_token]
131
+ end
132
+ end
133
+
134
+ def refresh_access_token
135
+ oauth2_client = IntuitOAuth::Client.new(@client_id, @client_secret, QUICKBOOKS_REDIRECT_URL, @environment)
136
+ response = oauth2_client.token.refresh_tokens(@refresh_token)
137
+ if @connector_instance&.configuration
138
+ config = @connector_instance.configuration
139
+ config = {} unless config.is_a?(Hash)
140
+ new_config = config.merge("refresh_token" => response.refresh_token)
141
+ @connector_instance.update!(configuration: new_config)
142
+ end
143
+ response.access_token
144
+ end
145
+
146
+ def create_streams(records)
147
+ group_by_table(records).map do |r|
148
+ Outhad::Integrations::Protocol::Stream.new(name: r[:table_name], action: StreamAction["fetch"], json_schema: convert_to_json_schema(r[:columns]))
149
+ end
150
+ end
151
+
152
+ def group_by_table(records)
153
+ records.filter_map do |table_name, rows|
154
+ if rows.is_a?(Array) && rows.all? { |row| row.is_a?(Hash) }
155
+ row_sample = rows.first || {}
156
+ columns = row_sample.map do |key, value|
157
+ {
158
+ column_name: key,
159
+ data_type: normalize_type(value),
160
+ is_nullable: rows.any? { |row| row[key].nil? }
161
+ }
162
+ end
163
+ { table_name: table_name, columns: columns }
164
+ end
165
+ end
166
+ end
167
+
168
+ def batched_query_for_quickbooks(query)
169
+ query = query.strip.chomp(";")
170
+ limit = query[/LIMIT\s+(\d+)/i, 1] || 1000
171
+ offset = query[/OFFSET\s+(\d+)/i, 1]
172
+
173
+ base_query = query.gsub(/LIMIT\s+\d+/i, "").gsub(/OFFSET\s+\d+/i, "").strip
174
+ {
175
+ base_query: base_query,
176
+ limit: limit.to_i,
177
+ offset: offset.to_i
178
+ }
179
+ end
180
+
181
+ def flatten_hash(hash, parent_key = "", result = {})
182
+ hash.each do |key, value|
183
+ full_key = parent_key.empty? ? key.to_s : "#{parent_key}.#{key}"
184
+
185
+ case value
186
+ when Hash
187
+ flatten_hash(value, full_key, result)
188
+ when Array
189
+ next
190
+ else
191
+ result[full_key] = value.is_a?(Integer) || value.is_a?(Float) ? value : value.to_s
192
+ end
193
+ end
194
+ result
195
+ end
196
+
197
+ def normalize_type(value)
198
+ case value
199
+ when Integer, Float then "NUMBER"
200
+ else "string"
201
+ end
202
+ end
203
+
204
+ def load_catalog
205
+ read_json(CATALOG_SPEC_PATH)
206
+ end
207
+
208
+ def build_url(url, query)
209
+ format(url, realm_id: @realm_id, query: query)
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,6 @@
1
+ {
2
+ "request_rate_limit": 500,
3
+ "request_rate_limit_unit": "minute",
4
+ "request_rate_concurrency": 10,
5
+ "streams": []
6
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "data": {
3
+ "name": "IntuitQuickBooks",
4
+ "title": "Intuit QuickBooks",
5
+ "connector_type": "source",
6
+ "category": "Data Warehouse",
7
+ "sub_category": "Relational Database",
8
+ "documentation_url": "https://docs.squared.ai/guides/sources/data-sources/intuit_quickbooks",
9
+ "github_issue_label": "source-intuit-quickbooks",
10
+ "icon": "icon.svg",
11
+ "license": "MIT",
12
+ "release_stage": "alpha",
13
+ "support_level": "community",
14
+ "tags": ["language:ruby", "outhad"]
15
+ }
16
+ }
17
+
@@ -0,0 +1,44 @@
1
+ {
2
+ "documentation_url": "https://docs.squared.ai/guides/sources/data-sources/intuit_quickbooks",
3
+ "stream_type": "dynamic",
4
+ "connector_query_type": "raw_sql",
5
+ "connection_specification": {
6
+ "$schema": "http://json-schema.org/draft-07/schema#",
7
+ "title": "Intuit QuickBooks",
8
+ "type": "object",
9
+ "required": ["environment","client_id","client_secret","realm_id","refresh_token"],
10
+ "properties": {
11
+ "environment": {
12
+ "type": "string",
13
+ "title": "Environment",
14
+ "enum": ["sandbox", "production"],
15
+ "order": 0
16
+ },
17
+ "client_id": {
18
+ "type": "string",
19
+ "outhad_secret": true,
20
+ "title": "Client Id",
21
+ "order": 1
22
+ },
23
+ "client_secret": {
24
+ "type": "string",
25
+ "outhad_secret": true,
26
+ "title": "Client Secret",
27
+ "order": 2
28
+ },
29
+ "realm_id": {
30
+ "type": "string",
31
+ "outhad_secret": true,
32
+ "title": "Realm Id",
33
+ "order": 3
34
+ },
35
+ "refresh_token": {
36
+ "type": "string",
37
+ "outhad_secret": true,
38
+ "title": "Refresh Token",
39
+ "order": 4
40
+ }
41
+ }
42
+ }
43
+ }
44
+
@@ -0,0 +1 @@
1
+ <svg enable-background="new 0 0 2500 2500" viewBox="0 0 2500 2500" xmlns="http://www.w3.org/2000/svg"><circle cx="1250" cy="1250" fill="#2ca01c" r="1250"/><path d="m301.3 1249.6c.1 282.6 228 512.4 510.6 514.9h72.3v-188.9h-72.3c-175.2 47.8-355.9-55.5-403.6-230.7-.4-1.4-.7-2.8-1.1-4.2-49.1-177.5 53.7-361.4 230.6-412.5h36.1c45.3-9.9 92.2-9.9 137.5 0h175.6v1002.9c-.9 106.1 84.4 192.9 190.5 193.9v-1395.4h-364.5c-284.6 1.5-514 233.4-512.5 518v.1zm1387.5-519.8h-72.3v198.9h72.3c174.8-47.7 355.1 55.3 402.8 230 .4 1.3.7 2.7 1.1 4 48.8 176.9-53.7 360.1-229.9 411.1h-36.1c-45.3 9.9-92.2 9.9-137.5 0h-175.6v-1002.8c.9-106.1-84.4-192.9-190.5-193.9v1397.4h364.5c287.1-4.5 516.2-240.8 511.8-527.9-4.4-280.8-230.9-507.4-511.8-511.8z" fill="#fff"/></svg>
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Outhad::Integrations::Source
4
+ module MariaDB
5
+ include Outhad::Integrations::Core
6
+ class Client < SourceConnector
7
+ def check_connection(connection_config)
8
+ connection_config = connection_config.with_indifferent_access
9
+ create_connection(connection_config)
10
+ ConnectionStatus.new(status: ConnectionStatusType["succeeded"]).to_outhad_message
11
+ rescue StandardError => e
12
+ ConnectionStatus.new(status: ConnectionStatusType["failed"], message: e.message).to_outhad_message
13
+ end
14
+
15
+ def discover(connection_config)
16
+ connection_config = connection_config.with_indifferent_access
17
+ query = "SELECT table_name, column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema = '#{connection_config[:database]}' ORDER BY table_name, ordinal_position;"
18
+ db = create_connection(connection_config)
19
+ results = query_execution(db, query)
20
+ catalog = Catalog.new(streams: create_streams(results))
21
+ catalog.to_outhad_message
22
+ rescue StandardError => e
23
+ handle_exception(e, {
24
+ context: "MARIA:DB:DISCOVER:EXCEPTION",
25
+ type: "error"
26
+ })
27
+ end
28
+
29
+ def read(sync_config)
30
+ connection_config = sync_config.source.connection_specification.with_indifferent_access
31
+ query = sync_config.model.query
32
+ query = batched_query(query, sync_config.limit, sync_config.offset) unless sync_config.limit.nil? && sync_config.offset.nil?
33
+ db = create_connection(connection_config)
34
+ query(db, query)
35
+ rescue StandardError => e
36
+ handle_exception(e, {
37
+ context: "MARIA:DB:READ:EXCEPTION",
38
+ type: "error",
39
+ sync_id: sync_config.sync_id,
40
+ sync_run_id: sync_config.sync_run_id
41
+ })
42
+ end
43
+
44
+ private
45
+
46
+ def create_connection(connection_config)
47
+ Sequel.connect(
48
+ adapter: "mysql2",
49
+ host: connection_config[:host],
50
+ port: connection_config[:port],
51
+ user: connection_config[:username],
52
+ password: connection_config[:password],
53
+ database: connection_config[:database]
54
+ )
55
+ end
56
+
57
+ def query_execution(db, query)
58
+ db.fetch(query).all
59
+ end
60
+
61
+ def create_streams(records)
62
+ group_by_table(records).map do |_, r|
63
+ Outhad::Integrations::Protocol::Stream.new(name: r[:tablename], action: StreamAction["fetch"], json_schema: convert_to_json_schema(r[:columns]))
64
+ end
65
+ end
66
+
67
+ def query(db, query)
68
+ records = []
69
+ query_execution(db, query).map do |row|
70
+ records << RecordMessage.new(data: row, emitted_at: Time.now.to_i).to_outhad_message
71
+ end
72
+ records
73
+ end
74
+
75
+ def group_by_table(records)
76
+ result = {}
77
+ records.each_with_index do |entry, index|
78
+ table_name = entry[:table_name]
79
+ column_data = {
80
+ column_name: entry[:column_name],
81
+ data_type: entry[:data_type],
82
+ is_nullable: entry[:is_nullable] == "YES"
83
+ }
84
+ result[index] ||= {}
85
+ result[index][:tablename] = table_name
86
+ result[index][:columns] = [column_data]
87
+ end
88
+ result
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,16 @@
1
+ {
2
+ "data": {
3
+ "name": "MariaDB",
4
+ "title": "Maria DB",
5
+ "connector_type": "source",
6
+ "category": "Data Warehouse",
7
+ "sub_category": "Relational Database",
8
+ "documentation_url": "https://docs.squared.ai/guides/sources/data-sources/maria_db",
9
+ "github_issue_label": "source-maria-db",
10
+ "icon": "icon.svg",
11
+ "license": "MIT",
12
+ "release_stage": "alpha",
13
+ "support_level": "community",
14
+ "tags": ["language:ruby", "outhad"]
15
+ }
16
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "documentation_url": "https://docs.squared.ai/guides/sources/data-sources/maria_db",
3
+ "stream_type": "dynamic",
4
+ "connector_query_type": "raw_sql",
5
+ "connection_specification": {
6
+ "$schema": "http://json-schema.org/draft-07/schema#",
7
+ "title": "Maria DB",
8
+ "type": "object",
9
+ "required": ["host", "port", "username", "password", "database"],
10
+ "properties": {
11
+ "host": {
12
+ "description": "The hostname or IP address of the server where the MariaDB database is hosted.",
13
+ "examples": ["localhost"],
14
+ "type": "string",
15
+ "title": "Host",
16
+ "order": 0
17
+ },
18
+ "port": {
19
+ "description": "The port number on which the MariaDB server is listening for connections.",
20
+ "examples": ["3306"],
21
+ "type": "string",
22
+ "title": "Port",
23
+ "order": 1
24
+ },
25
+ "username": {
26
+ "description": "The username used to authenticate and connect to the MariaDB database.",
27
+ "examples": ["root"],
28
+ "type": "string",
29
+ "title": "Username",
30
+ "order": 2
31
+ },
32
+ "password": {
33
+ "description": "The password corresponding to the username used for authentication.",
34
+ "type": "string",
35
+ "outhad_secret": true,
36
+ "title": "Password",
37
+ "order": 3
38
+ },
39
+ "database": {
40
+ "description": "The name of the specific database within the MariaDB server to connect to.",
41
+ "examples": ["mydatabase"],
42
+ "type": "string",
43
+ "title": "Database",
44
+ "order": 4
45
+ }
46
+ }
47
+ }
48
+ }