From 9e6d2195e9f177205503d8c4785900d308bb4b11 Mon Sep 17 00:00:00 2001 From: Rudo Kemper Date: Mon, 3 Mar 2025 16:59:05 -0500 Subject: [PATCH 01/12] Initial scaffolding --- README.md | 2 +- c_arcgis_account.resource-type.json | 25 ++++++ f/apps/folder.meta.yaml | 8 +- f/connectors/arcgis/README.md | 9 +++ f/connectors/arcgis/arcgis.jpg | Bin 0 -> 132332 bytes f/connectors/arcgis/arcgis_feature_layer.py | 72 ++++++++++++++++++ .../arcgis/arcgis_feature_layer.script.lock | 6 ++ .../arcgis/arcgis_feature_layer.script.yaml | 50 ++++++++++++ .../arcgis/tests/arcgis_feature_layer_test.py | 0 f/connectors/arcgis/tests/conftest.py | 0 .../arcgis/tests/requirements-test.txt | 0 f/connectors/folder.meta.yaml | 10 ++- f/export/folder.meta.yaml | 9 ++- tox.ini | 9 ++- 14 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 c_arcgis_account.resource-type.json create mode 100644 f/connectors/arcgis/README.md create mode 100644 f/connectors/arcgis/arcgis.jpg create mode 100644 f/connectors/arcgis/arcgis_feature_layer.py create mode 100644 f/connectors/arcgis/arcgis_feature_layer.script.lock create mode 100644 f/connectors/arcgis/arcgis_feature_layer.script.yaml create mode 100644 f/connectors/arcgis/tests/arcgis_feature_layer_test.py create mode 100644 f/connectors/arcgis/tests/conftest.py create mode 100644 f/connectors/arcgis/tests/requirements-test.txt diff --git a/README.md b/README.md index 8b8c072..eb4e6a4 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ secrets managements, etc). Some of the tools available in the Guardian Connector Scripts Hub are: -* Connector scripts to ingest data from data collection tools such as KoboToolbox, ODK, CoMapeo, and Locus Map, +* Connector scripts to ingest data from data collection tools such as KoboToolbox, ODK, CoMapeo, ArcGIS, and Locus Map, and store this data (tabular and media attachments) in a data lake. * A flow to download and store GeoJSON and GeoTIFF change detection alerts, post these to a CoMapeo Archive Server API, and send a message to WhatsApp recipients via Twilio. diff --git a/c_arcgis_account.resource-type.json b/c_arcgis_account.resource-type.json new file mode 100644 index 0000000..0e3892e --- /dev/null +++ b/c_arcgis_account.resource-type.json @@ -0,0 +1,25 @@ +{ + "type": "object", + "order": [ + "username", + "password" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "required": [ + "server_url" + ], + "properties": { + "username": { + "type": "string", + "default": "", + "nullable": false, + "description": "The username of your ArcGIS account" + }, + "password": { + "type": "string", + "default": "", + "nullable": false, + "description": "The password of your ArcGIS account" + } + } +} \ No newline at end of file diff --git a/f/apps/folder.meta.yaml b/f/apps/folder.meta.yaml index 8a289d1..e502f13 100644 --- a/f/apps/folder.meta.yaml +++ b/f/apps/folder.meta.yaml @@ -1,2 +1,8 @@ summary: null -display_name: apps \ No newline at end of file +display_name: apps +extra_perms: + g/all: true + u/rudo: true +owners: + - u/rudo + - g/all diff --git a/f/connectors/arcgis/README.md b/f/connectors/arcgis/README.md new file mode 100644 index 0000000..80eff2e --- /dev/null +++ b/f/connectors/arcgis/README.md @@ -0,0 +1,9 @@ +# `arcgis_feature_layer`: Fetch Feature Layer from ArcGIS REST API + +The feature layer URL can be found on the item details page of your layer on ArcGIS Online: + +![Screenshot of a feature layer item page](arcgis.jpg) + +This script uses the [ArcGIS REST API Query Feature Service / Layer](https://developers.arcgis.com/rest/services-reference/enterprise/query-feature-service-layer/) endpoint. + +Note: we have opted not to use the [ArcGIS API for Python](https://developers.arcgis.com/python/latest/) library because it requires installing `libkrb5-dev` as a system-level dependency. Workers in Windmill can [preinstall binaries](https://www.windmill.dev/docs/advanced/preinstall_binaries), but it requires modifying the Windmill `docker-compose.yml`, which is too heavy-handed an approach for this simple fetch script. \ No newline at end of file diff --git a/f/connectors/arcgis/arcgis.jpg b/f/connectors/arcgis/arcgis.jpg new file mode 100644 index 0000000000000000000000000000000000000000..830e44e280baba5dc6513119cab50de722becc38 GIT binary patch literal 132332 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 Xu#o)Akm}bwW|Y7O%so(s@fJ) zcKeDv!|R!^%GAql3yki~8{k!Z(ceCMP0`5%+)gjwBA(~P*1tdP{LB@xyr<>j>%4P_ z1x9EY{OEX-Zr^9PB#_!dLIbw-8gsS8aUMV3e(xHuf9Q~X5Z6b%W}cXLC9Bor305jU z2#KP4ak@~<^FaYF*~z`;PYOi{)D++1dW6D~j%B=m$M_#ro8||v;^UfG95+^Zph8k%L7IAUOp8Z zq%31|rC2#Z&VH&;!6LhdDcg#aM~W8Z+VgT9#TMf0M#p+WdL>^rR^|F%>tDasGi3X= z#tkm|VWFsaQ~+W8>&Tz~j2D$5#&98gz-q7gMavV`j>l(>fvP|SsAceocGXiGm+x=> zI%4oD9^c9g4wP7^TQWa*g3+$gX;6-#MT&+o3@g!0UPotq{m5$725IY~4XHhV4p4Bx zTMk+NU-c3@WAyp4oc;>s{Ks1mO*iSIC(WbJ>p7GNy!1$`)%#gr%kuHhB+Gf;S*3

EO%sG*Gk5*Jj%+9&Ji(r<$YIM!KIlOiRu=+t zf)AZLwOlD5Ym+VkawCZ5e;s+hSf|{o5cu18uh#ZO71axr>PfP|R<_u0>E|rFPq3@Qk{AeW$;pc(!U=dJM_H| z5RBF5@HhUH3XRr026rQ_2ab1V^-O7aIj4f$-|h>!xur&MVv1H`DE$!!&x@-><6SXT zl5mkBBSD&uOp|L)sIi!Ou2g2Xk22Kq;K*#Bms2f zVbIlq+oxl{5TlZy-~Oqr9TyjGD+*n;(EmA&%eE7k#%~+MEbkK{64)V${S}O}EmiKB znbC7&?QNtDblrZ={xM@;kQisiSCb43=@UC_sK_<0E66ypCvyvu`J2D%5~7uh&6H9H zQxRlPYUx1`pQjxk)^S6nbpyLA6p7v$r}^$5y6#>S#C&~s$o~&`gqXmEJ*j^Ef&sdI zJ^1j>@#7W+gK1`(u09bCTh3Aso1p}}fQ+P?*1nW2=AR8iUeyJ6x)(P(Z`}U!gGwy5 zEd$34vzCf#*>%{(WpC?{K+#t;Rz}>=_uSw^;H96wU#69`e~Y?lC>+(G-2SCD<54?n zlu<+s;*UVB8PEcGL}N_p0p8RUS6v*k(>f78zGEH!b@cj!__DIimuT~}@HfL*#&$Ed zFrSC7F${t9DwV01G^28AUe7c!L8LYB}#rTOu1xOEa$U3rIa~9L_G-OQW3&&ly!M`f&4;vvS^tS1|*2{5^Xm z&hnn5ZaL+A)`<(O9E13g>5ZLi3y$+%{o6f_+Y2+slh4nZpun(eWC(GGjlrQA)lvmq%g zZ&_U6s3$d8LhoP?7HSYBMn{%8-7tM~P8iPPX53Iow2rE-i)vtrE|I4JU^jeUF|I|= zYX|V)7fI+YmZ&g=72NJA#FINGh;gO-+0Kmx1bPew+A4dwVx6VRtFK&`3WFC{E)>-Y z1de8`bO~L&T7|kql7>f0Zb7D66ioC8z2AAnR07vz*&>k3&5@?~%sOE~%E&b&fhkFc>&Tamu!0Nt z0qWGb(6hz{Eg-o!jl6eO6*5`zxib++{=*D`0ucrkX!}P+5$@&#r!p)`T0AzBwOgg8 zA|Pbt%G@b8O?@Z=+zl6~o}f_Yy--!>>bIdc9S1o1&2Pl zw*+MQxkNRmqT1ew;(e9?Z<9gkMJPYU0sBkd%u~)H3%crmR=$=Q^QLhqwLMeuhr}Kf(<9wD59>^iFi~3NiMaD5IiV}@`V%=d z%wLBpX(|tRwZ!G=<-uyM?e}b}HrDcZeYWotl$x$dww#i0wy8t0l{&t`F3WhRHnR!C zu%cQbE@{hzd{vfz{5KDC(Zd1`hd745y-lh^LJJH1fUDV(+2ORU$r={X!C)j`|a z3klDSIvvw|8@?s(I$XX->>^Y6^}h6=n!3UPY=D5EbTi{_?|awvlb)9{J8m2M+~CGL zDn*`3)ML+*L!@&6O!l zasHmP;f3~PC78NudkcAHL#dRw4%BJqr=}YUk|bDwz>Va|Yhh}??anPo(Lp+ZAMxJU zJG^ovc3jq|He0=AwyhL~0ItN+asqz(<9~g@w(s40q}4PmoUsQ)vBaYTQMap3qmom%sMu=n0kO=kPvIF4;B;2?t1 zM?fNdC;>wqM~d{JB%z~p5{e{1lu*V3LLeYuK)QyWP^1J5p{VpOB|w0HLg<8Er26L0 zJ$K!6&zW=HyY73}+~4}Gn?JJhJbUjv``P=u+qZl^;6<>XATI)C9uV~!QPLeI=P?Kg z(6)~UtL<1$-h0v4>5Q?jpr*&a5F}|vj zvh?wuG1iIvfpUdw*;^Olmsv8u>=<^V@w?KOJBmK8rtLMM^Lf_268F_nQHeIag*Y;# zE}{Gz+i!uM#w02;_`)UCm9(0`hK{8!ik!B}zVc8>Lrm(*n}p=C;ne3hpm#)?tMv>f z`sOipS8-SIsO|a3)t$5QulQu!U03uV6#^})db=XzwxRq@!kbqZOd5U#^Ru54ax_g! zQ_7y)uDSYznFCFOb4lM1yI=Of|-$-H2BgMuPts9(RyVU^jccD&ql3GUEts5H=D)fRj zM&=bq32f*gp&{8-siuFTxOMt|U4Cv3_A!`tIs|GgqEiiRn&^upOc08}bPGzU&ZmhF zB^S7PDvAr!mn$ZA+tKgUOxeAk%ZolQoAYU>+++Fnrq@9U1%em(>Xd4*57sE)V!3M+ z)Lq=Q*_Oe{cH{K9M|U$=HiupQpZ{KR{~%uHblUGqbIvs1!;EiiFFu~sd-=XQXlc2; zkuO%;7UP&-UrzBc%&IRpZ^&Upo1!D@+_5Oj!G`$*_5S@pgr!!|P?B zf;wgJ4#!@>JH^-cY>qM&{AXt;k4XUXN@~@Ed>!2h-RW6v({F6=!kD+F=u0Qz_-AD+tmDC8PD|wGhv#>5F-en?&DmGvrleEWGLTC5ju>jnj__SUYT;$oeSr* z>|UHGYUyJEP)0Ru@_%ES$W<`%7C!c1ZL8%QjutJjfxjzH|GuIBOLtC%{MAg7$JM~p z`Nnq0ZTfXi@#lYFr%@WMKIv}$#W9kY7^u;2d@5SeNuQfvpg0IGvbEG%ZGUj^a{RaJ zyI+GBa$85<6qd*9n=h+~ucbg5y`%(dwh%|fU=^QbnljiJr#$MwU@aOYw2q9c-a8hb zP64hq>Y07e!)AeJT*v}G*d*t~3T*lqA>WxAs{;n36hP2`#SG;{&qJ@TUOZZyZgb(C z29*JHEdaoy2&R(eJ?y=+WBl89a_-s>L&L5o*QHZF58GZa&vh^25A?Rw@sDvpc@bE{ zG!jFskxNrF_8hYbwM#5=%WtdNj_7GMX~~Jl>kc8v-8A98s9_gJ_kcxTHa-o*d>Poe zveKtOP0&ks|947UByF^ToS5^H;WEn6%4Y5yzcNHgiw(gDP=jS^zEg9;&Z0%VGPtov&k! zy1N$FI!t|JRdTllxZ|2wi9IA>=wIer@RB0=B0vk5#X3|Pn>vhzz0_I5qz0~P{6Wv3 z_+1y7tD_J!R=4>0cMO3OZhtWxy8Xp=^&1=8CawXI^$JnFVzDX2x;&#C_l-@7b=rG1 zdAu!=;OxOV9BNPQVZX7hnm?}ngU9?=Z=DcE9ThZuV|!BgjZI+Ivm~M$9h7(-Xibw2 zJ!hZZg^)xl+p;5kGW{(r9Rt`S|AD6C|Mv9%#MJYbzkscCxvq2P<-JSliuM^~qP_4w zGyr<6&jP>9Q2y_~mMgb0Am7E0{8Ai!c+8`gWym!QxX1vEE!TDRzbK9DPwtHpi z!q|~6@2CY95)w9hEwV~azG!aFfmp5b;D-#}`GTo4*+bVfv{3B%)1`^5J){)=i9gy} z3=1Q&48oJH3pEn7uk@yit?d4$c}9XZNZMK(al_V`E-V#4R?>}xhDOb4s(o2DdKz7W z_&(81uYe$5xGZ7~yS}xGZ~ZFqjcqq0Z26%;+R}$9)6X)%b+oX$X04Fe@pl7`SIah= zE`_H9QCusoKW%Nqd+Vz?*x)N|V%eBtN z@}{+W_CS3KMZrNpzO;OujYP^j_OQQdsr~21?!~kI;!>727#^Fts3(UpSZ+~$;qw}1 ztXSOSfE{q)W)|Q2CHw>jG*>~QIF*a^?l=#jlI+n?6~Ydoo6>*6v&-`LJ7^+;Ly0WRck`qdaHyTlL3 zmq0eNMUi_fpBYTC@lkhgc0f}4mb#|~$XDLN@N@alYnEvmPKO#gxmcQXDbvu2seu(`7CQB$c7ZHKP|t~6B;dPhf9@Aceq>s-U8_%}AD(xulw|J~y9 zpYqB1yHfwv8)s*;vQLV>c*=6Vae8tTIWoycE5fx!ePc5#oz=NDiQSld#$@5nu8s!u z>dLW9q)Zl>#j^0Y)Qix|_x*yRBi|Y6I+a@&tM`D5<~DLu`1vyg&etuBY81Ph)I;yA z@|6>QB0)HAn7zC|pfT9E*JwN7piFN4n1@Wa$*w0q^KIGl{TTR-Ef2*MQt1rksy*>d-1ZLqe3eT`2vx!jjyOBZJNfJJaO z;`p?4poIb1btxuY+1$}6QZF`Xo@Z=VBe(4LYH@c6{+I{lEqNkn#pUQ3{yD%6Rhfsh z`E8?CAB%@Q-svkbyX4k?e^+(iq(gx3zI*6EVdv|2`X@7#QrIm?)4dcEDKq#c`RJG@ zy1iVU>v+x;Dt9z(pwxPRKW*{nObo~lKd@0R0yykL}&QQ@D zAQbpY3S?dQsq5D^+7k~~_+@~pwf$w%Ji`)^4OX!qt*tlrXY!@cUiF4oEW~{bv-mzA z>It@}=1S(DsR69VPApW*{85bpFsfS5-^GL$P~hxI?UtnMCHFFegLNICcEa{UyJ5L| z4FAaOAS7Ny3Y3WBKB$i6pZ_`%q&w6N(xZeQn}h&Pa_P`U37epqQ02YVXidg9w(LTJ5m}j`ul{9RXliZU`^I+eVecVEc$MeU+e^43Zax+0qR@5cmCs5g z&H(#Y9(e!3`A$W?t;_%j@fB6u9(UC@K;wmK@Aqp~b*AsrIW8!E*K0sEv3B-~bt)ME zwa);}EC>KZ>hVdBI+gGW!p6J(3xcVm&u>Iy^bB?pL$Q`KZXJY7m!*mN08NKd{JVB@ z@efda_$?>jLKlA-VBsDopGcpb%VVJt|D*~mi;#YPlqWHzbAnhAL^$|SN!UT&bKA7V zpkaaq%0FLrd_L@G&hbDfc&{;3(C>WQ4_nPkqgTzKrDvSdD~x4j0~M5&-H7RPQi0mq ztP_mpi!I~(NYF9g!!sASP@?WFKWLQ|o}5j@aUPUPnL%CXs*g8OY>J>|;U5<5KE6*9 zMML;;hYEa4(y}`|VAk7njsYCf{3nqtcfBTd(d>1(&P9P4gTAu#c0Pl+B)yPMz6`sD z#Dh!Wp?LyECWVdNru*WpZ5m-0zh;Q691Fvq%Tc1e@q(*e%rUqoqN- z4l=vTUtnHMjwly;2-SjFclPjyg~=VC{9Q;EXNcj~WG1SxLECEY?0AqYuA^4w{WQ}u zmH*d;{kQF>;)yOUQPu@TkWx#8oQ}!S`>)f#DKA(|O?+n*9{Kt4hVl8jl^LJaa3;)m zg@6wQYho(idsN;~t4V$~EL&kNFYr~ABxbIo25uu>-Xw8ydGD~4uVVq4HYV5noyMC+_Rf5$(ZhPffHlsxcoDD2m&MCj-`KtvM8!SwHUpei+7`N=YfzIKTP5^mH&Ns8~AxX`q>Hl0U94!vpW&;$>nA<|DA;HGlo>D6T{*RK-(kq3E!E#~ymX z0)X`w5Rf+Vh{8xj#q2g|o8I}nKe^*>JG~?j>?M6ZFO^7eLiC6izRFm+lspwt)AK1% zU2g?7Wg=DYm_ufA}2Ywf{#yI-gyd%QP-(_!&b!{=j?auixB z$$JL?`DF;1^VnPxPv+)inUIuYh0Q|#y>HtNgzX@QKK)?>)_Vzq$DbBq;%@0Mr*CW} z=Z6pEOZ`MdYR~75yQvRM?yIqO6tflI*jfg^v5mdhU;9!YUUCW+dd8_@3#bge`e+aS zgYo50!Lhp8d<;vJ8vk&I3A2Y0oWB#T|FM5NQ&yQBn&<8L_0IA)w(@;h<#57J50o&0 z@W`~*BMa${zjy*}G|O3;2Cp*B_og)dQ;MyBGSZZ}^hR6e_o`-%tH+T1?_CZKLeGAE zeDCVv8PkU9&tZ{kyqd?8kB@(}I{l69MB*y*tMHWiUy^uEg#9Id++R{o|K|9Y{G$K+ zPygFa$N#^Sg3*5)h2xyk^4Z+uoi!x~-V?d+|1%5IA86P9GsXTN@?e)_0%H4!*kWbH z8!y}3q*X~5Lsfa&)mPx$tCq7!t07VWp#&Mvm2Xcycb#K+Us3}Kzkn7d00#D-UQ(}^ zTKQ#U20V+kmi7%lx`FL>lu(uQ!eB6wukX9wBFd_0!`L02@65?>e&$p&;Dsnq@+NU+ z*3xh>AD^CWW2hGk=0`%^nLIMgtX2@o(paj46E+2IO*LEsI!0$@^;b$Zv?z%pVWScK zQYS#oXMCy(OM|3e@*1)y@N{34!6m$J zf{6+C!phQA+yN$OkB4t>$~rr`yebIeWl5U~wu8X0*x`KpnGbyfqHNRqGCEaT*NZT?W32AwcDXM4*7p;2lm*Fqo73F6!d8Y~5f=SNiA^iqjkW{trsVF7VT6 z>3GgEXiB{$Q`dJC)Rv~9Y+Rd{gVx)3nX2t9+_+U#b@@V}l)4BbB<4jQG^FIbH(j9h zZclknVfP_qYX-8zcWwU`;n(Rhk_Zt(xqRzQROFC+;do@!_$g=mB{_rLvMV!#*VM&$ zxwA4?tW~0j{5Azm2VS0CwVj8)Dt?dlm(IpDgHvbm*)KAw?leq`DrNHIG>kdAcR@r! z39JnzA5!+ZYLCxK#CJ?>UW+NVK~9?oGTqXO6m&aS4!5*x1%;i_N#}s%#T#~v;w@2c9HQx{_|%N`I}R>wuS&Ih1{E+s7uetz+mrl zvndG^$X9WeFe!5&8j4ey-LT65TSfQb zx!z6M4X^^DEqGJNrj_L9){vJO*@@*$-nQdNidx{-MttCAfbx$V!RaTzD3~Ow7?6u& z$Dv;yEAS-Lp*gz}74F_=s@;NC!X!66RRYMTeH26| zPGd!#mI(#))Irl5T$EA^C$@;;&%5k|S#{KzPeyP@M7x?6)H+)qd=%PkQ6YJWT~^mMaaApRJ1Y^YY9bP=riG* z#|&>PnIz;?k-T2%v@wX~gE&_w$B;8^ljj56C;`a65}(CEsHk0SJygQ|(_T}ETCqGC zbIsV>86+tEl+ju(s;VY3$^i%2OW8#MEpLH^N51qUh_hSI%>`UCysKgsjO6@=ti`KYPR<1oHi}?Emj+r?et#c6wstxMDuuV@(x& z5mSB5pY#*NLETRE`mXV2k3_IoD9DT(9nT5Yf86zsG|jG7=pV-Qy2-YEdTV)Da^`;i z`EGTfx9(7Jgl?wAfl4bMnGds~2Qs^QPud-*$5$yN8)99!Zey$UJf}JH$WBTcO17y9 z2#zl*MSCGsP*QKHBKZx2QOZ%bMMcNX<1du$YCkf^JvNu%9}`y-Z2-QRr~mRL93=? zpBa3nyb-}eK8xM}=<^HG#*VTKIKQMxeXG=ss%`M5{12D#DXES6km8Yz!3>AZF+uBu zKq&yS&d`dvgDB}_VNNbvyX6m+=&T@_CZTEB~3%SWMPw+T{6Be zo`FAH=B*tvxtR7EHu|JF_+{sX>|~!nQ7{Yk1RsE+%YNBD0|h<%pxwUt2y^S}3`$fO z)+hqcSQa5$1ngd@Qm|UYVs}=D!z+-iePA(6%Z)hfJzT=_MGt8LuiTl_K%V->20oPE+<3h0A1585J{BXaFqHl#vzF~dSG4jRLKV0eRc`<0wI5`w_xsA&pw+mw z3K)E|@k*t5{&}ha$~B_lK8u9yLmIFcPBn9eo1tLam++qUxd}w>!+>|*kc@(_c@f%Z zp-I;g6HMmf)lQ`ai&iiuRx<}k{CUvj*G4}17#K>_CWBL>N$wv=?w>CIY;5W@F7laE>tBo1NjLL#&wwa{GeSLeQj5jeN) z=-sqsB8!kt-iAPnjwW%2{c#udwi9TGV1Eivbf!vDcVMbx)651v5C1aYsU1`E`W@I zk?rF_s_2*SUP-(cX18S+#M;?_wIoQ8Oe)W6^9^Zb12?goRR0GWZP8tIs@HdW&v-K6 z^WE%%sY9av4{M!W^vcX^3a_B?tK*(Jw!fChUimV+@_N(XVMfCLQ`sX&DZcuTg@Kaq zSAXxX69#u%Wm(v)2N=u0>%TvJUCuq4>AX`OI;iGZHRp1I3z86pCGbj!I;#E z<*Qo}na)en=PL{krvNQhHxDuwV4it}Bm=7Dz|~-tl5$}2aPKsJmg#EvQ?>s>S8e-* zZpbD7nE}2%nsl<9Q>Azu6W&E3rc|5Gs>a$re;1t+U7SE~(l2U@ok0fq%&cq}F+m@T zIHZ28dR-UNvrMIEERVTb?(iYOaWKi;#e1IzAUC*8<_ww_myORnUDNzq zsAhW^r;?NpEy?QQcNZ*;znC}#=&Ce?GJ6vi?@3xJemEQtR8-z4xQ9 zZIib;gMN8SEF&5}HocLXL! zeC2T|k#B^QYsl-(-4$7j$pPTa&|l@WESztud~|1$`THJO*K8jny3+Lr;+b7Z``_3$ z5_eu9?n}AYey%W-vRR>!EJCxooO)l&%g#*D>sdZ^ybWUP`BX(?xztHk{f%Q4p-zZl zUKZ2vs0J(2Dh;4LNfT*r9M_$kL1}V@>XY9uq*{%q2YyLKX%sND5qWeoi>}`c-v5U& z@u%;^PFxxi0QHSF0C)_4{c?Hc)oU;Ic(?d8d4V|*uVE1sISlj@8)@1 zhXaD{j9;HsB0~GTc$EEatrA-+0u|S5Gu-o998E zs36PIBp>aX|1DfR(*e4qBBKzO%rsy^7l@9dfxns6J_KZ5WN~nQ3`l6!1#Ug zTAGdBl<=kXpvLPad=-<-y{T}p)kM!DQRC_*>b2;@na-MzbO(S_r6}x|McPGRSK1ud zA+VnJjT4ZI;;IjZZd`BlS0bXq)y4a*rMqh(XSI^rZzp7u^1;C?E=ev=O1-Fh@;A_d z4qzRWoTS`KUy+YrluU&!uD;nwmZ25n%OL}t8tpPwIVGtay|{i*FU+85n}0by4G&OQ z5&rop1FLw6Qz6wQBf)c;)x}p%)?l5B$14Hn2laR^tKT~zEVQ1Rt9md1Tu}YY5SV zceZ)FoimMx`#FV|M3zC+44SFFoL9BeE)RVhVEJ&Nc++m>HidMFL-5v5% zFIWGNQUJWT-{-es#(dS~Qg)|ZPEJx9Ybwz>vKip3u<28>>E#t59+6w<)}Rr+1Fzo! zz2y9~92;&e+0kxA9N(!kv!KIH2n`&Xr1qe}vaX-RMTKvOI|T~?R6P$o-Jbhk>hSe< zQ9kYCjKds!Qq>20@)?k$1{z5V(frsX*;?+_Jca;WB||hOGF`RpDOlTg_Rpd=8U)oV z$@f~C^~*@yk}}-!_mQY=+Z@cW$Zr13_~wm919FV6L7t(aD@`ExU5S2tI5zgK<7Hsc z-FQ_XD;#2vYPL#;K9JLEmbsxWO|n5-4EbNI+?y)e)w?1G6%`9&_Lb-o6ORNl`nazo z;qiI%zibFdeX#C)*pGo0B;0nWEt9z>ZgQ8);{X^>q?O7gP<+x@uq>A2sVM)G{V5vW z6Wa#=s-OH$ne98Hzmb#w_Dd(*Ca2Z$?08Dt6SP&kFc6f371&=5_QCczl@@}X$ElW^ zAd4Q?-=rO~4FM)zp$0k1gb}A!B}I9m_G@)aBovQ7I1lrm2=7bP*woMUuadjG*T*kK z0K1r=Ey&z9X#_yoxbM$=P>(thrh2OJKK$CU6w31TVwV*sxMSPWDQ}_>Jc&ce-19#Z z*}vHeH}N!rD0`%cc_ok_x(qK2HOMEb;boCiX+=lohXyvi#kFs2cg(vf<^%n=A`zwN z+<>-)vU1{Z$YfVkgTe}>Gz_dv^?S9b>^+`uaLv8#F0J@(i-@W_3F`z}nzMx4(1*JQ z@7(-{4f*FE9t8|-?{9MyO^4n&XUiSk?OZl$d)2QJL9+!fwklO^T=b8+VuTh8suCAp zH6CkALlp>C3MgK0H1@s24KsKp;HrE3R2&vZIu2CUW}!Rk>JDY|Ma{xkc&Gk%@*eci z$K%+Qau0?affOrAMPsE{C}=~K!rcRQU=aln2&d8op8f(MXK%h67q4%b6)cpldI+Dh z#U(WhG-0v*8W7n&H3@Dkx?e>b>noWpK)HPJDwmAw*2=SPE#6ZI*nYH=RlknqWAn6S zFY2)h1hKt&lWPqrBhz%GkG_->ph;|WT7Ok+%Vp1xFW;>%uP^Vun;h`1X!S#$q9ZP%_zb_V(G{@mm!H`m`{wRi|9hM&N4Vk;cyKv6D-+ zg0X!DtV8Z?`7Mr24iq=#fMoC1GO;x7DFp z9T$7x?_+7d+!kF$R|eeUK)F8;)+RaFRIGO@&(At%2R%&VOCwphvLc?uRpF=4qz+gW zD>`@ro^$J-znEZ5&hawK=Ij~ETg1`>mG6DuBOI}>b@D}NV$1Y$&LDLO3}~~;R2^qR zC}pww0&UeWV(>HLcrX_}Nj_Ne(>plHclzz7&W+P4o@Krx!C*9mTrOs&s^j7fscY{T z!lS%0xI8_+1I2%tjq8{nJ=AKupJ2y+7Y=<=>MJSjRZ?UX7%z|@LUMM;bvKw0D1|7w zH(NtKHc7JmnV|V!7Pn{f5kU#%SG{Y4*Xl|ja*~fE32AuQmaiwy_RQ;7)I2wYrU}OQ zr1uY0Uq7o6C02eQ-!l74kB+;fJcbdb+D0gG4tmtC8=A9Q#v@5}S1%WRWZ`eTgOCe4 zzH^W5+0@pATQz(P%nBoA?zEIvtVG-RDvGV3B}MSe>M{Q1P;)P~$RNj#4e!tG^^<{< zqiCU%lJwo8EcS-VWVY*y3$=|i$TYCBeZwmMfnjt$;vg&$u!TL!X*)BE|ZA@-%|kee5TlM-`j4@+V_GD z3d>6;`45b7={kGYk-X<6OD0=P-8T|`JO$iGOB4Ic!|qOwTJWTIRt0Ln^4b{S1Lu&e z4r4aXt_b1l>M}0BiV#3osrX09bm{&Nj4ygSxIUFJUzvu=m%o94^s@wUs)J&j0JZpw zQF2z5d!df;u=nn!pvkp~R!OpY^JkWr{|^{set+se`T7I?jDM2*za#1Us<%^+#7_xg zK?z1h9&cHu{F2MeHX5IA4S7v_r`kIiPmrYtj-0zI5~PCR({cnA=81Rov`bYDB4{5S z3a`Yg`9PKRzc>rlNJ@%GmeBR@{exQFcSGKyYhknGk}F5I;nk=*Gcb!Wo-Z=PZU(Ha zZNiFbdnWaKT(^5k=1%+@-xHr%(4rk>^&noSm`H|hqpLkbRgZ+$zDs>%+lQia-+6OV zFSB=|g;Dk{#!Xf#sa!%aZOq#0T1yKSOgD=IgJ+EsoeuX# zzim!Kv#lc8mLP24D=YapOAZ@;sedObGErb33sJshyM!zqfofEmKAJ*bRlR@ZQH? zf7AP*)6mnKyXZ!T+2DSQ#_b#Elv^O02F(>K>g*bREoKI(rhGSYqM&T=Tz&V+lM=o$ zHo|7}H7OZk*k0%DGXa&pBKK+LR`KwzV=HP2LWbx?J%22X3zstFw;uZ5X3t@_OuYn!7o~!^8nSIZ&nk*$7UMeMg5Vd~hBwE%gMpO3 zP6IWsFi-wIh$hXZT|KYN6+$xOl9Wa;-?kgNp&ju^j^KS^jO%3LTOVu+^vd|!2IAz0# z>QWsh*2Qg9Fjm9J+kUuY{VW5+VnKNK8VjFBB zNrG)HwL)?qetyD}EgGvVYSogD5CLeLKBzgR=s4fD?QrLU+eGrmxrOPWkDZ269c9GK zp=^gie_tI-T5$WsJvD6xY%HY6E#-rrc@Ouyz^Nn$gF&-$gKew6GOyw=j~=&d+a;sb zP5&Ry0t!DK85>?=cWaLe!HlrW$HCF=*}~5JFzY-MACnk_M!~Qbk%dL$JxtUsg#cY( zER#}KBn2;*p|XklA26Q$eRuuGJAb-aK;R%o5q7AYMkXk=lx6W$I$`V2za(}G#QGOd z5k*)iKo2Ft?BjUiv0at2HJvBt*}%SLdQlhPP1{LNFNGhOEHo63HT0ZPlPe71c}7iz zpN(&KBN;H}XY%}kerq`cngB}%{aTE(8H?rwY+KXdLnyxEvU1atW{n+u>*n<^b>h2Y z&--g~5>6%akH%-fdS%N@?z${FFx0oa+BM#Opq`f~Znlx5SR<1q&dBw+j zAF(4iCVNOcG2m zkmSKWSgKr|km!xr<%t-p(qCeb@iwt~^4-W+I|Ps_se>gN%LN55g5|gsk`#i_+@_}! zdm&x4P91UmGHOx0jIC*uEv?4iKBa>~afJf;3NSN}yl-p~wVI*7`&8_8{)C_+7_qdn{R`yoBY9mQvo2MVTDzny*4EtSi1CtPK(MJwR92)ZwCb zrMKC(ir_e3H5(czJ*_&x3BU2$+fTr9Af+}#)e^=yymUjI>bIvag%}_!-JL;;29+vo zIa5m@*}@&Qv3~h)TCs93qXjj!Ocy<;yY+7b+or@W1U8{}UX(qmf|2;Pp<1{!`NdcV z@Yn^N2WL$^ijHiz2%af7oyCGHl?Q;d1m|>2WP$_T?%2VV*4CmCgapx1lb$_%L5|G} z7ky(XVFro4Dq2qwUrpgxy2gB58DmwGU&l5M>P{k`QQy(6b7ux`sr0mnScmuZUT&2^ z|4_Hy$u&~f>k@+mtL82AftAfAKCVcC2S<$M^&~75x*{wt2o|i`;LGm3NV=8s<-+BB z`@M8R8mWM>k3J>RW0B-ZXKgS^UZZeQ| zRHPUzJF&w#J#8gpz5sc6_SD8hbeUr^WIDlMKWHfPiR!KImp;ZM-4&u`@aMz}ePgSX z)VSInrlsvRCY=_MdSYCw)v@r#zJ=O#zo8+4kki=)^-TDq`=z7$H2lVb0CN)C7#2 z-4$t>b#8U>Q4N=Fs@(ISy~Py7z<1}gSpwclj%+Py8&>n5>@sENy82|`noieFVZMSn zgU9sD%vx;Vh>0;dIq@*NAz)jB9t1CVkPoyTDdMj46wJ8slze*h+9VIc7XCU&^Lqwq zl;|knqe~Fc9+s0b>v5n&j6s%)O45h;9F5KUQHWpNXEH%vjneFQ(YMl%qr{5AmNWFA zpo|^*gucBdU}1?byC2uFTQ=VQOaw){f!-tlbfkj|MY&5Ll)#378Oz0Cz%SRIenze*`ypI_?1nG> zF553J-H!5pm2s1dlclF#*D1Pi<$)nF4M=`_-lQy`R^DE@$q~?ZZE;4G%rO_ zG&l@))t_q!4I?GX2t$(98GLg92p%t)H*k09(4iv9rFmMR3<|gV>OE3i$zv4Ba{yl0ou8OLLCPlPXr>)$bu33kHp4J`isZwbpmmx>d6om0alTJxw z&#oHzoUYt6@}aD|TERMBtDv4lV$LEikNB8%S{%|2va05CWsnpDXW)Gzv=W3DWs4*A z5+^t3@1O-CiJ$&@t@!f||9}1TKl=TruU&ssiNC)0r*Dj<1je6N?RqnYTWn9vLyF`lFl3#+c0TEt>}}+^06e2+I7+dQOUk@Coe$oQxBJE> z;n=kW!Al~5V@QC?O|H$J6~b-#al34HKxA%GziRYZTP=}0F%9ugcy2pE)1BZu>rq4V-}7(>?Xmp@I)WcFOkD9_hawa>N)^NQr{IeLVR^ zxtVZW_xyy5rCz`Wx~q<}wKg0pS^(6o?YFl1!D;(+m$7@RAS$rO=n1tHT

VPT{s< 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 literal 0 HcmV?d00001 diff --git a/f/connectors/arcgis/arcgis_feature_layer.py b/f/connectors/arcgis/arcgis_feature_layer.py new file mode 100644 index 0000000..0df2de9 --- /dev/null +++ b/f/connectors/arcgis/arcgis_feature_layer.py @@ -0,0 +1,72 @@ +# requirements: +# psycopg2-binary +# requests~=2.32 + +import logging + +import requests + +from f.common_logic.db_connection import postgresql + +# type names that refer to Windmill Resources +c_arcgis_account = dict + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def main( + arcgis_account: c_arcgis_account, + feature_layer_url: str, + db: postgresql, + db_table_name: str, + attachment_root: str = "/persistent-storage/datalake", +): + arcgis_token = get_arcgis_token(arcgis_account) + + features = get_features_from_arcgis(feature_layer_url, arcgis_token) + + # logging.info(f"Wrote response content to database table [{db_table_name}]") + + +def get_arcgis_token(arcgis_account: c_arcgis_account): + arcgis_username = arcgis_account["username"] + arcgis_password = arcgis_account["password"] + + token_response = requests.post( + "https://www.arcgis.com/sharing/rest/generateToken", + data={ + "username": arcgis_username, + "password": arcgis_password, + "client": "requestip", + "f": "json", + }, + ) + + arcgis_token = token_response.json().get("token") + + return arcgis_token + + +def get_features_from_arcgis(feature_layer_url: str, arcgis_token: str): + response = requests.get( + feature_layer_url, + params={ + "where": "1=1", # get all features + "outFields": "*", # get all fields + "returnGeometry": "true", + "f": "geojson", + "token": arcgis_token, + }, + ) + + if ( + response.status_code != 200 or "error" in response.json() + ): # ArcGIS sometimes returns 200 with an error message e.g. if a token is invalid + error_message = response.json().get("error", {}).get("message", "Unknown error") + raise ValueError(f"Error fetching features: {error_message}") + + logger.info( + f"{len(response.json().get('features', []))} features fetched from the ArcGIS feature layer" + ) + return response.json() diff --git a/f/connectors/arcgis/arcgis_feature_layer.script.lock b/f/connectors/arcgis/arcgis_feature_layer.script.lock new file mode 100644 index 0000000..30a1beb --- /dev/null +++ b/f/connectors/arcgis/arcgis_feature_layer.script.lock @@ -0,0 +1,6 @@ +certifi==2024.12.14 +charset-normalizer==3.4.1 +idna==3.10 +psycopg2-binary==2.9.10 +requests==2.32.3 +urllib3==2.3.0 \ No newline at end of file diff --git a/f/connectors/arcgis/arcgis_feature_layer.script.yaml b/f/connectors/arcgis/arcgis_feature_layer.script.yaml new file mode 100644 index 0000000..896f33e --- /dev/null +++ b/f/connectors/arcgis/arcgis_feature_layer.script.yaml @@ -0,0 +1,50 @@ +summary: 'ArcGIS: Fetch Feature Layer' +description: This script fetches the contents of an ArcGIS feature layer and stores it in a PostgreSQL database. +lock: '!inline f/connectors/arcgis/arcgis_feature_layer.script.lock' +concurrency_time_window_s: 0 +kind: script +schema: + $schema: 'https://json-schema.org/draft/2020-12/schema' + type: object + order: + - arcgis_account + - feature_layer_url + - db + - db_table_name + - attachment_root + properties: + arcgis_account: + type: object + description: The name of the ArcGIS account to use for fetching the feature layer. + default: null + format: resource-c_arcgis_account + originalType: string + attachment_root: + type: string + description: >- + A path where ArcGIS attachments will be stored. Attachment files (e.g., + photos and audio) will be stored in the following directory schema: + `{attachment_root}/arcGIS/my_feature_layer/attachments/...` + default: /persistent-storage/datalake + originalType: string + db: + type: object + description: A database connection for storing tabular data. + default: null + format: resource-postgresql + db_table_name: + type: string + description: The name of the database table where the form data will be stored. + default: null + originalType: string + pattern: '^.{1,54}$' + feature_layer_url: + type: string + description: The URL of the ArcGIS feature layer to fetch. + default: null + originalType: string + required: + - arcgis_account + - feature_layer_url + - db + - db_table_name \ No newline at end of file diff --git a/f/connectors/arcgis/tests/arcgis_feature_layer_test.py b/f/connectors/arcgis/tests/arcgis_feature_layer_test.py new file mode 100644 index 0000000..e69de29 diff --git a/f/connectors/arcgis/tests/conftest.py b/f/connectors/arcgis/tests/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/f/connectors/arcgis/tests/requirements-test.txt b/f/connectors/arcgis/tests/requirements-test.txt new file mode 100644 index 0000000..e69de29 diff --git a/f/connectors/folder.meta.yaml b/f/connectors/folder.meta.yaml index 903740b..bc2fc27 100644 --- a/f/connectors/folder.meta.yaml +++ b/f/connectors/folder.meta.yaml @@ -1,2 +1,8 @@ -summary: null -display_name: connectors \ No newline at end of file +summary: '' +display_name: connectors +extra_perms: + g/all: true + u/rudo: true +owners: + - u/rudo + - g/all diff --git a/f/export/folder.meta.yaml b/f/export/folder.meta.yaml index a8209a9..36cc724 100644 --- a/f/export/folder.meta.yaml +++ b/f/export/folder.meta.yaml @@ -1 +1,8 @@ -display_name: export \ No newline at end of file +summary: '' +display_name: export +extra_perms: + g/all: true + u/rudo: true +owners: + - u/rudo + - g/all diff --git a/tox.ini b/tox.ini index 72406a8..8fe2b79 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] skipsdist = True -env_list = alerts, comapeo, kobotoolbox_responses, locusmap, odk_responses, postgres_to_geojson +env_list = alerts, arcgis, comapeo, kobotoolbox_responses, locusmap, odk_responses, postgres_to_geojson [testenv] setenv = @@ -23,6 +23,13 @@ environment = expose = TOX_DOCKER_GCS_PORT=10010/tcp +[testenv:arcgis] +deps = + -r{toxinidir}/f/connectors/arcgis/arcgis_feature_layer.script.lock + -r{toxinidir}/f/connectors/arcgis/tests/requirements-test.txt +commands = + pytest {posargs} f/connectors/arcgis + [testenv:comapeo] deps = -r{toxinidir}/f/connectors/comapeo/comapeo_observations.script.lock From 3ac7241039ab44920fc2c85e2f8463baafd339ac Mon Sep 17 00:00:00 2001 From: Rudo Kemper Date: Tue, 4 Mar 2025 09:49:49 -0500 Subject: [PATCH 02/12] Finish ArcGIS script --- f/connectors/arcgis/README.md | 4 + f/connectors/arcgis/arcgis_feature_layer.py | 204 +++++++++++++++++- .../arcgis/arcgis_feature_layer.script.lock | 1 - .../arcgis/arcgis_feature_layer.script.yaml | 2 +- 4 files changed, 200 insertions(+), 11 deletions(-) diff --git a/f/connectors/arcgis/README.md b/f/connectors/arcgis/README.md index 80eff2e..2f68253 100644 --- a/f/connectors/arcgis/README.md +++ b/f/connectors/arcgis/README.md @@ -1,5 +1,9 @@ # `arcgis_feature_layer`: Fetch Feature Layer from ArcGIS REST API +This script fetches the contents of an ArcGIS feature layer and stores it in a PostgreSQL database. + +Usage of this script requires you to have an ArcGIS account, in order to generate a token. + The feature layer URL can be found on the item details page of your layer on ArcGIS Online: ![Screenshot of a feature layer item page](arcgis.jpg) diff --git a/f/connectors/arcgis/arcgis_feature_layer.py b/f/connectors/arcgis/arcgis_feature_layer.py index 0df2de9..7ae5ff0 100644 --- a/f/connectors/arcgis/arcgis_feature_layer.py +++ b/f/connectors/arcgis/arcgis_feature_layer.py @@ -1,8 +1,10 @@ # requirements: -# psycopg2-binary # requests~=2.32 +import json import logging +import os +from pathlib import Path import requests @@ -22,14 +24,39 @@ def main( db_table_name: str, attachment_root: str = "/persistent-storage/datalake", ): + storage_path = Path(attachment_root) / db_table_name + arcgis_token = get_arcgis_token(arcgis_account) features = get_features_from_arcgis(feature_layer_url, arcgis_token) - # logging.info(f"Wrote response content to database table [{db_table_name}]") + features_with_attachments = download_feature_attachments( + features, feature_layer_url, arcgis_token, storage_path + ) + + features_with_global_ids = set_global_id(features_with_attachments) + + save_geojson_file_to_disk(features_with_global_ids, storage_path) + + # At this point, the ArcGIS data is GeoJSON-compliant, and we don't need anything + # from the REST API anymore. The data can therefore be handled further using the + # existing GeoJSON connector. def get_arcgis_token(arcgis_account: c_arcgis_account): + """ + Generate an ArcGIS token using the provided account credentials. + + Parameters + ---------- + arcgis_account : dict + A dictionary containing the ArcGIS account credentials with keys "username" and "password". + + Returns + ------- + str + The generated ArcGIS token. + """ arcgis_username = arcgis_account["username"] arcgis_password = arcgis_account["password"] @@ -38,7 +65,8 @@ def get_arcgis_token(arcgis_account: c_arcgis_account): data={ "username": arcgis_username, "password": arcgis_password, - "client": "requestip", + "client": "referer", + "referer": os.environ.get("WM_BASE_URL"), "f": "json", }, ) @@ -49,8 +77,23 @@ def get_arcgis_token(arcgis_account: c_arcgis_account): def get_features_from_arcgis(feature_layer_url: str, arcgis_token: str): + """ + Fetch features from an ArcGIS feature layer using the provided token. + + Parameters + ---------- + feature_layer_url : str + The URL of the ArcGIS feature layer. + arcgis_token : str + The ArcGIS token for authentication. + + Returns + ------- + list + A list of features retrieved from the ArcGIS feature layer. + """ response = requests.get( - feature_layer_url, + f"{feature_layer_url}/0/query", params={ "where": "1=1", # get all features "outFields": "*", # get all fields @@ -63,10 +106,153 @@ def get_features_from_arcgis(feature_layer_url: str, arcgis_token: str): if ( response.status_code != 200 or "error" in response.json() ): # ArcGIS sometimes returns 200 with an error message e.g. if a token is invalid - error_message = response.json().get("error", {}).get("message", "Unknown error") + try: + error_message = ( + response.json().get("error", {}).get("message", "Unknown error") + ) + except (KeyError, ValueError): + error_message = "Unknown error" raise ValueError(f"Error fetching features: {error_message}") - logger.info( - f"{len(response.json().get('features', []))} features fetched from the ArcGIS feature layer" - ) - return response.json() + features = response.json().get("features", []) + + logger.info(f"{len(features)} features fetched from the ArcGIS feature layer") + return features + + +def download_feature_attachments( + features: list, feature_layer_url: str, arcgis_token: str, storage_path: str +): + """ + Download attachments for each feature and save them to the specified directory. + + Parameters + ---------- + features : list + A list of features for which attachments need to be downloaded. + feature_layer_url : str + The URL of the ArcGIS feature layer. + arcgis_token : str + The ArcGIS token for authentication. + storage_path : str + The directory where attachments should be saved. + + Returns + ------- + list + The list of features with updated properties including attachment information. + """ + total_downloaded_attachments = 0 + skipped_attachments = 0 + + for feature in features: + object_id = feature["properties"]["objectid"] + + attachments_response = requests.get( + f"{feature_layer_url}/0/{object_id}/attachments", + params={"f": "json", "token": arcgis_token}, + ) + + attachments_response.raise_for_status() + + attachments = attachments_response.json().get("attachmentInfos", []) + + if not attachments: + logger.info(f"No attachments found for object_id {object_id}") + continue + + for attachment in attachments: + attachment_id = attachment["id"] + attachment_name = attachment["name"] + attachment_content_type = attachment["contentType"] + attachment_keywords = attachment["keywords"] + + feature["properties"][f"{attachment_keywords}_filename"] = attachment_name + feature["properties"][f"{attachment_keywords}_content_type"] = ( + attachment_content_type + ) + + attachment_path = Path(storage_path) / attachment_name + + if attachment_path.exists(): + logger.debug( + f"File already exists, skipping download: {attachment_path}" + ) + skipped_attachments += 1 + continue + + attachment_response = requests.get( + f"{feature_layer_url}/0/{object_id}/attachments/{attachment_id}", + params={"f": "json", "token": arcgis_token}, + ) + + attachment_response.raise_for_status() + + attachment_data = attachment_response.content + + attachment_path.parent.mkdir(parents=True, exist_ok=True) + + with open(attachment_path, "wb") as f: + f.write(attachment_data) + + logger.info( + f"Downloaded attachment {attachment_name} (content type: {attachment_content_type})" + ) + + total_downloaded_attachments += 1 + + logger.info(f"Total downloaded attachments: {total_downloaded_attachments}") + logger.info(f"Total skipped attachments: {skipped_attachments}") + return features + + +def set_global_id(features: list): + """ + Set the feature ID of each feature to its global ID (which is a uuid). + ArcGIS uses global IDs to uniquely identify features, but the + feature ID is set to the object ID by default (which is an integer + incremented by 1 for each feature). UUIDs are more reliable for + uniquely identifying features, and using them instead is consistent + with how we store other data in the data warehouse. + https://support.esri.com/en-us/gis-dictionary/globalid + + Parameters + ---------- + features : list + A list of features to update. + + Returns + ------- + list + The list of features with updated feature IDs. + """ + for feature in features: + feature["id"] = feature["properties"]["globalid"] + + return features + + +def save_geojson_file_to_disk( + features: list, + storage_path: str, +): + """ + Save the GeoJSON file to disk. + + Parameters + ---------- + features : list + A list of features to save. + storage_path : str + The directory where the GeoJSON file should be saved. + """ + geojson = {"type": "FeatureCollection", "features": features} + + geojson_filename = Path(storage_path) / "data.geojson" + + geojson_filename.parent.mkdir(parents=True, exist_ok=True) + + with open(geojson_filename, "w") as f: + json.dump(geojson, f) + + logger.info(f"GeoJSON file saved to: {geojson_filename}") diff --git a/f/connectors/arcgis/arcgis_feature_layer.script.lock b/f/connectors/arcgis/arcgis_feature_layer.script.lock index 30a1beb..266877f 100644 --- a/f/connectors/arcgis/arcgis_feature_layer.script.lock +++ b/f/connectors/arcgis/arcgis_feature_layer.script.lock @@ -1,6 +1,5 @@ certifi==2024.12.14 charset-normalizer==3.4.1 idna==3.10 -psycopg2-binary==2.9.10 requests==2.32.3 urllib3==2.3.0 \ No newline at end of file diff --git a/f/connectors/arcgis/arcgis_feature_layer.script.yaml b/f/connectors/arcgis/arcgis_feature_layer.script.yaml index 896f33e..e17f2e6 100644 --- a/f/connectors/arcgis/arcgis_feature_layer.script.yaml +++ b/f/connectors/arcgis/arcgis_feature_layer.script.yaml @@ -34,7 +34,7 @@ schema: format: resource-postgresql db_table_name: type: string - description: The name of the database table where the form data will be stored. + description: The name of the database table where the data will be stored. default: null originalType: string pattern: '^.{1,54}$' From 64d5cfbba162d87e89c1a4c7f4518baf6009317b Mon Sep 17 00:00:00 2001 From: Rudo Kemper Date: Tue, 4 Mar 2025 11:36:22 -0500 Subject: [PATCH 03/12] Add GeoJSON to Postgres script --- f/connectors/geojson/README.md | 3 + f/connectors/geojson/geojson_to_postgres.py | 273 ++++++++++++++++++ .../geojson/geojson_to_postgres.script.lock | 1 + .../geojson/geojson_to_postgres.script.yaml | 45 +++ .../geojson/tests/assets/data.geojson | 59 ++++ f/connectors/geojson/tests/conftest.py | 12 + .../geojson/tests/geojson_to_postgres_test.py | 53 ++++ .../geojson/tests/requirements-test.txt | 2 + tox.ini | 7 + 9 files changed, 455 insertions(+) create mode 100644 f/connectors/geojson/README.md create mode 100644 f/connectors/geojson/geojson_to_postgres.py create mode 100644 f/connectors/geojson/geojson_to_postgres.script.lock create mode 100644 f/connectors/geojson/geojson_to_postgres.script.yaml create mode 100644 f/connectors/geojson/tests/assets/data.geojson create mode 100644 f/connectors/geojson/tests/conftest.py create mode 100644 f/connectors/geojson/tests/geojson_to_postgres_test.py create mode 100644 f/connectors/geojson/tests/requirements-test.txt diff --git a/f/connectors/geojson/README.md b/f/connectors/geojson/README.md new file mode 100644 index 0000000..122ec3e --- /dev/null +++ b/f/connectors/geojson/README.md @@ -0,0 +1,3 @@ +# `geojson_to_postgres`: Upload a GeoJSON file to the data warehouse + +This script imports data from a GeoJSON file into a database table. It reads a file containing spatial data, transforms the data into a structured format, and inserts it into a PostgreSQL database table. Optionally, it then delete the export file. \ No newline at end of file diff --git a/f/connectors/geojson/geojson_to_postgres.py b/f/connectors/geojson/geojson_to_postgres.py new file mode 100644 index 0000000..161bf59 --- /dev/null +++ b/f/connectors/geojson/geojson_to_postgres.py @@ -0,0 +1,273 @@ +# requirements: +# psycopg2-binary + +import json +import logging +from pathlib import Path + +from psycopg2 import connect, errors, sql + +from f.common_logic.db_connection import conninfo, postgresql + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def main( + db: postgresql, + db_table_name: str, + geojson_path: str, + attachment_root: str = "/persistent-storage/datalake/", + delete_geojson_file: bool = False, +): + geojson_path = Path(attachment_root) / Path(geojson_path) + transformed_geojson_data = transform_geojson_data(geojson_path) + + db_writer = GeoJSONDbWriter(conninfo(db), db_table_name) + db_writer.handle_output(transformed_geojson_data) + + if delete_geojson_file: + delete_geojson_file(geojson_path) + + +def transform_geojson_data(geojson_path): + with open(geojson_path, "r") as f: + geojson_data = json.load(f) + + transformed_geojson_data = [] + for feature in geojson_data["features"]: + transformed_feature = { + "_id": feature["id"], + "g__type": feature["geometry"]["type"], + "g__coordinates": feature["geometry"]["coordinates"], + **feature.get("properties", {}), + } + transformed_geojson_data.append(transformed_feature) + return transformed_geojson_data + + +class GeoJSONDbWriter: + """ + Converts unstructured GeoJSON spatial data to structured SQL tables. + """ + + def __init__(self, db_connection_string, table_name): + """ + Initializes the GeoJSONIOManager with the provided connection string and form response table to be used. + """ + self.db_connection_string = db_connection_string + self.table_name = table_name + + def _get_conn(self): + """ + Establishes a connection to the PostgreSQL database using the class's configured connection string. + """ + return connect(dsn=self.db_connection_string) + + def _inspect_schema(self, table_name): + """ + Fetches the column names of the given table. + """ + conn = self._get_conn() + cursor = conn.cursor() + cursor.execute( + "SELECT column_name FROM information_schema.columns WHERE table_name = %s", + (table_name,), + ) + columns = [row[0] for row in cursor.fetchall()] + cursor.close() + conn.close() + return columns + + def _create_missing_fields(self, table_name, missing_columns): + """ + Generates and executes SQL statements to add missing fields to the table. + """ + table_name = sql.Identifier(table_name) + try: + with self._get_conn() as conn, conn.cursor() as cursor: + query = sql.SQL( + "CREATE TABLE IF NOT EXISTS {table_name} (_id TEXT PRIMARY KEY);" + ).format(table_name=table_name) + cursor.execute(query) + + for sanitized_column in missing_columns: + if sanitized_column == "_id": + continue + try: + query = sql.SQL( + "ALTER TABLE {table_name} ADD COLUMN {colname} TEXT;" + ).format( + table_name=table_name, + colname=sql.Identifier(sanitized_column), + ) + cursor.execute(query) + except errors.DuplicateColumn: + logger.debug( + f"Skipping insert due to DuplicateColumn, this form column has been accounted for already in the past: {sanitized_column}" + ) + continue + except Exception as e: + logger.error( + f"An error occurred while creating missing column: {sanitized_column} for {table_name}: {e}" + ) + raise + finally: + conn.close() + + @staticmethod + def _safe_insert(cursor, table_name, columns, values): + """ + Executes a safe INSERT operation into a PostgreSQL table, ensuring data integrity and preventing SQL injection. + This method also handles conflicts by updating existing records if necessary. + + The function first checks if a row with the same primary key (_id) already exists in the table. If it does, + and the existing row's data matches the new values, the operation is skipped. Otherwise, it performs an + INSERT operation. If a conflict on the primary key occurs, it updates the existing row with the new values. + + Parameters + ---------- + cursor : psycopg2 cursor + The database cursor used to execute SQL queries. + table_name : str + The name of the table where data will be inserted. + columns : list of str + The list of column names corresponding to the values being inserted. + values : list + The list of values to be inserted into the table, aligned with the columns. + + Returns + ------- + tuple + A tuple containing two integers: the count of rows inserted and the count of rows updated. + """ + inserted_count = 0 + updated_count = 0 + + # Check if there is an existing row that is different from the new values + # We are doing this in order to keep track of which rows are actually updated + # (Otherwise all existing rows would be added to updated_count) + id_index = columns.index("_id") + values[id_index] = str(values[id_index]) + select_query = sql.SQL("SELECT {fields} FROM {table} WHERE _id = %s").format( + fields=sql.SQL(", ").join(map(sql.Identifier, columns)), + table=sql.Identifier(table_name), + ) + cursor.execute(select_query, (values[columns.index("_id")],)) + existing_row = cursor.fetchone() + + if existing_row and list(existing_row) == values: + # No changes, skip the update + return inserted_count, updated_count + + query = sql.SQL( + "INSERT INTO {table} ({fields}) VALUES ({placeholders}) " + "ON CONFLICT (_id) DO UPDATE SET {updates} " + # The RETURNING clause is used to determine if the row was inserted or updated. + # xmax is a system column in PostgreSQL that stores the transaction ID of the deleting transaction. + # If xmax is 0, it means the row was newly inserted and not updated. + "RETURNING (xmax = 0) AS inserted" + ).format( + table=sql.Identifier(table_name), + fields=sql.SQL(", ").join(map(sql.Identifier, columns)), + placeholders=sql.SQL(", ").join(sql.Placeholder() for _ in values), + updates=sql.SQL(", ").join( + sql.Composed( + [sql.Identifier(col), sql.SQL(" = EXCLUDED."), sql.Identifier(col)] + ) + for col in columns + if col != "_id" + ), + ) + + cursor.execute(query, values) + result = cursor.fetchone() + if result and result[0]: + inserted_count += 1 + else: + updated_count += 1 + + return inserted_count, updated_count + + def handle_output(self, outputs): + """ + Inserts GeojSON spatial data into the specified PostgreSQL database table. + It checks the database schema and adds any missing fields, then constructs + and executes SQL insert queries to store the data. After processing all data, + it commits the transaction and closes the database connection. + """ + table_name = self.table_name + + conn = self._get_conn() + cursor = conn.cursor() + + existing_fields = self._inspect_schema(table_name) + rows = [] + for entry in outputs: + sanitized_entry = {k: v for k, v in entry.items()} + rows.append(sanitized_entry) + + missing_field_keys = set() + for row in rows: + missing_field_keys.update(set(row.keys()).difference(existing_fields)) + + if missing_field_keys: + self._create_missing_fields(table_name, missing_field_keys) + + logger.info(f"Attempting to write {len(rows)} submissions to the DB.") + + inserted_count = 0 + updated_count = 0 + + for row in rows: + try: + cols, vals = zip(*row.items()) + + # Serialize lists, dict values to JSON text + vals = list(vals) + for i in range(len(vals)): + value = vals[i] + if isinstance(value, list) or isinstance(value, dict): + vals[i] = json.dumps(value) + + result_inserted_count, result_updated_count = self._safe_insert( + cursor, table_name, cols, vals + ) + inserted_count += result_inserted_count + updated_count += result_updated_count + except Exception as e: + logger.error(f"Error inserting data: {e}, {type(e).__name__}") + conn.rollback() + + try: + conn.commit() + except Exception as e: + logger.error(f"Error committing transaction: {e}") + conn.rollback() + + logger.info(f"Total rows inserted: {inserted_count}") + logger.info(f"Total rows updated: {updated_count}") + + cursor.close() + conn.close() + + +def delete_geojson_file( + geojson_path: str, +): + """ + Deletes the GeoJSON file after processing. + + Parameters + ---------- + geojson_path : str + The path to the GeoJSON file to delete. + """ + try: + geojson_path.unlink() + logger.info(f"Deleted GeoJSON file: {geojson_path}") + except FileNotFoundError: + logger.warning(f"GeoJSON file not found: {geojson_path}") + except Exception as e: + logger.error(f"Error deleting GeoJSON file: {e}") + raise diff --git a/f/connectors/geojson/geojson_to_postgres.script.lock b/f/connectors/geojson/geojson_to_postgres.script.lock new file mode 100644 index 0000000..64833c1 --- /dev/null +++ b/f/connectors/geojson/geojson_to_postgres.script.lock @@ -0,0 +1 @@ +psycopg2-binary==2.9.10 \ No newline at end of file diff --git a/f/connectors/geojson/geojson_to_postgres.script.yaml b/f/connectors/geojson/geojson_to_postgres.script.yaml new file mode 100644 index 0000000..0ae567f --- /dev/null +++ b/f/connectors/geojson/geojson_to_postgres.script.yaml @@ -0,0 +1,45 @@ +summary: 'GeoJSON: Upload to Postgres' +description: This script uploads GeoJSON data to a Postgres database. +lock: '!inline f/connectors/geojson/geojson_to_postgres.script.lock' +concurrency_time_window_s: 0 +kind: script +schema: + $schema: 'https://json-schema.org/draft/2020-12/schema' + type: object + order: + - geojson_path + - db + - db_table_name + - delete_geojson_file + - attachment_root + properties: + attachment_root: + type: string + description: >- + A path where to find the GeoJSON file. + default: /persistent-storage/datalake + originalType: string + db: + type: object + description: A database connection for storing tabular data. + default: null + format: resource-postgresql + db_table_name: + type: string + description: The name of the database table where the data will be stored. + default: null + originalType: string + pattern: '^.{1,54}$' + delete_geojson_file: + type: boolean + description: Whether to delete the GeoJSON file file after processing. + default: false + geojson_path: + type: string + description: The path to the GeoJSON file to upload, including the filename. + default: null + originalType: string + required: + - geojson_path + - db + - db_table_name \ No newline at end of file diff --git a/f/connectors/geojson/tests/assets/data.geojson b/f/connectors/geojson/tests/assets/data.geojson new file mode 100644 index 0000000..7d95bdc --- /dev/null +++ b/f/connectors/geojson/tests/assets/data.geojson @@ -0,0 +1,59 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "1", + "geometry": { + "type": "Point", + "coordinates": [-105.01621, 39.57422] + }, + "properties": { + "name": "Pine Tree", + "height": 30, + "age": 50, + "species": "Pinus ponderosa" + } + }, + { + "type": "Feature", + "id": "2", + "geometry": { + "type": "LineString", + "coordinates": [ + [-105.01621, 39.57422], + [-105.01621, 39.57423], + [-105.01622, 39.57424] + ] + }, + "properties": { + "name": "River Stream", + "length": 2.5, + "flow_rate": "moderate", + "water_type": "freshwater" + } + }, + { + "type": "Feature", + "id": "3", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-105.01621, 39.57422], + [-105.01621, 39.57423], + [-105.01622, 39.57423], + [-105.01622, 39.57422], + [-105.01621, 39.57422] + ] + ] + }, + "properties": { + "name": "Meadow", + "area": 1.2, + "flora": ["wildflowers", "grasses"], + "fauna": ["deer", "rabbits"] + } + } + ] +} diff --git a/f/connectors/geojson/tests/conftest.py b/f/connectors/geojson/tests/conftest.py new file mode 100644 index 0000000..26b3b2e --- /dev/null +++ b/f/connectors/geojson/tests/conftest.py @@ -0,0 +1,12 @@ +import pytest +import testing.postgresql + + +@pytest.fixture +def pg_database(): + """A dsn that may be used to connect to a live (local for test) postgresql server""" + db = testing.postgresql.Postgresql(port=7654) + dsn = db.dsn() + dsn["dbname"] = dsn.pop("database") + yield dsn + db.stop() diff --git a/f/connectors/geojson/tests/geojson_to_postgres_test.py b/f/connectors/geojson/tests/geojson_to_postgres_test.py new file mode 100644 index 0000000..7e094de --- /dev/null +++ b/f/connectors/geojson/tests/geojson_to_postgres_test.py @@ -0,0 +1,53 @@ +import psycopg2 + +from f.connectors.geojson.geojson_to_postgres import main + +geojson_fixture_path = "f/connectors/geojson/tests/assets/" + + +def test_script_e2e(pg_database): + main(pg_database, "my_geojson_data", "data.geojson", geojson_fixture_path, False) + + with psycopg2.connect(**pg_database) as conn: + with conn.cursor() as cursor: + cursor.execute("SELECT COUNT(*) FROM my_geojson_data") + assert cursor.fetchone()[0] == 3 + + cursor.execute( + "SELECT g__type, g__coordinates, name, height, age, species FROM my_geojson_data WHERE _id = '1'" + ) + point_data = cursor.fetchone() + assert point_data == ( + "Point", + "[-105.01621, 39.57422]", + "Pine Tree", + "30", + "50", + "Pinus ponderosa", + ) + + cursor.execute( + "SELECT g__type, g__coordinates, name, length, flow_rate, water_type FROM my_geojson_data WHERE _id = '2'" + ) + line_data = cursor.fetchone() + assert line_data == ( + "LineString", + "[[-105.01621, 39.57422], [-105.01621, 39.57423], [-105.01622, 39.57424]]", + "River Stream", + "2.5", + "moderate", + "freshwater", + ) + + cursor.execute( + "SELECT g__type, g__coordinates, name, area, flora, fauna FROM my_geojson_data WHERE _id = '3'" + ) + polygon_data = cursor.fetchone() + assert polygon_data == ( + "Polygon", + "[[[-105.01621, 39.57422], [-105.01621, 39.57423], [-105.01622, 39.57423], [-105.01622, 39.57422], [-105.01621, 39.57422]]]", + "Meadow", + "1.2", + '["wildflowers", "grasses"]', + '["deer", "rabbits"]', + ) diff --git a/f/connectors/geojson/tests/requirements-test.txt b/f/connectors/geojson/tests/requirements-test.txt new file mode 100644 index 0000000..4a80eaa --- /dev/null +++ b/f/connectors/geojson/tests/requirements-test.txt @@ -0,0 +1,2 @@ +pytest +testing.postgresql \ No newline at end of file diff --git a/tox.ini b/tox.ini index 8fe2b79..16049e6 100644 --- a/tox.ini +++ b/tox.ini @@ -38,6 +38,13 @@ deps = commands = pytest {posargs} f/connectors/comapeo +[testenv:geojson] +deps = + -r{toxinidir}/f/connectors/geojson/geojson_to_postgres.script.lock + -r{toxinidir}/f/connectors/geojson/tests/requirements-test.txt +commands = + pytest {posargs} f/connectors/geojson + [testenv:kobotoolbox_responses] deps = -r{toxinidir}/f/connectors/kobotoolbox/kobotoolbox_responses.script.lock From 3d69fd4f5c3649cbadcacd76ad179530be6921fd Mon Sep 17 00:00:00 2001 From: Rudo Kemper Date: Tue, 4 Mar 2025 12:16:02 -0500 Subject: [PATCH 04/12] Finish ArcGIS script and add e2e test --- f/connectors/arcgis/README.md | 4 +- f/connectors/arcgis/arcgis_feature_layer.py | 11 ++- .../arcgis/arcgis_feature_layer.script.lock | 3 +- .../arcgis/arcgis_feature_layer.script.yaml | 2 +- .../arcgis/tests/arcgis_feature_layer_test.py | 45 ++++++++++ .../arcgis/tests/assets/server_responses.py | 62 ++++++++++++++ .../arcgis/tests/assets/springfield_audio.mp4 | Bin 0 -> 920 bytes .../arcgis/tests/assets/springfield_photo.png | Bin 0 -> 3632 bytes f/connectors/arcgis/tests/conftest.py | 77 ++++++++++++++++++ .../arcgis/tests/requirements-test.txt | 3 + tox.ini | 2 +- 11 files changed, 203 insertions(+), 6 deletions(-) create mode 100644 f/connectors/arcgis/tests/assets/server_responses.py create mode 100644 f/connectors/arcgis/tests/assets/springfield_audio.mp4 create mode 100644 f/connectors/arcgis/tests/assets/springfield_photo.png diff --git a/f/connectors/arcgis/README.md b/f/connectors/arcgis/README.md index 2f68253..14d6ac5 100644 --- a/f/connectors/arcgis/README.md +++ b/f/connectors/arcgis/README.md @@ -1,6 +1,6 @@ -# `arcgis_feature_layer`: Fetch Feature Layer from ArcGIS REST API +# `arcgis_feature_layer`: Download Feature Layer from ArcGIS REST API -This script fetches the contents of an ArcGIS feature layer and stores it in a PostgreSQL database. +This script fetches the contents of an ArcGIS feature layer and stores it in a PostgreSQL database. Additionally, it downloads any attachments and saves them to a specified directory. Usage of this script requires you to have an ArcGIS account, in order to generate a token. diff --git a/f/connectors/arcgis/arcgis_feature_layer.py b/f/connectors/arcgis/arcgis_feature_layer.py index 7ae5ff0..a7a6c2a 100644 --- a/f/connectors/arcgis/arcgis_feature_layer.py +++ b/f/connectors/arcgis/arcgis_feature_layer.py @@ -1,4 +1,5 @@ # requirements: +# psycopg2-binary # requests~=2.32 import json @@ -9,6 +10,7 @@ import requests from f.common_logic.db_connection import postgresql +from f.connectors.geojson.geojson_to_postgres import main as save_geojson_to_postgres # type names that refer to Windmill Resources c_arcgis_account = dict @@ -41,6 +43,13 @@ def main( # At this point, the ArcGIS data is GeoJSON-compliant, and we don't need anything # from the REST API anymore. The data can therefore be handled further using the # existing GeoJSON connector. + save_geojson_to_postgres( + db, + db_table_name, + str(storage_path / "data.geojson"), + storage_path, + False, + ) def get_arcgis_token(arcgis_account: c_arcgis_account): @@ -172,7 +181,7 @@ def download_feature_attachments( attachment_content_type ) - attachment_path = Path(storage_path) / attachment_name + attachment_path = Path(storage_path) / "attachments" / attachment_name if attachment_path.exists(): logger.debug( diff --git a/f/connectors/arcgis/arcgis_feature_layer.script.lock b/f/connectors/arcgis/arcgis_feature_layer.script.lock index 266877f..711c818 100644 --- a/f/connectors/arcgis/arcgis_feature_layer.script.lock +++ b/f/connectors/arcgis/arcgis_feature_layer.script.lock @@ -2,4 +2,5 @@ certifi==2024.12.14 charset-normalizer==3.4.1 idna==3.10 requests==2.32.3 -urllib3==2.3.0 \ No newline at end of file +urllib3==2.3.0 +psycopg2-binary==2.9.10 \ No newline at end of file diff --git a/f/connectors/arcgis/arcgis_feature_layer.script.yaml b/f/connectors/arcgis/arcgis_feature_layer.script.yaml index e17f2e6..ce1288c 100644 --- a/f/connectors/arcgis/arcgis_feature_layer.script.yaml +++ b/f/connectors/arcgis/arcgis_feature_layer.script.yaml @@ -1,4 +1,4 @@ -summary: 'ArcGIS: Fetch Feature Layer' +summary: 'ArcGIS: Download Feature Layer' description: This script fetches the contents of an ArcGIS feature layer and stores it in a PostgreSQL database. lock: '!inline f/connectors/arcgis/arcgis_feature_layer.script.lock' concurrency_time_window_s: 0 diff --git a/f/connectors/arcgis/tests/arcgis_feature_layer_test.py b/f/connectors/arcgis/tests/arcgis_feature_layer_test.py index e69de29..24ae965 100644 --- a/f/connectors/arcgis/tests/arcgis_feature_layer_test.py +++ b/f/connectors/arcgis/tests/arcgis_feature_layer_test.py @@ -0,0 +1,45 @@ +import psycopg2 + +from f.connectors.arcgis.arcgis_feature_layer import ( + main, +) + + +def test_script_e2e(arcgis_server, pg_database, tmp_path): + asset_storage = tmp_path / "datalake" + + main( + arcgis_server.account, + arcgis_server.feature_layer_url, + pg_database, + "my_arcgis_data", + asset_storage, + ) + + # Attachments are saved to disk + assert ( + asset_storage / "my_arcgis_data" / "attachments" / "springfield_photo.png" + ).exists() + + with psycopg2.connect(**pg_database) as conn: + # Survey responses from arcgis_feature_layer are written to a SQL Table in expected format + with conn.cursor() as cursor: + cursor.execute("SELECT COUNT(*) FROM my_arcgis_data") + assert cursor.fetchone()[0] == 1 + + cursor.execute("SELECT * FROM my_arcgis_data LIMIT 0") + columns = [desc[0] for desc in cursor.description] + + assert "what_is_your_name" in columns + + assert "what_is_the_date_and_time" in columns + + assert "add_a_photo_filename" in columns + + assert "add_an_audio_content_type" in columns + + cursor.execute("SELECT g__type FROM my_arcgis_data LIMIT 1") + assert cursor.fetchone()[0] == "Point" + + cursor.execute("SELECT g__coordinates FROM my_arcgis_data LIMIT 1") + assert cursor.fetchone()[0] == "[-73.965355, 40.782865]" diff --git a/f/connectors/arcgis/tests/assets/server_responses.py b/f/connectors/arcgis/tests/assets/server_responses.py new file mode 100644 index 0000000..8a5e3d7 --- /dev/null +++ b/f/connectors/arcgis/tests/assets/server_responses.py @@ -0,0 +1,62 @@ +def arcgis_token(): + return { + "token": "token_value", + "expires": 1741109789251, + "ssl": True, + } + + +def arcgis_features(): + return { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": 1, + "geometry": { + "type": "Point", + "coordinates": [-73.965355, 40.782865], + }, + "properties": { + "objectid": 1, + "globalid": "12345678-1234-5678-1234-567812345678", + "CreationDate": 1741017108116, + "Creator": "arcgis_account", + "EditDate": 1741017108116, + "Editor": "arcgis_account", + "what_is_your_name": "Community mapper", + "what_is_your_community": "Springfield", + "what_is_your_community_other": None, + "what_is_the_date_and_time": 1741017060000, + "did_you_like_this_survey": 7, + }, + } + ], + } + + +def arcgis_attachments(): + return { + "attachmentInfos": [ + { + "id": 1, + "globalId": "ab12cd34-56ef-78gh-90ij-klmn12345678", + "parentGlobalId": "12345678-1234-5678-1234-567812345678", + "name": "springfield_photo.png", + "contentType": "image/png", + "size": 3632, + "keywords": "add_a_photo", + "exifInfo": None, + }, + { + "id": 2, + "globalId": "mnop5678-qrst-uvwx-yzab-cdef98765432", + "parentGlobalId": "12345678-1234-5678-1234-567812345678", + "name": "springfield_audio.mp4", + "contentType": "audio/webm;codecs=opus", + "size": 920, + "keywords": "add_an_audio", + "exifInfo": None, + }, + ] + } diff --git a/f/connectors/arcgis/tests/assets/springfield_audio.mp4 b/f/connectors/arcgis/tests/assets/springfield_audio.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..4fed7fa3a9ee86407efdf5c341f40b672d32e6c7 GIT binary patch literal 920 zcma)5%TC)s6uo2fl0X$mpdw@>SArFV2FgQ7T?C0mE5WLw3)GF1aUjJLTb@Y-tnxej z2itx@rS6dO7vUVofe<0Vk+1L5=iZs|h)B?IH`(d#Q7GaGbvLhlhjXaK_e;6IP z|DK#go%-J4*4Klr?Mg+wO7(+U4yWHMBL0R+(+&s|)49~pBk+63_e2SwXS{F_9`mcu zm9JSI7Io(G=nBc-?yBhNj}(?{?EM5)1jqWk(RVG@&VnI`6YEMQFpzhmoLT)fx)~uC z8a=-DjRlb}3upJmmz$S5$x9BgX}dMFsei)P-=mgJV%Df@(`+?h%vZxWHorI}w3G6hTFC?vG5*d|udpDnhh=;D(%z$A4h0d}U$mHHUDTi8P|&UV<} zcgv!6^|T})_h-n8y~s43JWaS8oJ;llcn7w2kwFhLqtn#d+C@m6o4C*{`!srRIP5&i gGBQ44vpM)X*6&=(F;;P_NJ3)Xtp@JaPI0#$0axp7^#A|> literal 0 HcmV?d00001 diff --git a/f/connectors/arcgis/tests/assets/springfield_photo.png b/f/connectors/arcgis/tests/assets/springfield_photo.png new file mode 100644 index 0000000000000000000000000000000000000000..7216c48971a9232c65152e3868666396f008a687 GIT binary patch literal 3632 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 Date: Tue, 4 Mar 2025 12:20:38 -0500 Subject: [PATCH 05/12] Clarify readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eb4e6a4..1a89d49 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Some of the tools available in the Guardian Connector Scripts Hub are: * Scripts to export data from a database into a specific format (e.g., GeoJSON). ![Available scripts, flows, and apps in gc-scripts-hub](gc-scripts-hub.jpg) -_A Windmill Workspace populated with the tools in this repository._ +_A Windmill Workspace populated with some of the tools in this repository._ ## Deploying the code to a Windmill workspace From 976646cf0b14d196c4759c3047ab5e24c69c4d80 Mon Sep 17 00:00:00 2001 From: Rudo Kemper Date: Tue, 4 Mar 2025 12:58:37 -0500 Subject: [PATCH 06/12] Revert meta.yaml changes --- f/apps/folder.meta.yaml | 8 +------- f/connectors/folder.meta.yaml | 10 ++-------- f/export/folder.meta.yaml | 8 +------- 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/f/apps/folder.meta.yaml b/f/apps/folder.meta.yaml index e502f13..8a289d1 100644 --- a/f/apps/folder.meta.yaml +++ b/f/apps/folder.meta.yaml @@ -1,8 +1,2 @@ summary: null -display_name: apps -extra_perms: - g/all: true - u/rudo: true -owners: - - u/rudo - - g/all +display_name: apps \ No newline at end of file diff --git a/f/connectors/folder.meta.yaml b/f/connectors/folder.meta.yaml index bc2fc27..903740b 100644 --- a/f/connectors/folder.meta.yaml +++ b/f/connectors/folder.meta.yaml @@ -1,8 +1,2 @@ -summary: '' -display_name: connectors -extra_perms: - g/all: true - u/rudo: true -owners: - - u/rudo - - g/all +summary: null +display_name: connectors \ No newline at end of file diff --git a/f/export/folder.meta.yaml b/f/export/folder.meta.yaml index 36cc724..c3792b3 100644 --- a/f/export/folder.meta.yaml +++ b/f/export/folder.meta.yaml @@ -1,8 +1,2 @@ summary: '' -display_name: export -extra_perms: - g/all: true - u/rudo: true -owners: - - u/rudo - - g/all +display_name: export \ No newline at end of file From 6217b7984ed11381d114b65e79d1ebf52c017120 Mon Sep 17 00:00:00 2001 From: Rudo Kemper Date: Tue, 4 Mar 2025 13:03:09 -0500 Subject: [PATCH 07/12] Add note about Survey123 --- f/connectors/arcgis/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/f/connectors/arcgis/README.md b/f/connectors/arcgis/README.md index 14d6ac5..1d7db66 100644 --- a/f/connectors/arcgis/README.md +++ b/f/connectors/arcgis/README.md @@ -1,6 +1,6 @@ # `arcgis_feature_layer`: Download Feature Layer from ArcGIS REST API -This script fetches the contents of an ArcGIS feature layer and stores it in a PostgreSQL database. Additionally, it downloads any attachments and saves them to a specified directory. +This script fetches the contents of an ArcGIS feature layer and stores it in a PostgreSQL database. Additionally, it downloads any attachments (e.g. from Survey123) and saves them to a specified directory. Usage of this script requires you to have an ArcGIS account, in order to generate a token. From a5475f29d15852de2969cdf93877ec280be4e835 Mon Sep 17 00:00:00 2001 From: Rudo Kemper Date: Tue, 4 Mar 2025 16:11:31 -0500 Subject: [PATCH 08/12] Add comment about not deleting the geojson file --- f/connectors/arcgis/arcgis_feature_layer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/f/connectors/arcgis/arcgis_feature_layer.py b/f/connectors/arcgis/arcgis_feature_layer.py index a7a6c2a..94180fa 100644 --- a/f/connectors/arcgis/arcgis_feature_layer.py +++ b/f/connectors/arcgis/arcgis_feature_layer.py @@ -48,7 +48,9 @@ def main( db_table_name, str(storage_path / "data.geojson"), storage_path, - False, + False, # to not delete the GeoJSON file after its contents are written to the database. + # Users might like to have access to the GeoJSON file directly, in addition to the data + # in the database. ) From a67b055cf12df1efef7e0a6ba8bebad5417cb354 Mon Sep 17 00:00:00 2001 From: Rudo Kemper Date: Tue, 4 Mar 2025 16:18:53 -0500 Subject: [PATCH 09/12] Add docstring to geojson script --- f/connectors/geojson/geojson_to_postgres.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/f/connectors/geojson/geojson_to_postgres.py b/f/connectors/geojson/geojson_to_postgres.py index 161bf59..10a546f 100644 --- a/f/connectors/geojson/geojson_to_postgres.py +++ b/f/connectors/geojson/geojson_to_postgres.py @@ -31,13 +31,28 @@ def main( def transform_geojson_data(geojson_path): + """ + Transforms GeoJSON data from a file into a list of dictionaries suitable for database insertion. + + Args: + geojson_path (str or Path): The file path to the GeoJSON file. + + Returns: + list: A list of dictionaries where each dictionary represents a GeoJSON feature with keys: + '_id' for the feature's unique identifier, + 'g__type' for the geometry type, + 'g__coordinates' for the geometry coordinates, + and any additional properties from the feature. + """ with open(geojson_path, "r") as f: geojson_data = json.load(f) transformed_geojson_data = [] for feature in geojson_data["features"]: transformed_feature = { - "_id": feature["id"], + "_id": feature[ + "id" + ], # Assuming that the GeoJSON feature has unique "id" field that can be used as the primary key "g__type": feature["geometry"]["type"], "g__coordinates": feature["geometry"]["coordinates"], **feature.get("properties", {}), @@ -48,7 +63,7 @@ def transform_geojson_data(geojson_path): class GeoJSONDbWriter: """ - Converts unstructured GeoJSON spatial data to structured SQL tables. + Converts GeoJSON spatial data to structured SQL tables. """ def __init__(self, db_connection_string, table_name): From c9051004d253aaa1e36fb1cf4707bc851231f118 Mon Sep 17 00:00:00 2001 From: Rudo Kemper Date: Tue, 4 Mar 2025 16:24:51 -0500 Subject: [PATCH 10/12] Minor edits to script yaml descriptions --- f/connectors/arcgis/arcgis_feature_layer.script.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/f/connectors/arcgis/arcgis_feature_layer.script.yaml b/f/connectors/arcgis/arcgis_feature_layer.script.yaml index ce1288c..2da78f5 100644 --- a/f/connectors/arcgis/arcgis_feature_layer.script.yaml +++ b/f/connectors/arcgis/arcgis_feature_layer.script.yaml @@ -22,9 +22,9 @@ schema: attachment_root: type: string description: >- - A path where ArcGIS attachments will be stored. Attachment files (e.g., - photos and audio) will be stored in the following directory schema: - `{attachment_root}/arcGIS/my_feature_layer/attachments/...` + A path where ArcGIS attachments (e.g., from Survey123) will be stored. Attachment + files like photo and audio will be stored in the following directory schema: + `{attachment_root}/{db_table_name}/attachments/...` default: /persistent-storage/datalake originalType: string db: From 62b5637df702b006b8c00d159bccacddaef07ca6 Mon Sep 17 00:00:00 2001 From: Rudo Kemper Date: Tue, 4 Mar 2025 16:29:39 -0500 Subject: [PATCH 11/12] Add comment about client param for token generation --- f/connectors/arcgis/arcgis_feature_layer.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/f/connectors/arcgis/arcgis_feature_layer.py b/f/connectors/arcgis/arcgis_feature_layer.py index 94180fa..d389e8b 100644 --- a/f/connectors/arcgis/arcgis_feature_layer.py +++ b/f/connectors/arcgis/arcgis_feature_layer.py @@ -71,6 +71,11 @@ def get_arcgis_token(arcgis_account: c_arcgis_account): arcgis_username = arcgis_account["username"] arcgis_password = arcgis_account["password"] + # According to the ArcGIS REST API documentation, you can set `client to `requestip` + # to generate a token based on the IP address of the request. However, this does not + # seem to work well, neither in local development nor in production. Therefore, we use + # `referer` as the client type, and use the base URL of the Windmill app as the referer. + # https://developers.arcgis.com/rest/services-reference/enterprise/generate-token/ token_response = requests.post( "https://www.arcgis.com/sharing/rest/generateToken", data={ From ccb814d33d409728128492a97ebfa9417183e6f3 Mon Sep 17 00:00:00 2001 From: Rudo Kemper Date: Fri, 11 Apr 2025 13:56:10 -0400 Subject: [PATCH 12/12] Add notes to our approach to readme --- f/connectors/geojson/README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/f/connectors/geojson/README.md b/f/connectors/geojson/README.md index 122ec3e..a224bc0 100644 --- a/f/connectors/geojson/README.md +++ b/f/connectors/geojson/README.md @@ -1,3 +1,20 @@ -# `geojson_to_postgres`: Upload a GeoJSON file to the data warehouse +# `geojson_to_postgres`: Import a GeoJSON file into a PostgreSQL table -This script imports data from a GeoJSON file into a database table. It reads a file containing spatial data, transforms the data into a structured format, and inserts it into a PostgreSQL database table. Optionally, it then delete the export file. \ No newline at end of file +This script reads a GeoJSON file and inserts its contents into a PostgreSQL table, flattening all data into TEXT columns. + +### Behavior + +* Each feature's `geometry` object is decomposed into separate TEXT columns, prefixed with `g__`. + + Example: `geometry.type` → `g__type`, `geometry.coordinates` → `g__coordinates` + +* Each properties field is inserted as-is into a column matching the property name. + + Example: `properties.category` → `category` + +* The feature's top-level `id` (if present) is used as the primary key `_id`. + +### Notes +* The data is inserted as flat text fields — no geometry types or JSONB columns are used. +* PostGIS is _not_ used at this stage. This approach may change based on requirements downstream. +* Optionally, the input file is deleted after import. \ No newline at end of file