mirror of
https://github.com/dbt-labs/dbt-core
synced 2025-12-17 19:31:34 +00:00
Compare commits
2354 Commits
v0.15.1
...
readme_doc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
845b95f3d0 | ||
|
|
13b18654f0 | ||
|
|
aafa1c7f47 | ||
|
|
638e3ad299 | ||
|
|
d9cfeb1ea3 | ||
|
|
e6786a2bc3 | ||
|
|
13571435a3 | ||
|
|
efb890db2d | ||
|
|
f3735187a6 | ||
|
|
3032594b26 | ||
|
|
1df7a029b4 | ||
|
|
f467fba151 | ||
|
|
8791313ec5 | ||
|
|
7798f932a0 | ||
|
|
a588607ec6 | ||
|
|
348764d99d | ||
|
|
5aeb088a73 | ||
|
|
e943b9fc84 | ||
|
|
892426eecb | ||
|
|
1d25b2b046 | ||
|
|
da70840be8 | ||
|
|
7632782ecd | ||
|
|
6fae647097 | ||
|
|
fc8b8c11d5 | ||
|
|
26a7922a34 | ||
|
|
c18b4f1f1a | ||
|
|
fa31a67499 | ||
|
|
742cd990ee | ||
|
|
8463af35c3 | ||
|
|
b34a4ab493 | ||
|
|
417ccdc3b4 | ||
|
|
7c46b784ef | ||
|
|
067b861d30 | ||
|
|
9f6ed3cec3 | ||
|
|
43edc887f9 | ||
|
|
6d4c64a436 | ||
|
|
0ed14fa236 | ||
|
|
51f2daf4b0 | ||
|
|
76f7bf9900 | ||
|
|
3fc715f066 | ||
|
|
b6811da84f | ||
|
|
1dffccd9da | ||
|
|
9ed9936c84 | ||
|
|
e75ae8c754 | ||
|
|
b68535b8cb | ||
|
|
5310498647 | ||
|
|
22b1a09aa2 | ||
|
|
6855fe06a7 | ||
|
|
affd8619c2 | ||
|
|
b67d5f396b | ||
|
|
b3039fdc76 | ||
|
|
9bdf5fe74a | ||
|
|
c675c2d318 | ||
|
|
2cd1f7d98e | ||
|
|
ce9ac8ea10 | ||
|
|
b90ab74975 | ||
|
|
6d3c3f1995 | ||
|
|
74fbaa18cd | ||
|
|
fc7c073691 | ||
|
|
29f504e201 | ||
|
|
eeb490ed15 | ||
|
|
c220b1e42c | ||
|
|
d973ae9ec6 | ||
|
|
f461683df5 | ||
|
|
41ed976941 | ||
|
|
e93ad5f118 | ||
|
|
d75ed964f8 | ||
|
|
284ac9b138 | ||
|
|
7448ec5adb | ||
|
|
caa6269bc7 | ||
|
|
31691c3b88 | ||
|
|
3a904a811f | ||
|
|
b927a31a53 | ||
|
|
d8dd75320c | ||
|
|
a613556246 | ||
|
|
8d2351d541 | ||
|
|
f72b603196 | ||
|
|
4eb17b57fb | ||
|
|
85a4b87267 | ||
|
|
0d320c58da | ||
|
|
ed1ff2caac | ||
|
|
d80646c258 | ||
|
|
a9b4316346 | ||
|
|
36776b96e7 | ||
|
|
7f2d3cd24f | ||
|
|
d046ae0606 | ||
|
|
e8c267275e | ||
|
|
a4951749a8 | ||
|
|
e1a2e8d9f5 | ||
|
|
f80c78e3a5 | ||
|
|
c541eca592 | ||
|
|
726aba0586 | ||
|
|
d300addee1 | ||
|
|
d5d16f01f4 | ||
|
|
2cb26e2699 | ||
|
|
b4793b4f9b | ||
|
|
045e70ccf1 | ||
|
|
ba23395c8e | ||
|
|
0aacd99168 | ||
|
|
e4b5d73dc4 | ||
|
|
bd950f687a | ||
|
|
aea23a488b | ||
|
|
22731df07b | ||
|
|
c55be164e6 | ||
|
|
9d73304c1a | ||
|
|
719b2026ab | ||
|
|
22416980d1 | ||
|
|
3d28b6704c | ||
|
|
5d1b104e1f | ||
|
|
4a8a68049d | ||
|
|
4b7fd1d46a | ||
|
|
0722922c03 | ||
|
|
40321d7966 | ||
|
|
434f3d2678 | ||
|
|
6dd9c2c5ba | ||
|
|
5e6be1660e | ||
|
|
31acb95d7a | ||
|
|
683190b711 | ||
|
|
ebb84c404f | ||
|
|
2ca6ce688b | ||
|
|
a40550b89d | ||
|
|
b2aea11cdb | ||
|
|
43b39fd1aa | ||
|
|
5cc8626e96 | ||
|
|
f95e9efbc0 | ||
|
|
25c974af8c | ||
|
|
b5c6f09a9e | ||
|
|
bd3e623240 | ||
|
|
63343653a9 | ||
|
|
d8b97c1077 | ||
|
|
e0b0edaeed | ||
|
|
3cafc9e13f | ||
|
|
13f31aed90 | ||
|
|
d513491046 | ||
|
|
281d2491a5 | ||
|
|
9857e1dd83 | ||
|
|
6b36b18029 | ||
|
|
d8868c5197 | ||
|
|
b141620125 | ||
|
|
51d8440dd4 | ||
|
|
5b2562a919 | ||
|
|
44a9da621e | ||
|
|
69aa6bf964 | ||
|
|
f9ef9da110 | ||
|
|
57ae9180c2 | ||
|
|
efe926d20c | ||
|
|
1081b8e720 | ||
|
|
8205921c4b | ||
|
|
da6c211611 | ||
|
|
354c1e0d4d | ||
|
|
855419d698 | ||
|
|
e94fd61b24 | ||
|
|
4cf9b73c3d | ||
|
|
8442fb66a5 | ||
|
|
f8cefa3eff | ||
|
|
d83e0fb8d8 | ||
|
|
3e9da06365 | ||
|
|
bda70c988e | ||
|
|
229e897070 | ||
|
|
f20e83a32b | ||
|
|
dd84f9a896 | ||
|
|
e6df4266f6 | ||
|
|
b591e1a2b7 | ||
|
|
3dab058c73 | ||
|
|
c7bc6eb812 | ||
|
|
c690ecc1fd | ||
|
|
73e272f06e | ||
|
|
95d087b51b | ||
|
|
40ae6b6bc8 | ||
|
|
fe20534a98 | ||
|
|
dd7af477ac | ||
|
|
178f74b753 | ||
|
|
a14f563ec8 | ||
|
|
ff109e1806 | ||
|
|
5e46694b68 | ||
|
|
73af9a56e5 | ||
|
|
d2aa920275 | ||
|
|
c34f3530c8 | ||
|
|
c019a94206 | ||
|
|
f9bdfa050b | ||
|
|
1b35d1aa21 | ||
|
|
420ef9cc7b | ||
|
|
02fdc2cb9f | ||
|
|
f82745fb0c | ||
|
|
3397bdc6a5 | ||
|
|
96e858ac0b | ||
|
|
f6a98b5674 | ||
|
|
824f0bf2c0 | ||
|
|
5648b1c622 | ||
|
|
bb1382e576 | ||
|
|
085ea9181f | ||
|
|
eace5b77a7 | ||
|
|
1c61bb18e6 | ||
|
|
f79a968a09 | ||
|
|
34c23fe650 | ||
|
|
3ae9475655 | ||
|
|
11436fed45 | ||
|
|
21a7b71657 | ||
|
|
280e9ad9c9 | ||
|
|
97f31c88e1 | ||
|
|
5f483a6b13 | ||
|
|
86f24e13db | ||
|
|
4bda8c8880 | ||
|
|
80a5d27127 | ||
|
|
4307a82058 | ||
|
|
5c01c42308 | ||
|
|
80ba71682b | ||
|
|
26625e9627 | ||
|
|
134e8423b7 | ||
|
|
04a9195297 | ||
|
|
8a10a69f59 | ||
|
|
fd7c95d1d2 | ||
|
|
79aa136301 | ||
|
|
3b5cec6cc6 | ||
|
|
0e9a67956d | ||
|
|
f9f0eab0b7 | ||
|
|
ed01b439cf | ||
|
|
a398ed1a3e | ||
|
|
a818e6551b | ||
|
|
0a7471ebdc | ||
|
|
1a5bc83598 | ||
|
|
6e2df00648 | ||
|
|
b338dfc99a | ||
|
|
47033c459f | ||
|
|
92a0930634 | ||
|
|
cad1a48eb0 | ||
|
|
b451f87e3c | ||
|
|
20756290bc | ||
|
|
f44c6ed136 | ||
|
|
b501f4317c | ||
|
|
91b43f71bb | ||
|
|
6fc64f0d3b | ||
|
|
ee5c697645 | ||
|
|
3caec08ccb | ||
|
|
f7680379fc | ||
|
|
3789acc5a7 | ||
|
|
8ae232abe8 | ||
|
|
332d23c5eb | ||
|
|
5799973474 | ||
|
|
3d816d56ec | ||
|
|
111f3c28f8 | ||
|
|
10aded793c | ||
|
|
b5cc7b8dff | ||
|
|
449f042742 | ||
|
|
66b70e025b | ||
|
|
578c6d6a20 | ||
|
|
64ce9d6aa4 | ||
|
|
213ddedb85 | ||
|
|
c96201c060 | ||
|
|
16b02f4f55 | ||
|
|
e0d2b02d46 | ||
|
|
65e76df6ec | ||
|
|
052a3060d4 | ||
|
|
b65ae1ddde | ||
|
|
a8246ab1f1 | ||
|
|
6854e67464 | ||
|
|
ca7c1fc4ad | ||
|
|
5dbc945f23 | ||
|
|
655ff85dc9 | ||
|
|
6a2ceaa073 | ||
|
|
e8fb29d185 | ||
|
|
8443142f27 | ||
|
|
7ebe21dccb | ||
|
|
c25b7a1143 | ||
|
|
38eb46dfc3 | ||
|
|
fe9ed9ccdd | ||
|
|
ff4e5219b1 | ||
|
|
04632a008f | ||
|
|
6925cebcf6 | ||
|
|
571beb13d9 | ||
|
|
69cd82f483 | ||
|
|
11e379280f | ||
|
|
0018eb7db6 | ||
|
|
154a682180 | ||
|
|
1b79a245e6 | ||
|
|
6b590122c7 | ||
|
|
d5f632e6fd | ||
|
|
2fc8e5e0b6 | ||
|
|
5ab07273ba | ||
|
|
19c9e5bfdf | ||
|
|
60794367a5 | ||
|
|
ea07729bbf | ||
|
|
c4370773f6 | ||
|
|
fda17b456e | ||
|
|
bc3e1a0a71 | ||
|
|
a06988706c | ||
|
|
ce73124bbf | ||
|
|
352c62f3c3 | ||
|
|
81a51d3942 | ||
|
|
64fc3a39a7 | ||
|
|
e5b6f4f293 | ||
|
|
d26e63ed9a | ||
|
|
f4f5d31959 | ||
|
|
e7e12075b9 | ||
|
|
74dda5aa19 | ||
|
|
092e96ce70 | ||
|
|
18102027ba | ||
|
|
f80825d63e | ||
|
|
9316e47b77 | ||
|
|
f99cf1218a | ||
|
|
5871915ce9 | ||
|
|
5ce290043f | ||
|
|
080d27321b | ||
|
|
1d0936bd14 | ||
|
|
706b8ca9df | ||
|
|
7dc491b7ba | ||
|
|
779c789a64 | ||
|
|
409b4ba109 | ||
|
|
59d131d3ac | ||
|
|
6563d09ba7 | ||
|
|
05dea18b62 | ||
|
|
d7177c7d89 | ||
|
|
35f0fea804 | ||
|
|
8953c7c533 | ||
|
|
76c59a5545 | ||
|
|
237048c7ac | ||
|
|
30ff395b7b | ||
|
|
5c0a31b829 | ||
|
|
243bc3d41d | ||
|
|
67b594a950 | ||
|
|
2493c21649 | ||
|
|
d3826e670f | ||
|
|
4b5b1696b7 | ||
|
|
abb59ef14f | ||
|
|
3b7c2816b9 | ||
|
|
484517416f | ||
|
|
39447055d3 | ||
|
|
95cca277c9 | ||
|
|
96083dcaf5 | ||
|
|
75b4cf691b | ||
|
|
7c9171b00b | ||
|
|
3effade266 | ||
|
|
44e7390526 | ||
|
|
c141798abc | ||
|
|
df7ec3fb37 | ||
|
|
90e5507d03 | ||
|
|
332d3494b3 | ||
|
|
6393f5a5d7 | ||
|
|
ce97a9ca7a | ||
|
|
9af071bfe4 | ||
|
|
45a41202f3 | ||
|
|
9768999ca1 | ||
|
|
fc0d11c0a5 | ||
|
|
e6344205bb | ||
|
|
9d7a6556ef | ||
|
|
15f4add0b8 | ||
|
|
464becacd0 | ||
|
|
51a76d0d63 | ||
|
|
052e54d43a | ||
|
|
9e796671dd | ||
|
|
a9a6254f52 | ||
|
|
8b3a09c7ae | ||
|
|
6aa4d812d4 | ||
|
|
07fa719fb0 | ||
|
|
650b34ae24 | ||
|
|
0a935855f3 | ||
|
|
d500aae4dc | ||
|
|
370d3e746d | ||
|
|
ab06149c81 | ||
|
|
e72895c7c9 | ||
|
|
fe4a67daa4 | ||
|
|
09ea989d81 | ||
|
|
7fa14b6948 | ||
|
|
d4974cd35c | ||
|
|
459178811b | ||
|
|
b37f6a010e | ||
|
|
e817164d31 | ||
|
|
09ce43edbf | ||
|
|
2980cd17df | ||
|
|
8c804de643 | ||
|
|
c8241b87e6 | ||
|
|
f204d24ed8 | ||
|
|
d5461ccd8b | ||
|
|
a20d2d93d3 | ||
|
|
57e1eec165 | ||
|
|
d2dbe6afe4 | ||
|
|
72eb163223 | ||
|
|
af16c74c3a | ||
|
|
664f6584b9 | ||
|
|
76fd3bdf8c | ||
|
|
b633adb881 | ||
|
|
b6e534cdd0 | ||
|
|
1dc4adb86f | ||
|
|
0a4d7c4831 | ||
|
|
ad67e55d74 | ||
|
|
2fae64a488 | ||
|
|
1a984601ee | ||
|
|
454168204c | ||
|
|
43642956a2 | ||
|
|
1fe53750fa | ||
|
|
8609c02383 | ||
|
|
355b0c496e | ||
|
|
cd6894acf4 | ||
|
|
b90b3a9c19 | ||
|
|
e7b8488be8 | ||
|
|
06cc0c57e8 | ||
|
|
87072707ed | ||
|
|
ef63319733 | ||
|
|
2068dd5510 | ||
|
|
3e1e171c66 | ||
|
|
5f9ed1a83c | ||
|
|
3d9e54d970 | ||
|
|
52a0fdef6c | ||
|
|
d9b02fb0a0 | ||
|
|
6c8de62b24 | ||
|
|
2d3d1b030a | ||
|
|
88acf0727b | ||
|
|
02839ec779 | ||
|
|
44a8f6a3bf | ||
|
|
751ea92576 | ||
|
|
02007b3619 | ||
|
|
fe0b9e7ef5 | ||
|
|
4b1c6b51f9 | ||
|
|
0b4689f311 | ||
|
|
b77eff8f6f | ||
|
|
2782a33ecf | ||
|
|
94c6cf1b3c | ||
|
|
3c8daacd3e | ||
|
|
2f9907b072 | ||
|
|
287c4d2b03 | ||
|
|
ba9d76b3f9 | ||
|
|
0efaaf7daf | ||
|
|
9ae7d68260 | ||
|
|
486afa9fcd | ||
|
|
1f189f5225 | ||
|
|
580b1fdd68 | ||
|
|
bad0198a36 | ||
|
|
252280b56e | ||
|
|
64bf9c8885 | ||
|
|
935c138736 | ||
|
|
5891b59790 | ||
|
|
4e020c3878 | ||
|
|
3004969a93 | ||
|
|
873e9714f8 | ||
|
|
fe24dd43d4 | ||
|
|
ed91ded2c1 | ||
|
|
757614d57f | ||
|
|
faff8c00b3 | ||
|
|
45fe76eef4 | ||
|
|
80244a09fe | ||
|
|
ea772ae419 | ||
|
|
c68fca7937 | ||
|
|
37e86257f5 | ||
|
|
c182c05c2f | ||
|
|
b02875a12b | ||
|
|
03332b2955 | ||
|
|
f1f99a2371 | ||
|
|
95116dbb5b | ||
|
|
868fd64adf | ||
|
|
2f7ab2d038 | ||
|
|
159e79ee6b | ||
|
|
3d4a82cca2 | ||
|
|
6ba837d73d | ||
|
|
f4775d7673 | ||
|
|
429396aa02 | ||
|
|
8a5e9b71a5 | ||
|
|
fa78102eaf | ||
|
|
5466d474c5 | ||
|
|
80951ae973 | ||
|
|
d5662ef34c | ||
|
|
57783bb5f6 | ||
|
|
d73ee588e5 | ||
|
|
40089d710b | ||
|
|
6ec61950eb | ||
|
|
72c831a80a | ||
|
|
929931a26a | ||
|
|
577e2438c1 | ||
|
|
2679792199 | ||
|
|
2adf982991 | ||
|
|
1fb4a7f428 | ||
|
|
30e72bc5e2 | ||
|
|
35645a7233 | ||
|
|
d583c8d737 | ||
|
|
a83f00c594 | ||
|
|
45bb955b55 | ||
|
|
c448702c1b | ||
|
|
558a6a03ac | ||
|
|
52ec7907d3 | ||
|
|
792f39a888 | ||
|
|
16264f58c1 | ||
|
|
2317c0c3c8 | ||
|
|
3c09ab9736 | ||
|
|
f10dc0e1b3 | ||
|
|
634bc41d8a | ||
|
|
d7ea3648c6 | ||
|
|
e5c8e19ff2 | ||
|
|
4ddba7e44c | ||
|
|
37b31d10c8 | ||
|
|
93cf1f085f | ||
|
|
a84f824a44 | ||
|
|
9c58f3465b | ||
|
|
0e3778132b | ||
|
|
72722635f2 | ||
|
|
a4c7c7fc55 | ||
|
|
2bad73eead | ||
|
|
c8bc25d11a | ||
|
|
4c06689ff5 | ||
|
|
a45c9d0192 | ||
|
|
34e2c4f90b | ||
|
|
c0e2023c81 | ||
|
|
108b55bdc3 | ||
|
|
a29367b7fe | ||
|
|
1d7e8349ed | ||
|
|
67c194dcd1 | ||
|
|
75d3d87d64 | ||
|
|
4ff3f6d4e8 | ||
|
|
d0773f3346 | ||
|
|
ee58d27d94 | ||
|
|
9e3da391a7 | ||
|
|
bd7010678a | ||
|
|
9f716b31b3 | ||
|
|
3dd486d8fa | ||
|
|
33217891ca | ||
|
|
1d37c4e555 | ||
|
|
9f62ec2153 | ||
|
|
372eca76b8 | ||
|
|
e3cb050bbc | ||
|
|
0ae93c7f54 | ||
|
|
1f6386d760 | ||
|
|
66eb3964e2 | ||
|
|
f460d275ba | ||
|
|
fb91bad800 | ||
|
|
eaec22ae53 | ||
|
|
b7c1768cca | ||
|
|
387b26a202 | ||
|
|
8a1e6438f1 | ||
|
|
aaac5ff2e6 | ||
|
|
4dc29630b5 | ||
|
|
f716631439 | ||
|
|
648a780850 | ||
|
|
de0919ff88 | ||
|
|
8b1ea5fb6c | ||
|
|
85627aafcd | ||
|
|
49065158f5 | ||
|
|
bdb3049218 | ||
|
|
e10d1b0f86 | ||
|
|
83b98c8ebf | ||
|
|
b9d5123aa3 | ||
|
|
c09300bfd2 | ||
|
|
fc490cee7b | ||
|
|
3baa3d7fe8 | ||
|
|
764c7c0fdc | ||
|
|
c97ebbbf35 | ||
|
|
85fe32bd08 | ||
|
|
eba3fd2255 | ||
|
|
e2f2c07873 | ||
|
|
70850cd362 | ||
|
|
16992e6391 | ||
|
|
fd0d95140e | ||
|
|
ac65fcd557 | ||
|
|
4d246567b9 | ||
|
|
1ad1c834f3 | ||
|
|
41610b822c | ||
|
|
c794600242 | ||
|
|
9d414f6ec3 | ||
|
|
552e831306 | ||
|
|
c712c96a0b | ||
|
|
eb46bfc3d6 | ||
|
|
f52537b606 | ||
|
|
762419d2fe | ||
|
|
4feb7cb15b | ||
|
|
eb47b85148 | ||
|
|
9faa019a07 | ||
|
|
9589dc91fa | ||
|
|
14507a283e | ||
|
|
af0fe120ec | ||
|
|
16501ec1c6 | ||
|
|
bf867f6aff | ||
|
|
eb4ad4444f | ||
|
|
8fdba17ac6 | ||
|
|
abe8e83945 | ||
|
|
02cbae1f9f | ||
|
|
65908b395f | ||
|
|
4971395d5d | ||
|
|
eeec2038aa | ||
|
|
4fac086556 | ||
|
|
8818061d59 | ||
|
|
b195778eb9 | ||
|
|
de1763618a | ||
|
|
7485066ed4 | ||
|
|
15ce956380 | ||
|
|
e5c63884e2 | ||
|
|
9fef62d83e | ||
|
|
7563b997c2 | ||
|
|
291ff3600b | ||
|
|
2c405304ee | ||
|
|
1e5a7878e5 | ||
|
|
d89e1d7f85 | ||
|
|
98c015b775 | ||
|
|
a56502688f | ||
|
|
c0d757ab19 | ||
|
|
e68fd6eb7f | ||
|
|
90edc38859 | ||
|
|
0f018ea5dd | ||
|
|
1be6254363 | ||
|
|
760af71ed2 | ||
|
|
82f5e9f5b2 | ||
|
|
988c187db3 | ||
|
|
b23129982c | ||
|
|
4d5d0e2150 | ||
|
|
c0c487bf77 | ||
|
|
835d805079 | ||
|
|
c2a767184c | ||
|
|
1e7c8802eb | ||
|
|
a76ec42586 | ||
|
|
7418f36932 | ||
|
|
f9ef5e7e8e | ||
|
|
dbfa351395 | ||
|
|
e775f2b38e | ||
|
|
6f27454be4 | ||
|
|
201723d506 | ||
|
|
17555faaca | ||
|
|
36e0ab9f42 | ||
|
|
6017bd6cba | ||
|
|
30fed8d421 | ||
|
|
8ac5cdd2e1 | ||
|
|
114ac0793a | ||
|
|
d0b750461a | ||
|
|
9693170eb9 | ||
|
|
bbab6c2361 | ||
|
|
cfe3636c78 | ||
|
|
aadf3c702e | ||
|
|
1eac726a07 | ||
|
|
85e2c89794 | ||
|
|
fffcd3b404 | ||
|
|
fbfef4b1a3 | ||
|
|
526a6c0d0c | ||
|
|
1f33b6a74a | ||
|
|
95fc6d43e7 | ||
|
|
d8c261ffcf | ||
|
|
66ea0a9e0f | ||
|
|
435b542e7b | ||
|
|
10cd06f515 | ||
|
|
9da1868c3b | ||
|
|
2649fac4a4 | ||
|
|
6e05226e3b | ||
|
|
c1c3397f66 | ||
|
|
2065db2383 | ||
|
|
08fb868b63 | ||
|
|
8d39ef16b6 | ||
|
|
66c5082aa7 | ||
|
|
26fb58bd1b | ||
|
|
fed8826043 | ||
|
|
9af78a3249 | ||
|
|
bf1ad6cd17 | ||
|
|
15e995f2f5 | ||
|
|
b3e73b0de8 | ||
|
|
dd2633dfcb | ||
|
|
29f0278451 | ||
|
|
f0f98be692 | ||
|
|
5956a64b01 | ||
|
|
5fb36e3e2a | ||
|
|
9d295a1d91 | ||
|
|
39f350fe89 | ||
|
|
8c55e744b8 | ||
|
|
a260d4e25b | ||
|
|
509797588f | ||
|
|
2eed20f1f3 | ||
|
|
1d7b4c0db2 | ||
|
|
ac8cd788cb | ||
|
|
33dc970859 | ||
|
|
f73202734c | ||
|
|
32bacdab4b | ||
|
|
6113c3b533 | ||
|
|
1c634af489 | ||
|
|
428cdea2dc | ||
|
|
f14b55f839 | ||
|
|
5934d263b8 | ||
|
|
3860d919e6 | ||
|
|
fd0b9434ae | ||
|
|
efb30d0262 | ||
|
|
cee0bfbfa2 | ||
|
|
dc684d31d3 | ||
|
|
bfdf7f01b5 | ||
|
|
2cc0579b6e | ||
|
|
bfc472dc0f | ||
|
|
ea4e3680ab | ||
|
|
f02139956d | ||
|
|
cacbd1c212 | ||
|
|
3f78bb7819 | ||
|
|
aa65b01fe3 | ||
|
|
4f0968d678 | ||
|
|
118973cf79 | ||
|
|
df7cc0521f | ||
|
|
40c02d2cc9 | ||
|
|
be70b1a0c1 | ||
|
|
7ec5c122e1 | ||
|
|
a10ab99efc | ||
|
|
9f4398c557 | ||
|
|
d60f6bc89b | ||
|
|
617eeb4ff7 | ||
|
|
5b55825638 | ||
|
|
103d524db5 | ||
|
|
babd084a9b | ||
|
|
749f87397e | ||
|
|
307d47ebaf | ||
|
|
6acd4b91c1 | ||
|
|
f4a9530894 | ||
|
|
ab65385a16 | ||
|
|
ebd761e3dc | ||
|
|
3b942ec790 | ||
|
|
b373486908 | ||
|
|
c8cd5502f6 | ||
|
|
d6dd968c4f | ||
|
|
b8d73d2197 | ||
|
|
17e57f1e0b | ||
|
|
e21bf9fbc7 | ||
|
|
12e281f076 | ||
|
|
a5ce658755 | ||
|
|
ce30dfa82d | ||
|
|
c04d1e9d5c | ||
|
|
80031d122c | ||
|
|
943b090c90 | ||
|
|
39fd53d1f9 | ||
|
|
777e7b3b6d | ||
|
|
2783fe2a9f | ||
|
|
f5880cb001 | ||
|
|
26e501008a | ||
|
|
2c67e3f5c7 | ||
|
|
033596021d | ||
|
|
f36c72e085 | ||
|
|
fefaf7b4be | ||
|
|
91431401ad | ||
|
|
59d96c08a1 | ||
|
|
f10447395b | ||
|
|
c2b6222798 | ||
|
|
3a58c49184 | ||
|
|
440a5e49e2 | ||
|
|
77c10713a3 | ||
|
|
48e367ce2f | ||
|
|
934c23bf39 | ||
|
|
e0febcb6c3 | ||
|
|
044a6c6ea4 | ||
|
|
8ebbc10572 | ||
|
|
7435828082 | ||
|
|
369b595e8a | ||
|
|
9a6d30f03d | ||
|
|
6bdd01d52b | ||
|
|
bae9767498 | ||
|
|
b0e50dedb8 | ||
|
|
96bfb3b259 | ||
|
|
909068dfa8 | ||
|
|
f4c74968be | ||
|
|
0e958f3704 | ||
|
|
a8b2942f93 | ||
|
|
564fe62400 | ||
|
|
5c5013191b | ||
|
|
31989b85d1 | ||
|
|
5ed4af2372 | ||
|
|
4d18e391aa | ||
|
|
2feeb5b927 | ||
|
|
2853f07875 | ||
|
|
4e6adc07a1 | ||
|
|
6a5ed4f418 | ||
|
|
ef25698d3d | ||
|
|
429dcc7000 | ||
|
|
ab3f994626 | ||
|
|
5f8235fcfc | ||
|
|
db325d0fde | ||
|
|
8dc1f49ac7 | ||
|
|
9fe2b651ed | ||
|
|
24e4b75c35 | ||
|
|
34174abf26 | ||
|
|
af778312cb | ||
|
|
280f5614ef | ||
|
|
8566a46793 | ||
|
|
af3c3f4cbe | ||
|
|
034a44e625 | ||
|
|
84155fdff7 | ||
|
|
8255c913a3 | ||
|
|
4d4d17669b | ||
|
|
540a0422f5 | ||
|
|
de4d7d6273 | ||
|
|
1345d95589 | ||
|
|
a5bc19dd69 | ||
|
|
25b143c8cc | ||
|
|
82cca959e4 | ||
|
|
d52374a0b6 | ||
|
|
c71a18ca07 | ||
|
|
8d73ae2cc0 | ||
|
|
7b0c74ca3e | ||
|
|
62be9f9064 | ||
|
|
2fdc113d93 | ||
|
|
b70fb543f5 | ||
|
|
31c88f9f5a | ||
|
|
af3a818f12 | ||
|
|
a07532d4c7 | ||
|
|
fb449ca4bc | ||
|
|
4da65643c0 | ||
|
|
bf64db474c | ||
|
|
344a14416d | ||
|
|
be47a0c5db | ||
|
|
808b980301 | ||
|
|
3528480562 | ||
|
|
6bd263d23f | ||
|
|
2b9aa3864b | ||
|
|
81155caf88 | ||
|
|
c7c057483d | ||
|
|
7f5170ae4d | ||
|
|
49b8693b11 | ||
|
|
d7b0a14eb5 | ||
|
|
8996cb1e18 | ||
|
|
38f278cce0 | ||
|
|
bb4e475044 | ||
|
|
4fbe36a8e9 | ||
|
|
a1a40b562a | ||
|
|
3a4a1bb005 | ||
|
|
4f8c10c1aa | ||
|
|
4833348769 | ||
|
|
ad07d59a78 | ||
|
|
e8aaabd1d3 | ||
|
|
d7d7396eeb | ||
|
|
41538860cd | ||
|
|
5c9f8a0cf0 | ||
|
|
11c997c3e9 | ||
|
|
1b1184a5e1 | ||
|
|
4ffcc43ed9 | ||
|
|
4ccaac46a6 | ||
|
|
ba88b84055 | ||
|
|
9086634c8f | ||
|
|
e88f1f1edb | ||
|
|
13c7486f0e | ||
|
|
8e811ba141 | ||
|
|
c5d86afed6 | ||
|
|
43a0cfbee1 | ||
|
|
8567d5f302 | ||
|
|
36d1bddc5b | ||
|
|
bf992680af | ||
|
|
e064298dfc | ||
|
|
e01a10ced5 | ||
|
|
2aa10fb1ed | ||
|
|
66f442ad76 | ||
|
|
11f1ecebcf | ||
|
|
e339cb27f6 | ||
|
|
bce3232b39 | ||
|
|
b08970ce39 | ||
|
|
533f88ceaf | ||
|
|
c8f0469a44 | ||
|
|
a1fc24e532 | ||
|
|
d80daa48df | ||
|
|
92aae2803f | ||
|
|
77cbbbfaf2 | ||
|
|
6c6649f912 | ||
|
|
55fbaabfda | ||
|
|
56c2518936 | ||
|
|
2b48152da6 | ||
|
|
e743e23d6b | ||
|
|
f846f921f2 | ||
|
|
e52a599be6 | ||
|
|
99744bd318 | ||
|
|
1060035838 | ||
|
|
69cc20013e | ||
|
|
3572bfd37d | ||
|
|
a6b82990f5 | ||
|
|
540c1fd9c6 | ||
|
|
46d36cd412 | ||
|
|
a170764fc5 | ||
|
|
f72873a1ce | ||
|
|
82496c30b1 | ||
|
|
cb3c007acd | ||
|
|
cb460a797c | ||
|
|
1b666d01cf | ||
|
|
df24c7d2f8 | ||
|
|
133c15c0e2 | ||
|
|
116e18a19e | ||
|
|
ec0af7c97b | ||
|
|
a34a877737 | ||
|
|
f018794465 | ||
|
|
d45f5e9791 | ||
|
|
04bd0d834c | ||
|
|
ed4f0c4713 | ||
|
|
c747068d4a | ||
|
|
aa0fbdc993 | ||
|
|
b50bfa7277 | ||
|
|
e91988f679 | ||
|
|
3ed1fce3fb | ||
|
|
e3ea0b511a | ||
|
|
c411c663de | ||
|
|
1c6f66fc14 | ||
|
|
1f927a374c | ||
|
|
07c4225aa8 | ||
|
|
42a85ac39f | ||
|
|
16e6d31ee3 | ||
|
|
a6db5b436d | ||
|
|
47675f2e28 | ||
|
|
0642bbefa7 | ||
|
|
43da603d52 | ||
|
|
f9e1f4d111 | ||
|
|
1508564e10 | ||
|
|
c14e6f4dcc | ||
|
|
75b6a20134 | ||
|
|
d82a07c221 | ||
|
|
c6f7dbcaa5 | ||
|
|
82cd099e48 | ||
|
|
546c011dd8 | ||
|
|
10b33ccaf6 | ||
|
|
bc01572176 | ||
|
|
ccd2064722 | ||
|
|
0fb42901dd | ||
|
|
a4280d7457 | ||
|
|
6966ede68b | ||
|
|
27dd14a5a2 | ||
|
|
2494301f1e | ||
|
|
f13143accb | ||
|
|
26d340a917 | ||
|
|
cc75cd4102 | ||
|
|
cf8615b231 | ||
|
|
30f473a2b1 | ||
|
|
4618709baa | ||
|
|
16b098ea42 | ||
|
|
b31c4d407a | ||
|
|
28c36cc5e2 | ||
|
|
6bfbcb842e | ||
|
|
a0eade4fdd | ||
|
|
ee24b7e88a | ||
|
|
c9baddf9a4 | ||
|
|
c5c780a685 | ||
|
|
421aaabf62 | ||
|
|
86788f034f | ||
|
|
232d3758cf | ||
|
|
71bcf9b31d | ||
|
|
bf4ee4f064 | ||
|
|
aa3bdfeb17 | ||
|
|
ce6967d396 | ||
|
|
330065f5e0 | ||
|
|
944db82553 | ||
|
|
c257361f05 | ||
|
|
ffdbfb018a | ||
|
|
cfa2bd6b08 | ||
|
|
51e90c3ce0 | ||
|
|
d69149f43e | ||
|
|
f261663f3d | ||
|
|
e5948dd1d3 | ||
|
|
5f13aab7d8 | ||
|
|
292d489592 | ||
|
|
0a01f20e35 | ||
|
|
2bd08d5c4c | ||
|
|
adae5126db | ||
|
|
dddf1bcb76 | ||
|
|
d23d4b0fd4 | ||
|
|
658f7550b3 | ||
|
|
cfb50ae21e | ||
|
|
9b0a365822 | ||
|
|
97ab130619 | ||
|
|
3578fde290 | ||
|
|
f382da69b8 | ||
|
|
2da3d215c6 | ||
|
|
43ed29c14c | ||
|
|
9df0283689 | ||
|
|
04b82cf4a5 | ||
|
|
274c3012b0 | ||
|
|
2b24a4934f | ||
|
|
692a423072 | ||
|
|
148f55335f | ||
|
|
2f752842a1 | ||
|
|
aff72996a1 | ||
|
|
08e425bcf6 | ||
|
|
454ddc601a | ||
|
|
b025f208a8 | ||
|
|
b60e533b9d | ||
|
|
37af0e0d59 | ||
|
|
ac1de5bce9 | ||
|
|
ef7ff55e07 | ||
|
|
608db5b982 | ||
|
|
8dd69efd48 | ||
|
|
73f7fba793 | ||
|
|
867e2402d2 | ||
|
|
a3b9e61967 | ||
|
|
cd149b68e8 | ||
|
|
cd3583c736 | ||
|
|
441f86f3ed | ||
|
|
f62bea65a1 | ||
|
|
886b574987 | ||
|
|
2888bac275 | ||
|
|
35c9206916 | ||
|
|
c4c5b59312 | ||
|
|
f25fb4e5ac | ||
|
|
868bfec5e6 | ||
|
|
e7c242213a | ||
|
|
862552ead4 | ||
|
|
9d90e0c167 | ||
|
|
a281f227cd | ||
|
|
5b981278db | ||
|
|
c1091ed3d1 | ||
|
|
08aed63455 | ||
|
|
90a550ee4f | ||
|
|
34869fc2a2 | ||
|
|
3deb10156d | ||
|
|
8c0e84de05 | ||
|
|
23be083c39 | ||
|
|
217aafce39 | ||
|
|
03210c63f4 | ||
|
|
a90510f6f2 | ||
|
|
36d91aded6 | ||
|
|
9afe8a1297 | ||
|
|
1e6f272034 | ||
|
|
a1aa2f81ef | ||
|
|
62899ef308 | ||
|
|
7f3396c002 | ||
|
|
453bc18196 | ||
|
|
dbb6b57b76 | ||
|
|
d7137db78c | ||
|
|
5ac4f2d80b | ||
|
|
5ba5271da9 | ||
|
|
b834e3015a | ||
|
|
c8721ded62 | ||
|
|
1e97372d24 | ||
|
|
fd4e111784 | ||
|
|
75094e7e21 | ||
|
|
8db2d674ed | ||
|
|
ffb140fab3 | ||
|
|
e93543983c | ||
|
|
0d066f80ff | ||
|
|
ccca1b2016 | ||
|
|
fec0e31a25 | ||
|
|
d246aa8f6d | ||
|
|
66bfba2258 | ||
|
|
b53b4373cb | ||
|
|
0810f93883 | ||
|
|
a4e696a252 | ||
|
|
0951d08f52 | ||
|
|
dbf367e070 | ||
|
|
6447ba8ec8 | ||
|
|
43e260966f | ||
|
|
b0e301b046 | ||
|
|
c8a9ea4979 | ||
|
|
afb7fc05da | ||
|
|
14124ccca8 | ||
|
|
df5022dbc3 | ||
|
|
015e798a31 | ||
|
|
c19125bb02 | ||
|
|
0e6ac5baf1 | ||
|
|
2c8d1b5b8c | ||
|
|
f7c0c1c21a | ||
|
|
4edd98f7ce | ||
|
|
df0abb7000 | ||
|
|
4f93da307f | ||
|
|
a8765d54aa | ||
|
|
bb834358d4 | ||
|
|
ec0f3d22e7 | ||
|
|
009b75cab6 | ||
|
|
d64668df1e | ||
|
|
72e808c9a7 | ||
|
|
96cc9223be | ||
|
|
13b099fbd0 | ||
|
|
1a8416c297 | ||
|
|
8538bec99e | ||
|
|
f983900597 | ||
|
|
3af02020ff | ||
|
|
8c71488757 | ||
|
|
74316bf702 | ||
|
|
7aa8c435c9 | ||
|
|
daeb51253d | ||
|
|
0ce2f41db4 | ||
|
|
02e5a962d7 | ||
|
|
dcc32dc69f | ||
|
|
af3d6681dd | ||
|
|
106968a3be | ||
|
|
2cd56ca044 | ||
|
|
eff198d079 | ||
|
|
c3b5b88cd2 | ||
|
|
4e19e87bbc | ||
|
|
6be6f6585d | ||
|
|
d7579f0c99 | ||
|
|
b741679c9c | ||
|
|
852990e967 | ||
|
|
21fd75b500 | ||
|
|
3e5d9010a3 | ||
|
|
784616ec29 | ||
|
|
6251d19946 | ||
|
|
17b1332a2a | ||
|
|
74eec3bdbe | ||
|
|
a9901c4ea7 | ||
|
|
348a2f91ee | ||
|
|
7115d862ea | ||
|
|
52ed4aa631 | ||
|
|
92cedf8931 | ||
|
|
e1097f11b5 | ||
|
|
eb34c0e46b | ||
|
|
ee2181b371 | ||
|
|
2a5d090e91 | ||
|
|
857bebe819 | ||
|
|
9728152768 | ||
|
|
2566a85429 | ||
|
|
46b3130198 | ||
|
|
8664516c8d | ||
|
|
0733c246ea | ||
|
|
4203985e3e | ||
|
|
900298bce7 | ||
|
|
09c37f508e | ||
|
|
c9e01bcc81 | ||
|
|
b079545e0f | ||
|
|
c3bf0f8cbf | ||
|
|
e945bca1d9 | ||
|
|
bf5835de5e | ||
|
|
7503f0cb10 | ||
|
|
3a751bcf9b | ||
|
|
c31ba101d6 | ||
|
|
ecadc74d44 | ||
|
|
63d25aaf19 | ||
|
|
5af82c3c05 | ||
|
|
8b4d74ed17 | ||
|
|
6a6a9064d5 | ||
|
|
b188a9488a | ||
|
|
7c2635f65d | ||
|
|
c67d0a0e1a | ||
|
|
7ee78e89c9 | ||
|
|
40370e104f | ||
|
|
a8809baa6c | ||
|
|
244d5d2c3b | ||
|
|
a0370a6617 | ||
|
|
eb077fcc75 | ||
|
|
c5adc50eed | ||
|
|
6e71b6fd31 | ||
|
|
278382589d | ||
|
|
6f0f6cf21a | ||
|
|
01331ed311 | ||
|
|
f638a3d50c | ||
|
|
512c41dbaf | ||
|
|
f6bab4adcf | ||
|
|
526ecee3da | ||
|
|
1bc9815d53 | ||
|
|
78bd7c9465 | ||
|
|
d74df8692b | ||
|
|
eda86412cc | ||
|
|
cce5945fd2 | ||
|
|
72038258ed | ||
|
|
056d8fa9ad | ||
|
|
3888e0066f | ||
|
|
ee6571d050 | ||
|
|
9472288304 | ||
|
|
fd5e10cfdf | ||
|
|
aeae18ec37 | ||
|
|
03d3943e99 | ||
|
|
214d137672 | ||
|
|
83db275ddf | ||
|
|
b8f16d081a | ||
|
|
675b01ed48 | ||
|
|
b20224a096 | ||
|
|
fd6edfccc4 | ||
|
|
4c58438e8a | ||
|
|
5ff383a025 | ||
|
|
dcb6854683 | ||
|
|
e4644bfe5a | ||
|
|
93168fef87 | ||
|
|
9832822bdf | ||
|
|
5d91aa3bcd | ||
|
|
354ab5229b | ||
|
|
00de0cd4b5 | ||
|
|
26210216da | ||
|
|
e29c14a22b | ||
|
|
a6990c8fb8 | ||
|
|
3e40e71b96 | ||
|
|
3f45abe331 | ||
|
|
6777c62789 | ||
|
|
1aac869738 | ||
|
|
493554ea30 | ||
|
|
1cf87c639b | ||
|
|
2cb3d92163 | ||
|
|
89b6e52a73 | ||
|
|
97407c10ff | ||
|
|
81222dadbc | ||
|
|
400555c391 | ||
|
|
9125b05809 | ||
|
|
139b353a28 | ||
|
|
fc474a07d0 | ||
|
|
8fd8fa09a5 | ||
|
|
41ae831d0e | ||
|
|
dbca540d70 | ||
|
|
dc7eca4bf9 | ||
|
|
fb07149cb7 | ||
|
|
b2bd5a5548 | ||
|
|
aa6b333e79 | ||
|
|
0cb9740535 | ||
|
|
46eadd54e5 | ||
|
|
6b032b49fe | ||
|
|
35f78ee0f9 | ||
|
|
5ec36df7f0 | ||
|
|
f918fd65b6 | ||
|
|
d08a39483d | ||
|
|
9191f4ff2d | ||
|
|
19232f554f | ||
|
|
b4a83414ac | ||
|
|
cb0e62576d | ||
|
|
e3f557406f | ||
|
|
676af831c0 | ||
|
|
873d76d72c | ||
|
|
8ee490b881 | ||
|
|
ff31b277f6 | ||
|
|
120eb5b502 | ||
|
|
a93e288d6a | ||
|
|
8cf9311ced | ||
|
|
713e781473 | ||
|
|
a32295e74a | ||
|
|
204b02de3e | ||
|
|
8379edce99 | ||
|
|
e265ab67c7 | ||
|
|
fde1f13b4e | ||
|
|
9c3839c7e2 | ||
|
|
c0fd702cc7 | ||
|
|
429419c4af | ||
|
|
56ae20602d | ||
|
|
a4b80cc2e4 | ||
|
|
4994cc07a0 | ||
|
|
e96cf02561 | ||
|
|
764c9b2986 | ||
|
|
40c6499d3a | ||
|
|
3a78efd83c | ||
|
|
eb33cf75e3 | ||
|
|
863d8e6405 | ||
|
|
1fc5a45b9e | ||
|
|
7751fece35 | ||
|
|
7670c42462 | ||
|
|
b72fc3cd25 | ||
|
|
4cc1a4f74c | ||
|
|
540607086c | ||
|
|
7d929e98af | ||
|
|
0086097639 | ||
|
|
daff0badc8 | ||
|
|
22c4d8fabe | ||
|
|
3485482460 | ||
|
|
c43873379c | ||
|
|
ea5e5df5a3 | ||
|
|
f2caf2f1ff | ||
|
|
07d4020fca | ||
|
|
2142e529ff | ||
|
|
b9d502e2e6 | ||
|
|
8c80862c10 | ||
|
|
2356c7b63d | ||
|
|
9c24fc25f5 | ||
|
|
4f1a6d56c1 | ||
|
|
b71b7e209e | ||
|
|
2581e98aff | ||
|
|
afc7136bae | ||
|
|
e489170558 | ||
|
|
50106f2bd3 | ||
|
|
e96f4a5be6 | ||
|
|
4768ac5fda | ||
|
|
c91fcc527a | ||
|
|
8520ff35b3 | ||
|
|
9b8a98f4ec | ||
|
|
4bd4afaec7 | ||
|
|
69352d8414 | ||
|
|
4a21ea6575 | ||
|
|
86bbb9fe38 | ||
|
|
4030d4fc20 | ||
|
|
182f69a9ec | ||
|
|
fb40efe4b7 | ||
|
|
9d00c00072 | ||
|
|
10c3118f9c | ||
|
|
1fa149dca2 | ||
|
|
60f4c963b5 | ||
|
|
51b8e64972 | ||
|
|
ae542dce74 | ||
|
|
fa8a4f2020 | ||
|
|
481bdd56d3 | ||
|
|
1a9083ddb7 | ||
|
|
9779f43620 | ||
|
|
d31e82edfc | ||
|
|
981535a1c3 | ||
|
|
5354e39e5f | ||
|
|
ca9293cbfb | ||
|
|
e2fe6a8249 | ||
|
|
a8347b7ada | ||
|
|
bcbf7c3b7b | ||
|
|
6a26cb280f | ||
|
|
fd658ace9d | ||
|
|
5e71a2aa3f | ||
|
|
e3fb923b34 | ||
|
|
f55b257609 | ||
|
|
81bf3dae5c | ||
|
|
d0074f3411 | ||
|
|
2cc2d971c6 | ||
|
|
5830f5590e | ||
|
|
75facebe80 | ||
|
|
0130398e9f | ||
|
|
22d9b86e9f | ||
|
|
c87b671275 | ||
|
|
1eb5857811 | ||
|
|
5fc1cb39a6 | ||
|
|
1f8e29276e | ||
|
|
cf02c7fd02 | ||
|
|
5d93c64c0e | ||
|
|
c738928ea3 | ||
|
|
707310db64 | ||
|
|
59bf43dc1f | ||
|
|
fe461381a2 | ||
|
|
685873ab42 | ||
|
|
b6a951903e | ||
|
|
acfa84918e | ||
|
|
75304eb3be | ||
|
|
1d7eb59ff2 | ||
|
|
4273cc9e29 | ||
|
|
29be2de5cb | ||
|
|
91b0496c89 | ||
|
|
f043f948de | ||
|
|
7ef7a8f306 | ||
|
|
db8eea2468 | ||
|
|
55813d9209 | ||
|
|
4c05daae1b | ||
|
|
16eb7232c3 | ||
|
|
c28ffcdd9f | ||
|
|
4b8652f1c4 | ||
|
|
674bd8f264 | ||
|
|
1ba832dbfe | ||
|
|
d3e4d3fbcb | ||
|
|
58a3cb4fbd | ||
|
|
8ad1551b15 | ||
|
|
123771163a | ||
|
|
f80a759488 | ||
|
|
42f8a4715e | ||
|
|
c29892e340 | ||
|
|
b4a2ed6bb5 | ||
|
|
67e8caf045 | ||
|
|
2562debe31 | ||
|
|
87e2fd610c | ||
|
|
af118bcc53 | ||
|
|
7e01172b4c | ||
|
|
f56ae93772 | ||
|
|
3834805929 | ||
|
|
50b6057bbf | ||
|
|
c0199abacf | ||
|
|
77688c74f3 | ||
|
|
47ab7419ac | ||
|
|
dc209f77ec | ||
|
|
76aa8c7df5 | ||
|
|
671a29ff34 | ||
|
|
eb35794aca | ||
|
|
a8d6691dee | ||
|
|
955f4ae977 | ||
|
|
25a869a686 | ||
|
|
7aa8030b76 | ||
|
|
108d843bba | ||
|
|
099fea8565 | ||
|
|
a573a2ada1 | ||
|
|
1468ca8ebc | ||
|
|
274aea9f8f | ||
|
|
38a99a75ed | ||
|
|
6e06bd0cb4 | ||
|
|
02a793998a | ||
|
|
81ab9469b7 | ||
|
|
bcc928495d | ||
|
|
621ae7dbc9 | ||
|
|
85f2c03903 | ||
|
|
f7fd741d43 | ||
|
|
fad0d81837 | ||
|
|
09687409dc | ||
|
|
091bcd107c | ||
|
|
f9b300d63a | ||
|
|
4f2acc2c96 | ||
|
|
6aa4a60d5c | ||
|
|
b075bf51b0 | ||
|
|
de2341ece0 | ||
|
|
ff67e7d47c | ||
|
|
31644ed39d | ||
|
|
acb235ef4f | ||
|
|
d554835b50 | ||
|
|
c8453d80fc | ||
|
|
f3f713ae65 | ||
|
|
cfa741e597 | ||
|
|
13fb2351ed | ||
|
|
3555ba518d | ||
|
|
c5a19ca42e | ||
|
|
153eb7e9d3 | ||
|
|
7ba52d4931 | ||
|
|
ebe5b46653 | ||
|
|
1bd82d4914 | ||
|
|
89775fa94f | ||
|
|
4456872635 | ||
|
|
ee9ae22651 | ||
|
|
afe0f46768 | ||
|
|
203d8c3481 | ||
|
|
c9ae49255d | ||
|
|
51f17d3358 | ||
|
|
cacdd58b41 | ||
|
|
8b7bcbbc47 | ||
|
|
bdf9482e75 | ||
|
|
b94d0b66e6 | ||
|
|
1dff122a94 | ||
|
|
fb8065df27 | ||
|
|
4274139210 | ||
|
|
b422a44c03 | ||
|
|
285479c0bc | ||
|
|
e641ec12fa | ||
|
|
36fda28a92 | ||
|
|
1ece515074 | ||
|
|
04f840d907 | ||
|
|
df8ccc04eb | ||
|
|
dd764b93e0 | ||
|
|
335497f688 | ||
|
|
41a9251982 | ||
|
|
84bf03580d | ||
|
|
43d5dfcb71 | ||
|
|
7a9fc7ef12 | ||
|
|
ecf24cd4d9 | ||
|
|
ba828e59de | ||
|
|
34719f94b4 | ||
|
|
c95a6792e5 | ||
|
|
88529d5c25 | ||
|
|
3489878395 | ||
|
|
d938013733 | ||
|
|
dfe2ea4d0a | ||
|
|
7a3e345cf7 | ||
|
|
1fb8c9c896 | ||
|
|
2ed97640d5 | ||
|
|
6d31aa6f67 | ||
|
|
a76ffbd194 | ||
|
|
0d7e0ba087 | ||
|
|
618d491f92 | ||
|
|
21a3462798 | ||
|
|
d2c5783e5a | ||
|
|
007cb4516a | ||
|
|
8ec93d9973 | ||
|
|
0a64708302 | ||
|
|
b964f6dd22 | ||
|
|
14233188be | ||
|
|
e3193c6243 | ||
|
|
8782714ec3 | ||
|
|
03010ecde7 | ||
|
|
97d63d3da4 | ||
|
|
a212f4eb5e | ||
|
|
5c993fd568 | ||
|
|
df60189266 | ||
|
|
97f61a9ecc | ||
|
|
8740e6ca66 | ||
|
|
c9eb3c2b54 | ||
|
|
d9411bc9bb | ||
|
|
a3a1b143cf | ||
|
|
5c18c78b50 | ||
|
|
3bcb5f147d | ||
|
|
1bfe43ff76 | ||
|
|
2d2019c059 | ||
|
|
4613619841 | ||
|
|
44e3c7eb6d | ||
|
|
fd0b460391 | ||
|
|
3b917b9d79 | ||
|
|
970049fde8 | ||
|
|
5b40cc4a01 | ||
|
|
0919c5642d | ||
|
|
de4f90b3f4 | ||
|
|
a1c2270956 | ||
|
|
b9cf0cfc44 | ||
|
|
d2869e4749 | ||
|
|
3ec911b62c | ||
|
|
4ff66c1cb6 | ||
|
|
184146b825 | ||
|
|
1e8543efb1 | ||
|
|
a976e54c19 | ||
|
|
88e26da026 | ||
|
|
58522b8f67 | ||
|
|
f09bb17da4 | ||
|
|
353ae0f090 | ||
|
|
0404881d85 | ||
|
|
fb88d54c31 | ||
|
|
a523356504 | ||
|
|
91ff7f1c80 | ||
|
|
51f6c268d3 | ||
|
|
5e1b284ad0 | ||
|
|
a962d28674 | ||
|
|
7f0c1f823a | ||
|
|
d08a2b8c02 | ||
|
|
d7f3518a9a | ||
|
|
d34d2a023c | ||
|
|
1ecd954199 | ||
|
|
6e161ab84e | ||
|
|
97c59f36a2 | ||
|
|
89443eac0e | ||
|
|
3e8414e259 | ||
|
|
06a30f0447 | ||
|
|
4e636aa53a | ||
|
|
7bd18ef331 | ||
|
|
181fcc3965 | ||
|
|
3a1f745e65 | ||
|
|
2dc3ea3f12 | ||
|
|
1e8ed49661 | ||
|
|
84bf169458 | ||
|
|
3dabe62254 | ||
|
|
0d246ac95b | ||
|
|
2d0612c972 | ||
|
|
4cf2b78fca | ||
|
|
3af8a22d13 | ||
|
|
b0c7b3a38b | ||
|
|
3be03b681b | ||
|
|
dd5b7240b6 | ||
|
|
2b9b82e5c4 | ||
|
|
c5c14bb798 | ||
|
|
a201fc773a | ||
|
|
d897164d5c | ||
|
|
e558d3f9ad | ||
|
|
6a7078f634 | ||
|
|
b14676f0c5 | ||
|
|
4cef1af8cb | ||
|
|
42aa407adc | ||
|
|
e7298d60d3 | ||
|
|
9c0ff932c8 | ||
|
|
288dd221f8 | ||
|
|
d374ef9ffe | ||
|
|
7fa6a363b2 | ||
|
|
13da3390e5 | ||
|
|
4164e6ee8e | ||
|
|
2b454f99dd | ||
|
|
2025634417 | ||
|
|
42e582a6ad | ||
|
|
339a7e9409 | ||
|
|
f420911e19 | ||
|
|
2e39a5fff0 | ||
|
|
e0f7deabfd | ||
|
|
abe345e925 | ||
|
|
4d33554465 | ||
|
|
32c559838d | ||
|
|
62a0bf8732 | ||
|
|
24ae8b4498 | ||
|
|
2945a91189 | ||
|
|
26735dac8d | ||
|
|
7c0a41c998 | ||
|
|
778a89ef8a | ||
|
|
83d85dc928 | ||
|
|
25a06aee01 | ||
|
|
c89ea1ad56 | ||
|
|
76f9f23b16 | ||
|
|
a45f6bea5b | ||
|
|
49fec95b33 | ||
|
|
d38fd80dff | ||
|
|
37b03ef5d2 | ||
|
|
0c878a056e | ||
|
|
1da4bef068 | ||
|
|
65c156750d | ||
|
|
bd34dfe1f7 | ||
|
|
78445f9879 | ||
|
|
053210330e | ||
|
|
df894f91f2 | ||
|
|
f758614b0a | ||
|
|
71846c98b3 | ||
|
|
272cad7b97 | ||
|
|
5eb9523325 | ||
|
|
d2b2f2f6b6 | ||
|
|
55f9720a44 | ||
|
|
9d52e6acbf | ||
|
|
8f649f5ab4 | ||
|
|
fbfb54e3e2 | ||
|
|
e523d908bf | ||
|
|
8b2d416035 | ||
|
|
078356458b | ||
|
|
4846246f12 | ||
|
|
dc10b3d904 | ||
|
|
05d4a5e4a3 | ||
|
|
2883b6dc17 | ||
|
|
5f9e153db8 | ||
|
|
c05153f00a | ||
|
|
3616641e93 | ||
|
|
78cbeca606 | ||
|
|
354c7ecddf | ||
|
|
4d659318c4 | ||
|
|
9d4399eb28 | ||
|
|
32702559f6 | ||
|
|
310a1dfaec | ||
|
|
6528b5f25b | ||
|
|
19378abfae | ||
|
|
593e86c15d | ||
|
|
6226d60c9a | ||
|
|
aa35dc9437 | ||
|
|
098d05c27f | ||
|
|
05917c6d2f | ||
|
|
e300c62d76 | ||
|
|
3829d1f964 | ||
|
|
0d2593e0e3 | ||
|
|
e99f0d12b8 | ||
|
|
d0e736f31b | ||
|
|
3a5db3c16d | ||
|
|
0cddd9c417 | ||
|
|
51ce3e1093 | ||
|
|
db50880399 | ||
|
|
8e63da2f04 | ||
|
|
a0b502dd44 | ||
|
|
ff272c797a | ||
|
|
f4b93c8a5a | ||
|
|
c01056b99a | ||
|
|
12ca721e0b | ||
|
|
9c7d519ab2 | ||
|
|
5299079f01 | ||
|
|
4c8575dea3 | ||
|
|
f3aaa8bcee | ||
|
|
2cc53321ae | ||
|
|
64e9d704e2 | ||
|
|
1c1f10056d | ||
|
|
ccae4a8888 | ||
|
|
5c1840914b | ||
|
|
be61e6724a | ||
|
|
38975ecee0 | ||
|
|
d8b31c02e2 | ||
|
|
331b961819 | ||
|
|
769e4d5193 | ||
|
|
dbf7e690b8 | ||
|
|
12a9c2b441 | ||
|
|
12d3c52de2 | ||
|
|
614f0b4350 | ||
|
|
74c78ef927 | ||
|
|
765c2de148 | ||
|
|
644fef1e84 | ||
|
|
b9053a24dc | ||
|
|
0be8326351 | ||
|
|
a7a73df696 | ||
|
|
e9b4c47887 | ||
|
|
7ee6f1de39 | ||
|
|
eea51bee30 | ||
|
|
0beb03fe53 | ||
|
|
4ccfe020a2 | ||
|
|
19735d5416 | ||
|
|
e53a45b3e0 | ||
|
|
e00d1ed78c | ||
|
|
83a4562b98 | ||
|
|
27fef50919 | ||
|
|
947a00f6c9 | ||
|
|
2f36d7d259 | ||
|
|
e2d7027074 | ||
|
|
a2de92d2a3 | ||
|
|
edef65f333 | ||
|
|
e7dfe97ecc | ||
|
|
cd0fe51109 | ||
|
|
6c0a5021cd | ||
|
|
c9b34681b7 | ||
|
|
8422c3a9d3 | ||
|
|
daed251d0d | ||
|
|
ddb6d791e8 | ||
|
|
107170163a | ||
|
|
7aa09e393c | ||
|
|
75cc3e40a5 | ||
|
|
78cf84310c | ||
|
|
0f6622fab6 | ||
|
|
bb20df0db9 | ||
|
|
8a2b5f53f6 | ||
|
|
e30db9816e | ||
|
|
5f8efc3aa8 | ||
|
|
f3565f3f70 | ||
|
|
2be7ee6ee0 | ||
|
|
f7a8cd1250 | ||
|
|
3f5cc224a6 | ||
|
|
26363638bc | ||
|
|
1cd51181eb | ||
|
|
a1d5c340e8 | ||
|
|
6f1763317a | ||
|
|
8973393333 | ||
|
|
a5d53c4453 | ||
|
|
85fda89413 | ||
|
|
7b6c11adb7 | ||
|
|
7e14666802 | ||
|
|
6fbf6721b9 | ||
|
|
ec76498686 | ||
|
|
05634e61a7 | ||
|
|
2dd7ef5302 | ||
|
|
49a5a556e9 | ||
|
|
855dd98c62 | ||
|
|
2759c9e3bd | ||
|
|
594b8b786a | ||
|
|
3387d085b4 | ||
|
|
ee6b842f9a | ||
|
|
fce11d7b46 | ||
|
|
48c9b8abdc | ||
|
|
c3c99f317e | ||
|
|
9deb50f812 | ||
|
|
18c02ea991 | ||
|
|
3e23a5ff75 | ||
|
|
076bb9c356 | ||
|
|
c21af17a30 | ||
|
|
1d298ea5cf | ||
|
|
f214ebf4e9 | ||
|
|
ce5ca6f845 | ||
|
|
cc2be00abb | ||
|
|
bbfcce1cc6 | ||
|
|
d81961ccee | ||
|
|
c1da5dd513 | ||
|
|
7c201fed81 | ||
|
|
f3a97b6c08 | ||
|
|
6509067856 | ||
|
|
6482718dbf | ||
|
|
6dac4c76eb | ||
|
|
0d825eb590 | ||
|
|
6cc0d9103d | ||
|
|
514a1ad7d0 | ||
|
|
7f99353ca0 | ||
|
|
a58657403e | ||
|
|
b9a86df795 | ||
|
|
cf94ce6a54 | ||
|
|
e9d112ac39 | ||
|
|
f4c6bf30b6 | ||
|
|
626522d1bb | ||
|
|
58f3905af4 | ||
|
|
89b4984009 | ||
|
|
804c779359 | ||
|
|
d699add216 | ||
|
|
e36fb3cc17 | ||
|
|
fcf57a5066 | ||
|
|
715c886dad | ||
|
|
e2cd45b18a | ||
|
|
a8f8708418 | ||
|
|
ca5b02af9a | ||
|
|
832d43efdc | ||
|
|
e9daff7616 | ||
|
|
22999a9861 | ||
|
|
64edf70b93 | ||
|
|
75dbb0bc19 | ||
|
|
e34439dcd7 | ||
|
|
9c84ce1e6f | ||
|
|
907003881e | ||
|
|
4e2ec6b7f8 | ||
|
|
4b90ff892f | ||
|
|
5591a82a15 | ||
|
|
598c06f5b0 | ||
|
|
bf12001c04 | ||
|
|
e59df0fbb5 | ||
|
|
47f5c51b2d | ||
|
|
e9b982b8ea | ||
|
|
770cf71c13 | ||
|
|
5734fb897c | ||
|
|
df8aa642c5 | ||
|
|
5c76cfb071 | ||
|
|
75f8bc2679 | ||
|
|
208c1cfb1a | ||
|
|
1d543b3737 | ||
|
|
b41b72c316 | ||
|
|
26703df4d7 | ||
|
|
f490f6af98 | ||
|
|
70ad551cf4 | ||
|
|
48705b6d8c | ||
|
|
6e5c8750f6 | ||
|
|
1f78f30756 | ||
|
|
df5b97c614 | ||
|
|
1de892bf0e | ||
|
|
457cbbd954 | ||
|
|
842edd62a6 | ||
|
|
7b374a4b08 | ||
|
|
d0f5664358 | ||
|
|
053546402d | ||
|
|
0fa9d0ab12 | ||
|
|
9d0eab6305 | ||
|
|
79a90d0671 | ||
|
|
451e95e48d | ||
|
|
d3f535a0a7 | ||
|
|
4fc54dae32 | ||
|
|
4e70d078ef | ||
|
|
31673d8acd | ||
|
|
d5e127bfea | ||
|
|
f40374deb9 | ||
|
|
f8cbea4dbf | ||
|
|
e3bd6cd5ef | ||
|
|
71e7b0ab71 | ||
|
|
fd662a1855 | ||
|
|
2a8940415d | ||
|
|
b63a271d55 | ||
|
|
47e70efca6 | ||
|
|
0f3dd88029 | ||
|
|
35a052f4ab | ||
|
|
a31e78817e | ||
|
|
f250d7ab47 | ||
|
|
8686ab9a9d | ||
|
|
2cd98c2c60 | ||
|
|
c9eec4fa06 | ||
|
|
6230c608d0 | ||
|
|
ab8392b856 | ||
|
|
067f02f9d8 | ||
|
|
0c1f8550b8 | ||
|
|
7287204279 | ||
|
|
e392212c0e | ||
|
|
dacfe38429 | ||
|
|
1eb75d9be7 | ||
|
|
a122d35e43 | ||
|
|
445543e256 | ||
|
|
cfeffe40f6 | ||
|
|
dfb72ccb2e | ||
|
|
0f4a369aeb | ||
|
|
385f7f2c24 | ||
|
|
aa36ed5aa4 | ||
|
|
c87096993e | ||
|
|
784ab79eec | ||
|
|
7122a3f90b | ||
|
|
868a447c58 | ||
|
|
002244e3b3 | ||
|
|
5a319d6ee1 | ||
|
|
a1ca9025e7 | ||
|
|
9cd7cbc9e3 | ||
|
|
2d3dc1f6cc | ||
|
|
d01c1614a5 | ||
|
|
fb5320f7bc | ||
|
|
7c916e9bdb | ||
|
|
b774702ee8 | ||
|
|
ffaaacc148 | ||
|
|
9de9335554 | ||
|
|
feae21992a | ||
|
|
18cfe81e00 | ||
|
|
869bdc454d | ||
|
|
6725234466 | ||
|
|
05bf1512f9 | ||
|
|
bd2d1f7722 | ||
|
|
f58bc49176 | ||
|
|
5cabafc257 | ||
|
|
0fdff04fc6 | ||
|
|
1448d0b0c2 | ||
|
|
f843e658a1 | ||
|
|
06ffb6dd53 | ||
|
|
78110e17e5 | ||
|
|
74df1a1c31 | ||
|
|
d9862af8e1 | ||
|
|
ea82d8f8ca | ||
|
|
8de3235a63 | ||
|
|
eb3ed5744b | ||
|
|
b98a4d82c3 | ||
|
|
ac562d9388 | ||
|
|
d0cb960455 | ||
|
|
7db16f8521 | ||
|
|
229d3986c2 | ||
|
|
c237c8ee9e | ||
|
|
bd9a1320fa | ||
|
|
9eb4506117 | ||
|
|
58c95bb399 | ||
|
|
af5af829cf | ||
|
|
0961bdb506 | ||
|
|
7b9111d386 | ||
|
|
f8580a23ff | ||
|
|
b5f99881fe | ||
|
|
6266216fda | ||
|
|
ca39ac5f00 | ||
|
|
5d99798c7d | ||
|
|
24ed84dddd | ||
|
|
e82e68d22a | ||
|
|
e6bb06030c | ||
|
|
595c82c1f1 | ||
|
|
a46855821b | ||
|
|
89ba3e707a | ||
|
|
2cd6a2d7a0 | ||
|
|
1f758f90d0 | ||
|
|
8681dd8c93 | ||
|
|
0781cef8b1 | ||
|
|
5edbe5758e | ||
|
|
20c5c4c3dd | ||
|
|
e376c14e8a | ||
|
|
68babfb4bb | ||
|
|
f3d4377fdd | ||
|
|
efaeb6786e | ||
|
|
96d2978a23 | ||
|
|
6e1665d1fb | ||
|
|
c6603be194 | ||
|
|
d17e706351 | ||
|
|
acd978e054 | ||
|
|
ed6591e450 | ||
|
|
fc61869ea8 | ||
|
|
80501e7f8f | ||
|
|
a3e6518c41 | ||
|
|
b71496a25d | ||
|
|
391ef70e67 | ||
|
|
29e2bbc0c2 | ||
|
|
5884f7df55 | ||
|
|
6708046951 | ||
|
|
4751a5f3e2 | ||
|
|
0b82def5f5 | ||
|
|
ee529a5472 | ||
|
|
d38344125e | ||
|
|
caee9415e4 | ||
|
|
7e85ad95c7 | ||
|
|
86a35d6c13 | ||
|
|
ff3360afc0 | ||
|
|
eea322e647 | ||
|
|
120f0f0959 | ||
|
|
38443cf3f5 | ||
|
|
c69f28e0a0 | ||
|
|
704e44e479 | ||
|
|
c19a7d7b32 | ||
|
|
7323ddbabf | ||
|
|
0b7dc326e9 | ||
|
|
afc0341b54 | ||
|
|
0df8462562 | ||
|
|
305ace528a | ||
|
|
f7b8e5c861 | ||
|
|
8bf5dd0502 | ||
|
|
8d84da40e1 | ||
|
|
ab886cde16 | ||
|
|
ab5432daa0 | ||
|
|
60d4708f15 | ||
|
|
7fba68fcbd | ||
|
|
8f45ecebb4 | ||
|
|
c51ba50946 | ||
|
|
616b32eb67 | ||
|
|
278f764d61 | ||
|
|
5ed5693cd5 | ||
|
|
236a087a42 | ||
|
|
044f8fce4e | ||
|
|
618adf501d | ||
|
|
0da3b07fb3 | ||
|
|
4bea303c5f | ||
|
|
c18b72b53d | ||
|
|
ac9ea53e8e | ||
|
|
669a18564f | ||
|
|
c6b234673c | ||
|
|
1adf994740 | ||
|
|
72d82e45ed | ||
|
|
070e13ded8 | ||
|
|
c7d8031681 | ||
|
|
bf8932efaa | ||
|
|
ff9e0cd4cf | ||
|
|
1522d4cb2b | ||
|
|
fac8a286e8 | ||
|
|
d37e74c62c | ||
|
|
0a1862e92e | ||
|
|
9dd16045fb | ||
|
|
c05b45b241 | ||
|
|
2a0f69ac5f | ||
|
|
d5f50954c0 | ||
|
|
dda9289d77 | ||
|
|
66f9ee0b3e | ||
|
|
72d2cab517 | ||
|
|
a376d656a7 | ||
|
|
204fc25c21 | ||
|
|
bea8c72a82 | ||
|
|
16c1fcf4db | ||
|
|
566f78a95c | ||
|
|
6ca707822e | ||
|
|
59477a32bc | ||
|
|
bc38750d47 | ||
|
|
51278c0fbf | ||
|
|
644e3e8721 | ||
|
|
107bc5c217 | ||
|
|
1313445afe | ||
|
|
79db8807a6 | ||
|
|
4dc12c72c2 | ||
|
|
ac427bdc0c | ||
|
|
96a3736cd7 | ||
|
|
6424b65097 | ||
|
|
c6121675bb | ||
|
|
626f835601 | ||
|
|
aa38c8101b | ||
|
|
d2760d5a49 | ||
|
|
0c3f9e7e2e | ||
|
|
fd4a33e450 | ||
|
|
f07face7c0 | ||
|
|
935f985e25 | ||
|
|
35b43e9708 | ||
|
|
9991b066c7 | ||
|
|
c76d6f34a2 | ||
|
|
380f7c2e51 | ||
|
|
ab07b8ca06 | ||
|
|
6f302e26b3 | ||
|
|
f9343c4683 | ||
|
|
466ed1f8e6 | ||
|
|
83cdd40191 | ||
|
|
579a6d6064 | ||
|
|
3a5eb4c2ad | ||
|
|
40455108bb | ||
|
|
2c241025c9 | ||
|
|
f5697fd82b | ||
|
|
10d20197ed | ||
|
|
5d5913efeb | ||
|
|
8cc0178b64 | ||
|
|
6a26a5f47d | ||
|
|
01f5ce73a3 | ||
|
|
1e94b1f5bb | ||
|
|
13e8b0dd0d | ||
|
|
f25b830aad | ||
|
|
be0e12edb0 | ||
|
|
6aafd827f9 | ||
|
|
41746eebbb | ||
|
|
fb3019f3f1 | ||
|
|
aa3cb37e05 | ||
|
|
f0a4810b08 | ||
|
|
42e8c56b13 | ||
|
|
ff0e955ccb | ||
|
|
f8ee0561cb | ||
|
|
6c7d71028b | ||
|
|
c63f60ef7c | ||
|
|
79e8a86750 | ||
|
|
aa0e58ef3b | ||
|
|
4c93b51427 | ||
|
|
f4f10f6dd2 | ||
|
|
e9b537a861 | ||
|
|
b385d176a7 | ||
|
|
df2ae076b9 | ||
|
|
9e6e381a40 | ||
|
|
a8bdaba497 | ||
|
|
e3ac64d14a | ||
|
|
c146e5076e | ||
|
|
374d0b4e91 | ||
|
|
c415b6b41f | ||
|
|
3807b0b6cd | ||
|
|
8b05113cc8 | ||
|
|
304b11ee09 | ||
|
|
11b7bf77c0 | ||
|
|
ee2d990ece | ||
|
|
031d844d30 | ||
|
|
33d3c9f0bd | ||
|
|
a6aa1c36b3 | ||
|
|
90542fb499 | ||
|
|
70031c63b6 | ||
|
|
b781cdad6d | ||
|
|
504c163570 | ||
|
|
5f294bc587 | ||
|
|
b202da7d84 | ||
|
|
43525d622a | ||
|
|
0979c67b63 | ||
|
|
90abe7bbe7 | ||
|
|
9c9d6248ae | ||
|
|
8c7fef47a4 | ||
|
|
78f1afa959 | ||
|
|
f5d358d3cf | ||
|
|
55cd72cf62 | ||
|
|
11d62f6c13 | ||
|
|
4182c03258 | ||
|
|
2da96e3169 | ||
|
|
8e3c95b48c | ||
|
|
3c3f57ece1 | ||
|
|
69ec5fd82e | ||
|
|
386895f800 | ||
|
|
6065237de4 | ||
|
|
a7617745e2 | ||
|
|
923f28225a | ||
|
|
7dfef27886 | ||
|
|
5c698908a4 | ||
|
|
d88089fb24 | ||
|
|
0188d03d1b | ||
|
|
7b6ea338ed | ||
|
|
40e88ded25 | ||
|
|
38bcc2b736 | ||
|
|
73e6941817 | ||
|
|
1268c119b5 | ||
|
|
434edce8a7 | ||
|
|
c0ceaee122 | ||
|
|
e9a053c18d | ||
|
|
427f3bf163 | ||
|
|
96cfc49cb9 | ||
|
|
dc65118f17 | ||
|
|
31025a0a7f | ||
|
|
d20c301dcd | ||
|
|
0a382c411f | ||
|
|
367bf6849b | ||
|
|
848716607c | ||
|
|
0a0e71a9ff | ||
|
|
ca232dbdf4 | ||
|
|
efcf78b348 | ||
|
|
bbc0d30fbf | ||
|
|
1c60882525 | ||
|
|
d6cb415f76 | ||
|
|
f58bf878d5 | ||
|
|
2fbc716615 | ||
|
|
cce99b1ff2 | ||
|
|
f98b1669fa | ||
|
|
efebabe44f | ||
|
|
42a0461f57 | ||
|
|
a23ad3ad4a | ||
|
|
1478be716d | ||
|
|
0eed51dfdd | ||
|
|
574d65ab4b | ||
|
|
ac336b20ca | ||
|
|
c9613016e2 | ||
|
|
f9696a85d3 | ||
|
|
deb97546c0 | ||
|
|
cea964910d | ||
|
|
f8b99f2e34 | ||
|
|
3a7e328c6e | ||
|
|
732792fc80 | ||
|
|
dd842ac3b8 | ||
|
|
bdcf10e209 | ||
|
|
a43edd9dd8 | ||
|
|
65d9c187b8 | ||
|
|
75e1c1dac8 | ||
|
|
28ccdcc52d | ||
|
|
7c34ff166a | ||
|
|
36bf479115 | ||
|
|
3b90bfcdc3 | ||
|
|
ab19135ca6 | ||
|
|
41f7b8cb9d | ||
|
|
d8b9541b86 | ||
|
|
a4620adbce | ||
|
|
02ac87d160 | ||
|
|
f4c62726ee | ||
|
|
0844be5a28 | ||
|
|
3a77626cd7 | ||
|
|
4fee33c020 | ||
|
|
e3c53ee874 | ||
|
|
367eff77fc | ||
|
|
1bed6a1b96 | ||
|
|
1cf9220380 | ||
|
|
fd8629a6cf | ||
|
|
be1ca971be | ||
|
|
5fa59d86e7 | ||
|
|
be80009aca | ||
|
|
f18f6af5e0 | ||
|
|
65f14723ca | ||
|
|
6910847c71 | ||
|
|
62b19b53f9 | ||
|
|
8fb6592c6a | ||
|
|
cda35e2a9a | ||
|
|
4472719362 | ||
|
|
6c12b7a951 | ||
|
|
ad3b63ed5d | ||
|
|
e7545ad183 | ||
|
|
e390595551 | ||
|
|
b77b3a4566 | ||
|
|
16692e2e5f | ||
|
|
757bdf69dc | ||
|
|
a259f154da | ||
|
|
e441c6dfa7 | ||
|
|
c4c7af88c0 | ||
|
|
d78b249827 | ||
|
|
af8cdedc07 | ||
|
|
fc7a62b650 | ||
|
|
7919c90b81 | ||
|
|
1854d20566 | ||
|
|
c5c5c3d863 | ||
|
|
6c47ae1d88 | ||
|
|
70ad200dca | ||
|
|
341956768a | ||
|
|
0656477131 | ||
|
|
e878d0e76e | ||
|
|
dc6a38c360 | ||
|
|
f883e11c7a | ||
|
|
7e4402b128 | ||
|
|
e045e14522 | ||
|
|
f209e17215 | ||
|
|
07461d24e0 | ||
|
|
47cef1d907 | ||
|
|
c59adc3369 | ||
|
|
cc3ba20ec9 | ||
|
|
562f3d0bb7 | ||
|
|
e8d321cbab | ||
|
|
1edffac2bc | ||
|
|
735ffb3c92 | ||
|
|
66b60d860e | ||
|
|
f99d31f7c5 | ||
|
|
170b1d80b5 | ||
|
|
10846653f0 | ||
|
|
5d4d4f32d2 | ||
|
|
1f49150d30 | ||
|
|
3f583bd927 | ||
|
|
d11987a2ad | ||
|
|
79f7985784 | ||
|
|
104422eed9 | ||
|
|
d77e4205c1 | ||
|
|
847bc0995b | ||
|
|
529b053620 | ||
|
|
4d72e4e257 | ||
|
|
8e18af5912 | ||
|
|
bee5bb8660 | ||
|
|
e486872d81 | ||
|
|
6e8d4a6fa6 | ||
|
|
9081303a97 | ||
|
|
40c07afd85 | ||
|
|
da46a679f3 | ||
|
|
283405dc17 | ||
|
|
ed38cbbc0b | ||
|
|
ad91636c74 | ||
|
|
5543dab97c | ||
|
|
d4ae9a89df | ||
|
|
032faf2dce | ||
|
|
bcea7cc8ad | ||
|
|
0ad94f80cd | ||
|
|
7e9533afc7 | ||
|
|
6ca6233387 | ||
|
|
503050904c | ||
|
|
ba2f9d5781 | ||
|
|
e571cbaeec | ||
|
|
202f8a1678 | ||
|
|
4068c6694e | ||
|
|
7d43037d4e | ||
|
|
d6bdb466ae | ||
|
|
1114885429 | ||
|
|
e29078e5d1 | ||
|
|
2bd56de934 | ||
|
|
df82cd5aec | ||
|
|
67571f4ede | ||
|
|
fa22c5b071 | ||
|
|
5b7c81eb2d | ||
|
|
8b20136571 | ||
|
|
2a426edfe5 | ||
|
|
5dd9c997e2 | ||
|
|
be5fa32c0c | ||
|
|
0c3fb9528a | ||
|
|
d5c24b0c63 | ||
|
|
9b8e06c51d | ||
|
|
ea4948ff8a | ||
|
|
5dc939355a | ||
|
|
2672859ee0 | ||
|
|
542a65323f | ||
|
|
85e6d7f649 | ||
|
|
e73e276e2b | ||
|
|
1879fb5109 | ||
|
|
15cac5afc4 | ||
|
|
1ae17d5745 | ||
|
|
7106a7c1d1 | ||
|
|
0115e469c1 | ||
|
|
7729295bce | ||
|
|
3265825446 | ||
|
|
90fdc9b2e2 | ||
|
|
9853951aa5 | ||
|
|
b839c79144 | ||
|
|
fad8813911 | ||
|
|
ed57876068 | ||
|
|
5143dbb167 | ||
|
|
75126aa167 | ||
|
|
00a1840a46 | ||
|
|
127f8768aa | ||
|
|
6e330fa993 | ||
|
|
c6ab6459fb | ||
|
|
4e58589afd | ||
|
|
89c65affea | ||
|
|
099b487560 | ||
|
|
25235a4d5a | ||
|
|
1d329e0364 | ||
|
|
d07f23395c | ||
|
|
5e3eba6662 | ||
|
|
61adfe42ff | ||
|
|
80e7e71526 | ||
|
|
1e6790ab05 | ||
|
|
18358db467 | ||
|
|
2eea89a0a6 | ||
|
|
dcce90bcc4 | ||
|
|
b5fc1ef4a3 | ||
|
|
4ae129d565 | ||
|
|
3ec9d358f7 | ||
|
|
1fc4718d36 | ||
|
|
bf3f5e1b92 | ||
|
|
20496651c5 | ||
|
|
9df123a180 | ||
|
|
e7b597787d | ||
|
|
048b96b540 | ||
|
|
be29156f6f | ||
|
|
140cfd70ce | ||
|
|
2e5fdbf5ce | ||
|
|
ee710e3922 | ||
|
|
b8febddad5 | ||
|
|
da74681de2 | ||
|
|
c8605647f1 | ||
|
|
032d77dd15 | ||
|
|
0df49c59c0 | ||
|
|
d354aa6f09 | ||
|
|
551bf42c66 | ||
|
|
b691a73dba | ||
|
|
26720997ea | ||
|
|
431ce50964 | ||
|
|
04bc2a800a | ||
|
|
ccab27a1e2 | ||
|
|
fa40c41124 | ||
|
|
1881c0b932 | ||
|
|
0d44dbf078 | ||
|
|
c5a6634d59 | ||
|
|
81dee7b857 | ||
|
|
b1f88cb1a3 | ||
|
|
f2ba4b03a2 | ||
|
|
c1af3abbdc | ||
|
|
0bf6ecafa0 | ||
|
|
0658a420ee | ||
|
|
5823683265 | ||
|
|
5b65591cc2 | ||
|
|
7fae368c0d | ||
|
|
04ebdbb00b | ||
|
|
b030b4eba5 | ||
|
|
b01226fe62 | ||
|
|
e080bfc79a | ||
|
|
9eaf2438dd | ||
|
|
bd5e6bd1e6 | ||
|
|
62755fe5b1 | ||
|
|
e570d22f40 | ||
|
|
3e48dc3b4d | ||
|
|
9b06679d56 | ||
|
|
9cc7a7a87f | ||
|
|
4e23e7dbd1 | ||
|
|
d9ec8125c9 | ||
|
|
73c418cf57 | ||
|
|
8b722c7951 | ||
|
|
fdfcd4c651 | ||
|
|
4357c3c74e | ||
|
|
5c80d6ab4e | ||
|
|
199b1ed1dc | ||
|
|
b9f8dedc8b | ||
|
|
96a9c6cb92 | ||
|
|
2ad5122ef4 | ||
|
|
1f749805a5 | ||
|
|
de5ff68943 | ||
|
|
e7d2221d08 | ||
|
|
0b8f3bc4c5 | ||
|
|
9d5488e361 | ||
|
|
80574a5a6a | ||
|
|
affdbe719a | ||
|
|
ba3e14cefd | ||
|
|
105dc001e5 | ||
|
|
8bdaa69a2c | ||
|
|
760d46af5c | ||
|
|
21c3f41814 | ||
|
|
5c42f1af14 | ||
|
|
7a89ef2465 | ||
|
|
38244bfdb3 | ||
|
|
e7759b4bae | ||
|
|
66a4f76c74 | ||
|
|
80c9e623ef | ||
|
|
722d87ca5c | ||
|
|
f0221b1fdb | ||
|
|
51e65a35a6 | ||
|
|
a8b93f6d48 | ||
|
|
9b7de69f08 | ||
|
|
554b32efb5 | ||
|
|
c4a5eb9803 | ||
|
|
f92f389612 | ||
|
|
1021637283 | ||
|
|
d85a54a01c | ||
|
|
77dceab7be | ||
|
|
01e97ff285 | ||
|
|
54b64f8922 | ||
|
|
706cea8996 | ||
|
|
1a72660a3f | ||
|
|
efe75d9b51 | ||
|
|
b11aab5369 | ||
|
|
a348c3f7c1 | ||
|
|
78a731b9e2 | ||
|
|
94ede83462 | ||
|
|
1085fb8cf4 | ||
|
|
0a5b436da1 | ||
|
|
4ed1986c26 | ||
|
|
33b4e72401 | ||
|
|
c36a463f44 | ||
|
|
f85015365f | ||
|
|
b9a47cafb2 | ||
|
|
37e373a68f | ||
|
|
ff9013d977 | ||
|
|
39f92e527c | ||
|
|
f1ef68c166 | ||
|
|
260752f501 | ||
|
|
dea5f98c4e | ||
|
|
38d51cd97c | ||
|
|
22db36c4a6 | ||
|
|
75356cd9ce | ||
|
|
df05037841 | ||
|
|
1235b3f468 | ||
|
|
99e778b9b7 | ||
|
|
c1b3690671 | ||
|
|
4927674306 | ||
|
|
eb464752fb | ||
|
|
210092eeda | ||
|
|
9f7ecd28f9 | ||
|
|
a05b013ecc | ||
|
|
bc49fc2e10 | ||
|
|
344af97656 | ||
|
|
ac650c1034 | ||
|
|
b411630ad4 | ||
|
|
c1815d6750 | ||
|
|
f7d39ae7b3 | ||
|
|
74672c8fad | ||
|
|
d4108ca840 | ||
|
|
499aa57ac2 | ||
|
|
62e22c27d5 | ||
|
|
58a371f9d9 | ||
|
|
c28f2dc5c1 | ||
|
|
0b18212e69 | ||
|
|
1dd4187cd0 | ||
|
|
9e36ebdaab | ||
|
|
aaa0127354 | ||
|
|
e60280c4d6 | ||
|
|
aef7866e29 | ||
|
|
70694e3bb9 |
@@ -1,23 +1,27 @@
|
||||
[bumpversion]
|
||||
current_version = 0.15.1
|
||||
current_version = 1.0.1
|
||||
parse = (?P<major>\d+)
|
||||
\.(?P<minor>\d+)
|
||||
\.(?P<patch>\d+)
|
||||
((?P<prerelease>[a-z]+)(?P<num>\d+))?
|
||||
((?P<prekind>a|b|rc)
|
||||
(?P<pre>\d+) # pre-release version num
|
||||
)?
|
||||
serialize =
|
||||
{major}.{minor}.{patch}{prerelease}{num}
|
||||
{major}.{minor}.{patch}{prekind}{pre}
|
||||
{major}.{minor}.{patch}
|
||||
commit = False
|
||||
tag = False
|
||||
|
||||
[bumpversion:part:prerelease]
|
||||
[bumpversion:part:prekind]
|
||||
first_value = a
|
||||
optional_value = final
|
||||
values =
|
||||
a
|
||||
b
|
||||
rc
|
||||
final
|
||||
|
||||
[bumpversion:part:num]
|
||||
[bumpversion:part:pre]
|
||||
first_value = 1
|
||||
|
||||
[bumpversion:file:setup.py]
|
||||
@@ -26,11 +30,8 @@ first_value = 1
|
||||
|
||||
[bumpversion:file:core/dbt/version.py]
|
||||
|
||||
[bumpversion:file:core/scripts/create_adapter_plugins.py]
|
||||
|
||||
[bumpversion:file:plugins/postgres/setup.py]
|
||||
|
||||
[bumpversion:file:plugins/redshift/setup.py]
|
||||
|
||||
[bumpversion:file:plugins/snowflake/setup.py]
|
||||
|
||||
[bumpversion:file:plugins/bigquery/setup.py]
|
||||
|
||||
[bumpversion:file:plugins/postgres/dbt/adapters/postgres/__version__.py]
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
version: 2
|
||||
jobs:
|
||||
unit:
|
||||
docker: &test_and_postgres
|
||||
- image: fishtownjacob/test-container:2
|
||||
- image: postgres
|
||||
name: database
|
||||
environment: &pgenv
|
||||
POSTGRES_USER: "root"
|
||||
POSTGRES_PASSWORD: "password"
|
||||
POSTGRES_DB: "dbt"
|
||||
steps:
|
||||
- checkout
|
||||
- run: &setupdb
|
||||
name: Setup postgres
|
||||
command: bash test/setup_db.sh
|
||||
environment:
|
||||
PGHOST: database
|
||||
PGUSER: root
|
||||
PGPASSWORD: password
|
||||
PGDATABASE: postgres
|
||||
- run: tox -e flake8,mypy,unit-py36,unit-py38
|
||||
integration-postgres-py36:
|
||||
docker: *test_and_postgres
|
||||
steps:
|
||||
- checkout
|
||||
- run: *setupdb
|
||||
- run:
|
||||
name: Run tests
|
||||
command: tox -e integration-postgres-py36
|
||||
- store_artifacts:
|
||||
path: ./logs
|
||||
integration-snowflake-py36:
|
||||
docker: &test_only
|
||||
- image: fishtownjacob/test-container:2
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Run tests
|
||||
command: tox -e integration-snowflake-py36
|
||||
no_output_timeout: 1h
|
||||
- store_artifacts:
|
||||
path: ./logs
|
||||
integration-redshift-py36:
|
||||
docker: *test_only
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Run tests
|
||||
command: tox -e integration-redshift-py36
|
||||
- store_artifacts:
|
||||
path: ./logs
|
||||
integration-bigquery-py36:
|
||||
docker: *test_only
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Run tests
|
||||
command: tox -e integration-bigquery-py36
|
||||
- store_artifacts:
|
||||
path: ./logs
|
||||
integration-postgres-py38:
|
||||
docker: *test_and_postgres
|
||||
steps:
|
||||
- checkout
|
||||
- run: *setupdb
|
||||
- run:
|
||||
name: Run tests
|
||||
command: tox -e integration-postgres-py38
|
||||
- store_artifacts:
|
||||
path: ./logs
|
||||
integration-snowflake-py38:
|
||||
docker: *test_only
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Run tests
|
||||
command: tox -e integration-snowflake-py38
|
||||
no_output_timeout: 1h
|
||||
- store_artifacts:
|
||||
path: ./logs
|
||||
integration-redshift-py38:
|
||||
docker: *test_only
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Run tests
|
||||
command: tox -e integration-redshift-py38
|
||||
- store_artifacts:
|
||||
path: ./logs
|
||||
integration-bigquery-py38:
|
||||
docker: *test_only
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Run tests
|
||||
command: tox -e integration-bigquery-py38
|
||||
- store_artifacts:
|
||||
path: ./logs
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
test-everything:
|
||||
jobs:
|
||||
- unit
|
||||
- integration-postgres-py36:
|
||||
requires:
|
||||
- unit
|
||||
- integration-redshift-py36:
|
||||
requires:
|
||||
- integration-postgres-py36
|
||||
- integration-bigquery-py36:
|
||||
requires:
|
||||
- integration-postgres-py36
|
||||
- integration-snowflake-py36:
|
||||
requires:
|
||||
- integration-postgres-py36
|
||||
- integration-postgres-py38:
|
||||
requires:
|
||||
- unit
|
||||
- integration-redshift-py38:
|
||||
requires:
|
||||
- integration-postgres-py38
|
||||
- integration-bigquery-py38:
|
||||
requires:
|
||||
- integration-postgres-py38
|
||||
- integration-snowflake-py38:
|
||||
requires:
|
||||
- integration-postgres-py38
|
||||
@@ -1 +1,3 @@
|
||||
*
|
||||
!docker/requirements/*.txt
|
||||
!dist
|
||||
|
||||
43
.github/CODEOWNERS
vendored
Normal file
43
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# This file contains the code owners for the dbt-core repo.
|
||||
# PRs will be automatically assigned for review to the associated
|
||||
# team(s) or person(s) that touches any files that are mapped to them.
|
||||
#
|
||||
# A statement takes precedence over the statements above it so more general
|
||||
# assignments are found at the top with specific assignments being lower in
|
||||
# the ordering (i.e. catch all assignment should be the first item)
|
||||
#
|
||||
# Consult GitHub documentation for formatting guidelines:
|
||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#example-of-a-codeowners-file
|
||||
|
||||
# As a default for areas with no assignment,
|
||||
# the core team as a whole will be assigned
|
||||
* @dbt-labs/core
|
||||
|
||||
# Changes to GitHub configurations including Actions
|
||||
/.github/ @leahwicz
|
||||
|
||||
# Language core modules
|
||||
/core/dbt/config/ @dbt-labs/core-language
|
||||
/core/dbt/context/ @dbt-labs/core-language
|
||||
/core/dbt/contracts/ @dbt-labs/core-language
|
||||
/core/dbt/deps/ @dbt-labs/core-language
|
||||
/core/dbt/parser/ @dbt-labs/core-language
|
||||
|
||||
# Execution core modules
|
||||
/core/dbt/events/ @dbt-labs/core-execution @dbt-labs/core-language # eventually remove language but they have knowledge here now
|
||||
/core/dbt/graph/ @dbt-labs/core-execution
|
||||
/core/dbt/task/ @dbt-labs/core-execution
|
||||
|
||||
# Adapter interface, scaffold, Postgres plugin
|
||||
/core/dbt/adapters @dbt-labs/core-adapters
|
||||
/core/scripts/create_adapter_plugin.py @dbt-labs/core-adapters
|
||||
/plugins/ @dbt-labs/core-adapters
|
||||
|
||||
# Global project: default macros, including generic tests + materializations
|
||||
/core/dbt/include/global_project @dbt-labs/core-execution @dbt-labs/core-adapters
|
||||
|
||||
# Perf regression testing framework
|
||||
# This excludes the test project files itself since those aren't specific
|
||||
# framework changes (excluded by not setting an owner next to it- no owner)
|
||||
/performance @nathaniel-may
|
||||
/performance/projects
|
||||
85
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
85
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
name: 🐞 Bug
|
||||
description: Report a bug or an issue you've found with dbt
|
||||
title: "[Bug] <title>"
|
||||
labels: ["bug", "triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: A concise description of what you're experiencing.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A concise description of what you expected to happen.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. In this environment...
|
||||
2. With this config...
|
||||
3. Run '...'
|
||||
4. See error...
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: |
|
||||
If applicable, log output to help explain your problem.
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
examples:
|
||||
- **OS**: Ubuntu 20.04
|
||||
- **Python**: 3.7.2 (`python --version`)
|
||||
- **dbt**: 0.21.0 (`dbt --version`)
|
||||
value: |
|
||||
- OS:
|
||||
- Python:
|
||||
- dbt:
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: database
|
||||
attributes:
|
||||
label: What database are you using dbt with?
|
||||
multiple: true
|
||||
options:
|
||||
- postgres
|
||||
- redshift
|
||||
- snowflake
|
||||
- bigquery
|
||||
- other (mention it in "Additional Context")
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: |
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
41
.github/ISSUE_TEMPLATE/bug_report.md
vendored
41
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,41 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a bug or an issue you've found with dbt
|
||||
title: ''
|
||||
labels: bug, triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Describe the bug
|
||||
A clear and concise description of what the bug is. What command did you run? What happened?
|
||||
|
||||
### Steps To Reproduce
|
||||
In as much detail as possible, please provide steps to reproduce the issue. Sample data that triggers the issue, example model code, etc is all very helpful here.
|
||||
|
||||
### Expected behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
### Screenshots and log output
|
||||
If applicable, add screenshots or log output to help explain your problem.
|
||||
|
||||
### System information
|
||||
**Which database are you using dbt with?**
|
||||
- [ ] postgres
|
||||
- [ ] redshift
|
||||
- [ ] bigquery
|
||||
- [ ] snowflake
|
||||
- [ ] other (specify: ____________)
|
||||
|
||||
|
||||
**The output of `dbt --version`:**
|
||||
```
|
||||
<output goes here>
|
||||
```
|
||||
|
||||
**The operating system you're using:**
|
||||
|
||||
**The output of `python --version`:**
|
||||
|
||||
### Additional context
|
||||
Add any other context about the problem here.
|
||||
16
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
16
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
contact_links:
|
||||
- name: Create an issue for dbt-redshift
|
||||
url: https://github.com/dbt-labs/dbt-redshift/issues/new/choose
|
||||
about: Report a bug or request a feature for dbt-redshift
|
||||
- name: Create an issue for dbt-bigquery
|
||||
url: https://github.com/dbt-labs/dbt-bigquery/issues/new/choose
|
||||
about: Report a bug or request a feature for dbt-bigquery
|
||||
- name: Create an issue for dbt-snowflake
|
||||
url: https://github.com/dbt-labs/dbt-snowflake/issues/new/choose
|
||||
about: Report a bug or request a feature for dbt-snowflake
|
||||
- name: Ask a question or get support
|
||||
url: https://docs.getdbt.com/docs/guides/getting-help
|
||||
about: Ask a question or request support
|
||||
- name: Questions on Stack Overflow
|
||||
url: https://stackoverflow.com/questions/tagged/dbt
|
||||
about: Look at questions/answers at Stack Overflow
|
||||
49
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
49
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: ✨ Feature
|
||||
description: Suggest an idea for dbt
|
||||
title: "[Feature] <title>"
|
||||
labels: ["enhancement", "triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this feature requests!
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing feature request for this?
|
||||
description: Please search to see if an issue already exists for the feature you would like.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Feature
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: |
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Who will this benefit?
|
||||
description: |
|
||||
What kind of use case will this feature be useful for? Please be specific and provide examples, this will help us prioritize properly.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
attributes:
|
||||
label: Are you interested in contributing this feature?
|
||||
description: Let us know if you want to write some code, and how we can help.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? References? Anything that will give us more context about the feature you are suggesting!
|
||||
validations:
|
||||
required: false
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for dbt
|
||||
title: ''
|
||||
labels: enhancement, triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Describe the feature
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
### Describe alternatives you've considered
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
### Additional context
|
||||
Is this feature database-specific? Which database(s) is/are relevant? Please include any other relevant context here.
|
||||
|
||||
### Who will this benefit?
|
||||
What kind of use case will this feature be useful for? Please be specific and provide examples, this will help us prioritize properly.
|
||||
@@ -1,21 +0,0 @@
|
||||
resolves #
|
||||
|
||||
<!---
|
||||
Include the number of the issue addressed by this PR above if applicable.
|
||||
PRs for code changes without an associated issue *will not be merged*.
|
||||
See CONTRIBUTING.md for more information.
|
||||
|
||||
Example:
|
||||
resolves #1234
|
||||
-->
|
||||
|
||||
|
||||
### Description
|
||||
|
||||
<!--- Describe the Pull Request here -->
|
||||
|
||||
|
||||
### Checklist
|
||||
- [ ] I have signed the [CLA](https://docs.getdbt.com/docs/contributor-license-agreements)
|
||||
- [ ] I have run this code in development and it appears to resolve the stated issue
|
||||
- [ ] This PR includes tests, or tests are not required/relevant for this PR
|
||||
10
.github/actions/setup-postgres-linux/action.yml
vendored
Normal file
10
.github/actions/setup-postgres-linux/action.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
name: "Set up postgres (linux)"
|
||||
description: "Set up postgres service on linux vm for dbt integration tests"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- shell: bash
|
||||
run: |
|
||||
sudo systemctl start postgresql.service
|
||||
pg_isready
|
||||
sudo -u postgres bash ${{ github.action_path }}/setup_db.sh
|
||||
1
.github/actions/setup-postgres-linux/setup_db.sh
vendored
Symbolic link
1
.github/actions/setup-postgres-linux/setup_db.sh
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../test/setup_db.sh
|
||||
24
.github/actions/setup-postgres-macos/action.yml
vendored
Normal file
24
.github/actions/setup-postgres-macos/action.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: "Set up postgres (macos)"
|
||||
description: "Set up postgres service on macos vm for dbt integration tests"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- shell: bash
|
||||
run: |
|
||||
brew services start postgresql
|
||||
echo "Check PostgreSQL service is running"
|
||||
i=10
|
||||
COMMAND='pg_isready'
|
||||
while [ $i -gt -1 ]; do
|
||||
if [ $i == 0 ]; then
|
||||
echo "PostgreSQL service not ready, all attempts exhausted"
|
||||
exit 1
|
||||
fi
|
||||
echo "Check PostgreSQL service status"
|
||||
eval $COMMAND && break
|
||||
echo "PostgreSQL service not ready, wait 10 more sec, attempts left: $i"
|
||||
sleep 10
|
||||
((i--))
|
||||
done
|
||||
createuser -s postgres
|
||||
bash ${{ github.action_path }}/setup_db.sh
|
||||
1
.github/actions/setup-postgres-macos/setup_db.sh
vendored
Symbolic link
1
.github/actions/setup-postgres-macos/setup_db.sh
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../test/setup_db.sh
|
||||
12
.github/actions/setup-postgres-windows/action.yml
vendored
Normal file
12
.github/actions/setup-postgres-windows/action.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: "Set up postgres (windows)"
|
||||
description: "Set up postgres service on windows vm for dbt integration tests"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- shell: pwsh
|
||||
run: |
|
||||
$pgService = Get-Service -Name postgresql*
|
||||
Set-Service -InputObject $pgService -Status running -StartupType automatic
|
||||
Start-Process -FilePath "$env:PGBIN\pg_isready" -Wait -PassThru
|
||||
$env:Path += ";$env:PGBIN"
|
||||
bash ${{ github.action_path }}/setup_db.sh
|
||||
1
.github/actions/setup-postgres-windows/setup_db.sh
vendored
Symbolic link
1
.github/actions/setup-postgres-windows/setup_db.sh
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../test/setup_db.sh
|
||||
30
.github/dependabot.yml
vendored
Normal file
30
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
version: 2
|
||||
updates:
|
||||
# python dependencies
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
rebase-strategy: "disabled"
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/core"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
rebase-strategy: "disabled"
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/plugins/postgres"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
rebase-strategy: "disabled"
|
||||
|
||||
# docker dependencies
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
rebase-strategy: "disabled"
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/docker"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
rebase-strategy: "disabled"
|
||||
21
.github/pull_request_template.md
vendored
Normal file
21
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
resolves #
|
||||
|
||||
<!---
|
||||
Include the number of the issue addressed by this PR above if applicable.
|
||||
PRs for code changes without an associated issue *will not be merged*.
|
||||
See CONTRIBUTING.md for more information.
|
||||
-->
|
||||
|
||||
### Description
|
||||
|
||||
<!---
|
||||
Describe the Pull Request here. Add any references and info to help reviewers
|
||||
understand your changes. Include any tradeoffs you considered.
|
||||
-->
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] I have signed the [CLA](https://docs.getdbt.com/docs/contributor-license-agreements)
|
||||
- [ ] I have run this code in development and it appears to resolve the stated issue
|
||||
- [ ] This PR includes tests, or tests are not required/relevant for this PR
|
||||
- [ ] I have updated the `CHANGELOG.md` and added information about my change
|
||||
95
.github/scripts/integration-test-matrix.js
vendored
Normal file
95
.github/scripts/integration-test-matrix.js
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
module.exports = ({ context }) => {
|
||||
const defaultPythonVersion = "3.8";
|
||||
const supportedPythonVersions = ["3.7", "3.8", "3.9"];
|
||||
const supportedAdapters = ["postgres"];
|
||||
|
||||
// if PR, generate matrix based on files changed and PR labels
|
||||
if (context.eventName.includes("pull_request")) {
|
||||
// `changes` is a list of adapter names that have related
|
||||
// file changes in the PR
|
||||
// ex: ['postgres', 'snowflake']
|
||||
const changes = JSON.parse(process.env.CHANGES);
|
||||
const labels = context.payload.pull_request.labels.map(({ name }) => name);
|
||||
console.log("labels", labels);
|
||||
console.log("changes", changes);
|
||||
const testAllLabel = labels.includes("test all");
|
||||
const include = [];
|
||||
|
||||
for (const adapter of supportedAdapters) {
|
||||
if (
|
||||
changes.includes(adapter) ||
|
||||
testAllLabel ||
|
||||
labels.includes(`test ${adapter}`)
|
||||
) {
|
||||
for (const pythonVersion of supportedPythonVersions) {
|
||||
if (
|
||||
pythonVersion === defaultPythonVersion ||
|
||||
labels.includes(`test python${pythonVersion}`) ||
|
||||
testAllLabel
|
||||
) {
|
||||
// always run tests on ubuntu by default
|
||||
include.push({
|
||||
os: "ubuntu-latest",
|
||||
adapter,
|
||||
"python-version": pythonVersion,
|
||||
});
|
||||
|
||||
if (labels.includes("test windows") || testAllLabel) {
|
||||
include.push({
|
||||
os: "windows-latest",
|
||||
adapter,
|
||||
"python-version": pythonVersion,
|
||||
});
|
||||
}
|
||||
|
||||
if (labels.includes("test macos") || testAllLabel) {
|
||||
include.push({
|
||||
os: "macos-latest",
|
||||
adapter,
|
||||
"python-version": pythonVersion,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("matrix", { include });
|
||||
|
||||
return {
|
||||
include,
|
||||
};
|
||||
}
|
||||
// if not PR, generate matrix of python version, adapter, and operating
|
||||
// system to run integration tests on
|
||||
|
||||
const include = [];
|
||||
// run for all adapters and python versions on ubuntu
|
||||
for (const adapter of supportedAdapters) {
|
||||
for (const pythonVersion of supportedPythonVersions) {
|
||||
include.push({
|
||||
os: 'ubuntu-latest',
|
||||
adapter: adapter,
|
||||
"python-version": pythonVersion,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// additionally include runs for all adapters, on macos and windows,
|
||||
// but only for the default python version
|
||||
for (const adapter of supportedAdapters) {
|
||||
for (const operatingSystem of ["windows-latest", "macos-latest"]) {
|
||||
include.push({
|
||||
os: operatingSystem,
|
||||
adapter: adapter,
|
||||
"python-version": defaultPythonVersion,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log("matrix", { include });
|
||||
|
||||
return {
|
||||
include,
|
||||
};
|
||||
};
|
||||
34
.github/workflows/backport.yml
vendored
Normal file
34
.github/workflows/backport.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# **what?**
|
||||
# When a PR is merged, if it has the backport label, it will create
|
||||
# a new PR to backport those changes to the given branch. If it can't
|
||||
# cleanly do a backport, it will comment on the merged PR of the failure.
|
||||
#
|
||||
# Label naming convention: "backport <branch name to backport to>"
|
||||
# Example: backport 1.0.latest
|
||||
#
|
||||
# You MUST "Squash and merge" the original PR or this won't work.
|
||||
|
||||
# **why?**
|
||||
# Changes sometimes need to be backported to release branches.
|
||||
# This automates the backporting process
|
||||
|
||||
# **when?**
|
||||
# Once a PR is "Squash and merge"'d and it has been correctly labeled
|
||||
# according to the naming convention.
|
||||
|
||||
name: Backport
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
runs-on: ubuntu-18.04
|
||||
name: Backport
|
||||
steps:
|
||||
- name: Backport
|
||||
uses: tibdex/backport@v1.1.1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
222
.github/workflows/integration.yml
vendored
Normal file
222
.github/workflows/integration.yml
vendored
Normal file
@@ -0,0 +1,222 @@
|
||||
# **what?**
|
||||
# This workflow runs all integration tests for supported OS
|
||||
# and python versions and core adapters. If triggered by PR,
|
||||
# the workflow will only run tests for adapters related
|
||||
# to code changes. Use the `test all` and `test ${adapter}`
|
||||
# label to run all or additional tests. Use `ok to test`
|
||||
# label to mark PRs from forked repositories that are safe
|
||||
# to run integration tests for. Requires secrets to run
|
||||
# against different warehouses.
|
||||
|
||||
# **why?**
|
||||
# This checks the functionality of dbt from a user's perspective
|
||||
# and attempts to catch functional regressions.
|
||||
|
||||
# **when?**
|
||||
# This workflow will run on every push to a protected branch
|
||||
# and when manually triggered. It will also run for all PRs, including
|
||||
# PRs from forks. The workflow will be skipped until there is a label
|
||||
# to mark the PR as safe to run.
|
||||
|
||||
name: Adapter Integration Tests
|
||||
|
||||
on:
|
||||
# pushes to release branches
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "develop"
|
||||
- "*.latest"
|
||||
- "releases/*"
|
||||
# all PRs, important to note that `pull_request_target` workflows
|
||||
# will run in the context of the target branch of a PR
|
||||
pull_request_target:
|
||||
# manual tigger
|
||||
workflow_dispatch:
|
||||
|
||||
# explicitly turn off permissions for `GITHUB_TOKEN`
|
||||
permissions: read-all
|
||||
|
||||
# will cancel previous workflows triggered by the same event and for the same ref for PRs or same SHA otherwise
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.ref || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# sets default shell to bash, for all operating systems
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
# generate test metadata about what files changed and the testing matrix to use
|
||||
test-metadata:
|
||||
# run if not a PR from a forked repository or has a label to mark as safe to test
|
||||
if: >-
|
||||
github.event_name != 'pull_request_target' ||
|
||||
github.event.pull_request.head.repo.full_name == github.repository ||
|
||||
contains(github.event.pull_request.labels.*.name, 'ok to test')
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
matrix: ${{ steps.generate-matrix.outputs.result }}
|
||||
|
||||
steps:
|
||||
- name: Check out the repository (non-PR)
|
||||
if: github.event_name != 'pull_request_target'
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check out the repository (PR)
|
||||
if: github.event_name == 'pull_request_target'
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Check if relevant files changed
|
||||
# https://github.com/marketplace/actions/paths-changes-filter
|
||||
# For each filter, it sets output variable named by the filter to the text:
|
||||
# 'true' - if any of changed files matches any of filter rules
|
||||
# 'false' - if none of changed files matches any of filter rules
|
||||
# also, returns:
|
||||
# `changes` - JSON array with names of all filters matching any of the changed files
|
||||
uses: dorny/paths-filter@v2
|
||||
id: get-changes
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
filters: |
|
||||
postgres:
|
||||
- 'core/**'
|
||||
- 'plugins/postgres/**'
|
||||
- 'dev-requirements.txt'
|
||||
|
||||
- name: Generate integration test matrix
|
||||
id: generate-matrix
|
||||
uses: actions/github-script@v4
|
||||
env:
|
||||
CHANGES: ${{ steps.get-changes.outputs.changes }}
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/scripts/integration-test-matrix.js')
|
||||
const matrix = script({ context })
|
||||
console.log(matrix)
|
||||
return matrix
|
||||
|
||||
test:
|
||||
name: ${{ matrix.adapter }} / python ${{ matrix.python-version }} / ${{ matrix.os }}
|
||||
|
||||
# run if not a PR from a forked repository or has a label to mark as safe to test
|
||||
# also checks that the matrix generated is not empty
|
||||
if: >-
|
||||
needs.test-metadata.outputs.matrix &&
|
||||
fromJSON( needs.test-metadata.outputs.matrix ).include[0] &&
|
||||
(
|
||||
github.event_name != 'pull_request_target' ||
|
||||
github.event.pull_request.head.repo.full_name == github.repository ||
|
||||
contains(github.event.pull_request.labels.*.name, 'ok to test')
|
||||
)
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
needs: test-metadata
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.test-metadata.outputs.matrix) }}
|
||||
|
||||
env:
|
||||
TOXENV: integration-${{ matrix.adapter }}
|
||||
PYTEST_ADDOPTS: "-v --color=yes -n4 --csv integration_results.csv"
|
||||
DBT_INVOCATION_ENV: github-actions
|
||||
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
if: github.event_name != 'pull_request_target'
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# explicity checkout the branch for the PR,
|
||||
# this is necessary for the `pull_request_target` event
|
||||
- name: Check out the repository (PR)
|
||||
if: github.event_name == 'pull_request_target'
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Set up postgres (linux)
|
||||
if: |
|
||||
matrix.adapter == 'postgres' &&
|
||||
runner.os == 'Linux'
|
||||
uses: ./.github/actions/setup-postgres-linux
|
||||
|
||||
- name: Set up postgres (macos)
|
||||
if: |
|
||||
matrix.adapter == 'postgres' &&
|
||||
runner.os == 'macOS'
|
||||
uses: ./.github/actions/setup-postgres-macos
|
||||
|
||||
- name: Set up postgres (windows)
|
||||
if: |
|
||||
matrix.adapter == 'postgres' &&
|
||||
runner.os == 'Windows'
|
||||
uses: ./.github/actions/setup-postgres-windows
|
||||
|
||||
- name: Install python dependencies
|
||||
run: |
|
||||
pip install --user --upgrade pip
|
||||
pip install tox
|
||||
pip --version
|
||||
tox --version
|
||||
|
||||
- name: Run tox (postgres)
|
||||
if: matrix.adapter == 'postgres'
|
||||
run: tox
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: always()
|
||||
with:
|
||||
name: logs
|
||||
path: ./logs
|
||||
|
||||
- name: Get current date
|
||||
if: always()
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%dT%H_%M_%S')" #no colons allowed for artifacts
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: always()
|
||||
with:
|
||||
name: integration_results_${{ matrix.python-version }}_${{ matrix.os }}_${{ matrix.adapter }}-${{ steps.date.outputs.date }}.csv
|
||||
path: integration_results.csv
|
||||
|
||||
require-label-comment:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: test
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Needs permission PR comment
|
||||
if: >-
|
||||
needs.test.result == 'skipped' &&
|
||||
github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository
|
||||
uses: unsplash/comment-on-pr@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
msg: |
|
||||
"You do not have permissions to run integration tests, @dbt-labs/core "\
|
||||
"needs to label this PR with `ok to test` in order to run integration tests!"
|
||||
check_for_duplicate_msg: true
|
||||
26
.github/workflows/jira-creation.yml
vendored
Normal file
26
.github/workflows/jira-creation.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# **what?**
|
||||
# Mirrors issues into Jira. Includes the information: title,
|
||||
# GitHub Issue ID and URL
|
||||
|
||||
# **why?**
|
||||
# Jira is our tool for tracking and we need to see these issues in there
|
||||
|
||||
# **when?**
|
||||
# On issue creation or when an issue is labeled `Jira`
|
||||
|
||||
name: Jira Issue Creation
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, labeled]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
call-label-action:
|
||||
uses: dbt-labs/jira-actions/.github/workflows/jira-creation.yml@main
|
||||
secrets:
|
||||
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
|
||||
JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
|
||||
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
|
||||
27
.github/workflows/jira-label.yml
vendored
Normal file
27
.github/workflows/jira-label.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# **what?**
|
||||
# Calls mirroring Jira label Action. Includes adding a new label
|
||||
# to an existing issue or removing a label as well
|
||||
|
||||
# **why?**
|
||||
# Jira is our tool for tracking and we need to see these labels in there
|
||||
|
||||
# **when?**
|
||||
# On labels being added or removed from issues
|
||||
|
||||
name: Jira Label Mirroring
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled, unlabeled]
|
||||
|
||||
permissions:
|
||||
issues: read
|
||||
|
||||
jobs:
|
||||
call-label-action:
|
||||
uses: dbt-labs/jira-actions/.github/workflows/jira-label.yml@main
|
||||
secrets:
|
||||
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
|
||||
JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
|
||||
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
|
||||
|
||||
24
.github/workflows/jira-transition.yml
vendored
Normal file
24
.github/workflows/jira-transition.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# **what?**
|
||||
# Transition a Jira issue to a new state
|
||||
# Only supports these GitHub Issue transitions:
|
||||
# closed, deleted, reopened
|
||||
|
||||
# **why?**
|
||||
# Jira needs to be kept up-to-date
|
||||
|
||||
# **when?**
|
||||
# On issue closing, deletion, reopened
|
||||
|
||||
name: Jira Issue Transition
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [closed, deleted, reopened]
|
||||
|
||||
jobs:
|
||||
call-label-action:
|
||||
uses: dbt-labs/jira-actions/.github/workflows/jira-transition.yml@main
|
||||
secrets:
|
||||
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
|
||||
JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
|
||||
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
|
||||
207
.github/workflows/main.yml
vendored
Normal file
207
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,207 @@
|
||||
# **what?**
|
||||
# Runs code quality checks, unit tests, and verifies python build on
|
||||
# all code commited to the repository. This workflow should not
|
||||
# require any secrets since it runs for PRs from forked repos.
|
||||
# By default, secrets are not passed to workflows running from
|
||||
# a forked repo.
|
||||
|
||||
# **why?**
|
||||
# Ensure code for dbt meets a certain quality standard.
|
||||
|
||||
# **when?**
|
||||
# This will run for all PRs, when code is pushed to a release
|
||||
# branch, and when manually triggered.
|
||||
|
||||
name: Tests and Code Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "develop"
|
||||
- "*.latest"
|
||||
- "releases/*"
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: read-all
|
||||
|
||||
# will cancel previous workflows triggered by the same event and for the same ref for PRs or same SHA otherwise
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.ref || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
code-quality:
|
||||
name: ${{ matrix.toxenv }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
toxenv: [flake8, mypy]
|
||||
|
||||
env:
|
||||
TOXENV: ${{ matrix.toxenv }}
|
||||
PYTEST_ADDOPTS: "-v --color=yes"
|
||||
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
|
||||
- name: Install python dependencies
|
||||
run: |
|
||||
pip install --user --upgrade pip
|
||||
pip install tox
|
||||
pip --version
|
||||
tox --version
|
||||
|
||||
- name: Run tox
|
||||
run: tox
|
||||
|
||||
unit:
|
||||
name: unit test / python ${{ matrix.python-version }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: [3.7, 3.8, 3.9]
|
||||
|
||||
env:
|
||||
TOXENV: "unit"
|
||||
PYTEST_ADDOPTS: "-v --color=yes --csv unit_results.csv"
|
||||
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install python dependencies
|
||||
run: |
|
||||
pip install --user --upgrade pip
|
||||
pip install tox
|
||||
pip --version
|
||||
tox --version
|
||||
|
||||
- name: Run tox
|
||||
run: tox
|
||||
|
||||
- name: Get current date
|
||||
if: always()
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%dT%H_%M_%S')" #no colons allowed for artifacts
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: always()
|
||||
with:
|
||||
name: unit_results_${{ matrix.python-version }}-${{ steps.date.outputs.date }}.csv
|
||||
path: unit_results.csv
|
||||
|
||||
build:
|
||||
name: build packages
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install python dependencies
|
||||
run: |
|
||||
pip install --user --upgrade pip
|
||||
pip install --upgrade setuptools wheel twine check-wheel-contents
|
||||
pip --version
|
||||
|
||||
- name: Build distributions
|
||||
run: ./scripts/build-dist.sh
|
||||
|
||||
- name: Show distributions
|
||||
run: ls -lh dist/
|
||||
|
||||
- name: Check distribution descriptions
|
||||
run: |
|
||||
twine check dist/*
|
||||
|
||||
- name: Check wheel contents
|
||||
run: |
|
||||
check-wheel-contents dist/*.whl --ignore W007,W008
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
test-build:
|
||||
name: verify packages / python ${{ matrix.python-version }} / ${{ matrix.os }}
|
||||
|
||||
needs: build
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
python-version: [3.7, 3.8, 3.9]
|
||||
|
||||
steps:
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install python dependencies
|
||||
run: |
|
||||
pip install --user --upgrade pip
|
||||
pip install --upgrade wheel
|
||||
pip --version
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
- name: Show distributions
|
||||
run: ls -lh dist/
|
||||
|
||||
- name: Install wheel distributions
|
||||
run: |
|
||||
find ./dist/*.whl -maxdepth 1 -type f | xargs pip install --force-reinstall --find-links=dist/
|
||||
|
||||
- name: Check wheel distributions
|
||||
run: |
|
||||
dbt --version
|
||||
|
||||
- name: Install source distributions
|
||||
# ignore dbt-1.0.0, which intentionally raises an error when installed from source
|
||||
run: |
|
||||
find ./dist/dbt-[a-z]*.gz -maxdepth 1 -type f | xargs pip install --force-reinstall --find-links=dist/
|
||||
|
||||
- name: Check source distributions
|
||||
run: |
|
||||
dbt --version
|
||||
176
.github/workflows/performance.yml
vendored
Normal file
176
.github/workflows/performance.yml
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
name: Performance Regression Tests
|
||||
# Schedule triggers
|
||||
on:
|
||||
# runs twice a day at 10:05am and 10:05pm
|
||||
schedule:
|
||||
- cron: "5 10,22 * * *"
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# checks fmt of runner code
|
||||
# purposefully not a dependency of any other job
|
||||
# will block merging, but not prevent developing
|
||||
fmt:
|
||||
name: Cargo fmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --manifest-path performance/runner/Cargo.toml --all -- --check
|
||||
|
||||
# runs any tests associated with the runner
|
||||
# these tests make sure the runner logic is correct
|
||||
test-runner:
|
||||
name: Test Runner
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# turns errors into warnings
|
||||
RUSTFLAGS: "-D warnings"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --manifest-path performance/runner/Cargo.toml
|
||||
|
||||
# build an optimized binary to be used as the runner in later steps
|
||||
build-runner:
|
||||
needs: [test-runner]
|
||||
name: Build Runner
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --release --manifest-path performance/runner/Cargo.toml
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: runner
|
||||
path: performance/runner/target/release/runner
|
||||
|
||||
# run the performance measurements on the current or default branch
|
||||
measure-dev:
|
||||
needs: [build-runner]
|
||||
name: Measure Dev Branch
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout dev
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2.2.2
|
||||
with:
|
||||
python-version: "3.8"
|
||||
- name: install dbt
|
||||
run: pip install -r dev-requirements.txt -r editable-requirements.txt
|
||||
- name: install hyperfine
|
||||
run: wget https://github.com/sharkdp/hyperfine/releases/download/v1.11.0/hyperfine_1.11.0_amd64.deb && sudo dpkg -i hyperfine_1.11.0_amd64.deb
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: runner
|
||||
- name: change permissions
|
||||
run: chmod +x ./runner
|
||||
- name: run
|
||||
run: ./runner measure -b dev -p ${{ github.workspace }}/performance/projects/
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: dev-results
|
||||
path: performance/results/
|
||||
|
||||
# run the performance measurements on the release branch which we use
|
||||
# as a performance baseline. This part takes by far the longest, so
|
||||
# we do everything we can first so the job fails fast.
|
||||
# -----
|
||||
# we need to checkout dbt twice in this job: once for the baseline dbt
|
||||
# version, and once to get the latest regression testing projects,
|
||||
# metrics, and runner code from the develop or current branch so that
|
||||
# the calculations match for both versions of dbt we are comparing.
|
||||
measure-baseline:
|
||||
needs: [build-runner]
|
||||
name: Measure Baseline Branch
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout latest
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: "0.20.latest"
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2.2.2
|
||||
with:
|
||||
python-version: "3.8"
|
||||
- name: move repo up a level
|
||||
run: mkdir ${{ github.workspace }}/../baseline/ && cp -r ${{ github.workspace }} ${{ github.workspace }}/../baseline
|
||||
- name: "[debug] ls new dbt location"
|
||||
run: ls ${{ github.workspace }}/../baseline/dbt/
|
||||
# installation creates egg-links so we have to preserve source
|
||||
- name: install dbt from new location
|
||||
run: cd ${{ github.workspace }}/../baseline/dbt/ && pip install -r dev-requirements.txt -r editable-requirements.txt
|
||||
# checkout the current branch to get all the target projects
|
||||
# this deletes the old checked out code which is why we had to copy before
|
||||
- name: checkout dev
|
||||
uses: actions/checkout@v2
|
||||
- name: install hyperfine
|
||||
run: wget https://github.com/sharkdp/hyperfine/releases/download/v1.11.0/hyperfine_1.11.0_amd64.deb && sudo dpkg -i hyperfine_1.11.0_amd64.deb
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: runner
|
||||
- name: change permissions
|
||||
run: chmod +x ./runner
|
||||
- name: run runner
|
||||
run: ./runner measure -b baseline -p ${{ github.workspace }}/performance/projects/
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: baseline-results
|
||||
path: performance/results/
|
||||
|
||||
# detect regressions on the output generated from measuring
|
||||
# the two branches. Exits with non-zero code if a regression is detected.
|
||||
calculate-regressions:
|
||||
needs: [measure-dev, measure-baseline]
|
||||
name: Compare Results
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dev-results
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: baseline-results
|
||||
- name: "[debug] ls result files"
|
||||
run: ls
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: runner
|
||||
- name: change permissions
|
||||
run: chmod +x ./runner
|
||||
- name: make results directory
|
||||
run: mkdir ./final-output/
|
||||
- name: run calculation
|
||||
run: ./runner calculate -r ./ -o ./final-output/
|
||||
# always attempt to upload the results even if there were regressions found
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: final-calculations
|
||||
path: ./final-output/*
|
||||
200
.github/workflows/release.yml
vendored
Normal file
200
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,200 @@
|
||||
# **what?**
|
||||
# Take the given commit, run unit tests specifically on that sha, build and
|
||||
# package it, and then release to GitHub and PyPi with that specific build
|
||||
|
||||
# **why?**
|
||||
# Ensure an automated and tested release process
|
||||
|
||||
# **when?**
|
||||
# This will only run manually with a given sha and version
|
||||
|
||||
name: Release to GitHub and PyPi
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sha:
|
||||
description: 'The last commit sha in the release'
|
||||
required: true
|
||||
version_number:
|
||||
description: 'The release version number (i.e. 1.0.0b1)'
|
||||
required: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
name: Unit test
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
TOXENV: "unit"
|
||||
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.inputs.sha }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install python dependencies
|
||||
run: |
|
||||
pip install --user --upgrade pip
|
||||
pip install tox
|
||||
pip --version
|
||||
tox --version
|
||||
|
||||
- name: Run tox
|
||||
run: tox
|
||||
|
||||
build:
|
||||
name: build packages
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.inputs.sha }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install python dependencies
|
||||
run: |
|
||||
pip install --user --upgrade pip
|
||||
pip install --upgrade setuptools wheel twine check-wheel-contents
|
||||
pip --version
|
||||
|
||||
- name: Build distributions
|
||||
run: ./scripts/build-dist.sh
|
||||
|
||||
- name: Show distributions
|
||||
run: ls -lh dist/
|
||||
|
||||
- name: Check distribution descriptions
|
||||
run: |
|
||||
twine check dist/*
|
||||
|
||||
- name: Check wheel contents
|
||||
run: |
|
||||
check-wheel-contents dist/*.whl --ignore W007,W008
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: |
|
||||
dist/
|
||||
!dist/dbt-${{github.event.inputs.version_number}}.tar.gz
|
||||
|
||||
test-build:
|
||||
name: verify packages
|
||||
|
||||
needs: [build, unit]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install python dependencies
|
||||
run: |
|
||||
pip install --user --upgrade pip
|
||||
pip install --upgrade wheel
|
||||
pip --version
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
- name: Show distributions
|
||||
run: ls -lh dist/
|
||||
|
||||
- name: Install wheel distributions
|
||||
run: |
|
||||
find ./dist/*.whl -maxdepth 1 -type f | xargs pip install --force-reinstall --find-links=dist/
|
||||
|
||||
- name: Check wheel distributions
|
||||
run: |
|
||||
dbt --version
|
||||
|
||||
- name: Install source distributions
|
||||
run: |
|
||||
find ./dist/*.gz -maxdepth 1 -type f | xargs pip install --force-reinstall --find-links=dist/
|
||||
|
||||
- name: Check source distributions
|
||||
run: |
|
||||
dbt --version
|
||||
|
||||
|
||||
github-release:
|
||||
name: GitHub Release
|
||||
|
||||
needs: test-build
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: '.'
|
||||
|
||||
# Need to set an output variable because env variables can't be taken as input
|
||||
# This is needed for the next step with releasing to GitHub
|
||||
- name: Find release type
|
||||
id: release_type
|
||||
env:
|
||||
IS_PRERELEASE: ${{ contains(github.event.inputs.version_number, 'rc') || contains(github.event.inputs.version_number, 'b') }}
|
||||
run: |
|
||||
echo ::set-output name=isPrerelease::$IS_PRERELEASE
|
||||
|
||||
- name: Creating GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: dbt-core v${{github.event.inputs.version_number}}
|
||||
tag_name: v${{github.event.inputs.version_number}}
|
||||
prerelease: ${{ steps.release_type.outputs.isPrerelease }}
|
||||
target_commitish: ${{github.event.inputs.sha}}
|
||||
body: |
|
||||
[Release notes](https://github.com/dbt-labs/dbt-core/blob/main/CHANGELOG.md)
|
||||
files: |
|
||||
dbt_postgres-${{github.event.inputs.version_number}}-py3-none-any.whl
|
||||
dbt_core-${{github.event.inputs.version_number}}-py3-none-any.whl
|
||||
dbt-postgres-${{github.event.inputs.version_number}}.tar.gz
|
||||
dbt-core-${{github.event.inputs.version_number}}.tar.gz
|
||||
|
||||
pypi-release:
|
||||
name: Pypi release
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: github-release
|
||||
|
||||
environment: PypiProd
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: 'dist'
|
||||
|
||||
- name: Publish distribution to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.4.2
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
87
.github/workflows/schema-check.yml
vendored
Normal file
87
.github/workflows/schema-check.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
# **what?**
|
||||
# Compares the schema of the dbt version of the given ref vs
|
||||
# the latest official schema releases found in schemas.getdbt.com.
|
||||
# If there are differences, the workflow will fail and upload the
|
||||
# diff as an artifact. The metadata team should be alerted to the change.
|
||||
#
|
||||
# **why?**
|
||||
# Reaction work may need to be done if artifact schema changes
|
||||
# occur so we want to proactively alert to it.
|
||||
#
|
||||
# **when?**
|
||||
# On pushes to `develop` and release branches. Manual runs are also enabled.
|
||||
name: Artifact Schema Check
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request: #TODO: remove before merging
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
- "*.latest"
|
||||
- "releases/*"
|
||||
|
||||
env:
|
||||
LATEST_SCHEMA_PATH: ${{ github.workspace }}/new_schemas
|
||||
SCHEMA_DIFF_ARTIFACT: ${{ github.workspace }}//schema_schanges.txt
|
||||
DBT_REPO_DIRECTORY: ${{ github.workspace }}/dbt
|
||||
SCHEMA_REPO_DIRECTORY: ${{ github.workspace }}/schemas.getdbt.com
|
||||
|
||||
jobs:
|
||||
checking-schemas:
|
||||
name: "Checking schemas"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Checkout dbt repo
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
path: ${{ env.DBT_REPO_DIRECTORY }}
|
||||
|
||||
- name: Checkout schemas.getdbt.com repo
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
repository: dbt-labs/schemas.getdbt.com
|
||||
ref: 'main'
|
||||
ssh-key: ${{ secrets.SCHEMA_SSH_PRIVATE_KEY }}
|
||||
path: ${{ env.SCHEMA_REPO_DIRECTORY }}
|
||||
|
||||
- name: Generate current schema
|
||||
run: |
|
||||
cd ${{ env.DBT_REPO_DIRECTORY }}
|
||||
python3 -m venv env
|
||||
source env/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -r dev-requirements.txt -r editable-requirements.txt
|
||||
python scripts/collect-artifact-schema.py --path ${{ env.LATEST_SCHEMA_PATH }}
|
||||
|
||||
# Copy generated schema files into the schemas.getdbt.com repo
|
||||
# Do a git diff to find any changes
|
||||
# Ignore any date or version changes though
|
||||
- name: Compare schemas
|
||||
run: |
|
||||
cp -r ${{ env.LATEST_SCHEMA_PATH }}/dbt ${{ env.SCHEMA_REPO_DIRECTORY }}
|
||||
cd ${{ env.SCHEMA_REPO_DIRECTORY }}
|
||||
diff_results=$(git diff -I='*[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T' \
|
||||
-I='*[0-9]{1}.[0-9]{2}.[0-9]{1}(rc[0-9]|b[0-9]| )' --compact-summary)
|
||||
if [[ $(echo diff_results) ]]; then
|
||||
echo $diff_results
|
||||
echo "Schema changes detected!"
|
||||
git diff -I='*[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T' \
|
||||
-I='*[0-9]{1}.[0-9]{2}.[0-9]{1}(rc[0-9]|b[0-9]| )' > ${{ env.SCHEMA_DIFF_ARTIFACT }}
|
||||
exit 1
|
||||
else
|
||||
echo "No schema changes detected"
|
||||
fi
|
||||
|
||||
- name: Upload schema diff
|
||||
uses: actions/upload-artifact@v2.2.4
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: 'schema_schanges.txt'
|
||||
path: '${{ env.SCHEMA_DIFF_ARTIFACT }}'
|
||||
18
.github/workflows/stale.yml
vendored
Normal file
18
.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: "Close stale issues and PRs"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# pinned at v4 (https://github.com/actions/stale/releases/tag/v4.0.0)
|
||||
- uses: actions/stale@cdf15f641adb27a71842045a94023bef6945e3aa
|
||||
with:
|
||||
stale-issue-message: "This issue has been marked as Stale because it has been open for 180 days with no activity. If you would like the issue to remain open, please remove the stale label or comment on the issue, or it will be closed in 7 days."
|
||||
stale-pr-message: "This PR has been marked as Stale because it has been open for 180 days with no activity. If you would like the PR to remain open, please remove the stale label or comment on the PR, or it will be closed in 7 days."
|
||||
# mark issues/PRs stale when they haven't seen activity in 180 days
|
||||
days-before-stale: 180
|
||||
# ignore checking issues with the following labels
|
||||
exempt-issue-labels: "epic,discussion"
|
||||
71
.github/workflows/structured-logging-schema-check.yml
vendored
Normal file
71
.github/workflows/structured-logging-schema-check.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
# This Action checks makes a dbt run to sample json structured logs
|
||||
# and checks that they conform to the currently documented schema.
|
||||
#
|
||||
# If this action fails it either means we have unintentionally deviated
|
||||
# from our documented structured logging schema, or we need to bump the
|
||||
# version of our structured logging and add new documentation to
|
||||
# communicate these changes.
|
||||
|
||||
|
||||
name: Structured Logging Schema Check
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "*.latest"
|
||||
- "releases/*"
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
# run the performance measurements on the current or default branch
|
||||
test-schema:
|
||||
name: Test Log Schema
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# turns warnings into errors
|
||||
RUSTFLAGS: "-D warnings"
|
||||
# points tests to the log file
|
||||
LOG_DIR: "/home/runner/work/dbt-core/dbt-core/logs"
|
||||
# tells integration tests to output into json format
|
||||
DBT_LOG_FORMAT: 'json'
|
||||
steps:
|
||||
|
||||
- name: checkout dev
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2.2.2
|
||||
with:
|
||||
python-version: "3.8"
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: install dbt
|
||||
run: pip install -r dev-requirements.txt -r editable-requirements.txt
|
||||
|
||||
- name: Set up postgres
|
||||
uses: ./.github/actions/setup-postgres-linux
|
||||
|
||||
- name: ls
|
||||
run: ls
|
||||
|
||||
# integration tests generate a ton of logs in different files. the next step will find them all.
|
||||
# we actually care if these pass, because the normal test run doesn't usually include many json log outputs
|
||||
- name: Run integration tests
|
||||
run: tox -e py38-postgres -- -nauto
|
||||
|
||||
# apply our schema tests to every log event from the previous step
|
||||
# skips any output that isn't valid json
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: run
|
||||
args: --manifest-path test/interop/log_parsing/Cargo.toml
|
||||
109
.github/workflows/version-bump.yml
vendored
Normal file
109
.github/workflows/version-bump.yml
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
# **what?**
|
||||
# This workflow will take a version number and a dry run flag. With that
|
||||
# it will run versionbump to update the version number everywhere in the
|
||||
# code base and then generate an update Docker requirements file. If this
|
||||
# is a dry run, a draft PR will open with the changes. If this isn't a dry
|
||||
# run, the changes will be committed to the branch this is run on.
|
||||
|
||||
# **why?**
|
||||
# This is to aid in releasing dbt and making sure we have updated
|
||||
# the versions and Docker requirements in all places.
|
||||
|
||||
# **when?**
|
||||
# This is triggered either manually OR
|
||||
# from the repository_dispatch event "version-bump" which is sent from
|
||||
# the dbt-release repo Action
|
||||
|
||||
name: Version Bump
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_number:
|
||||
description: 'The version number to bump to'
|
||||
required: true
|
||||
is_dry_run:
|
||||
description: 'Creates a draft PR to allow testing instead of committing to a branch'
|
||||
required: true
|
||||
default: 'true'
|
||||
repository_dispatch:
|
||||
types: [version-bump]
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set version and dry run values
|
||||
id: variables
|
||||
env:
|
||||
VERSION_NUMBER: "${{ github.event.client_payload.version_number == '' && github.event.inputs.version_number || github.event.client_payload.version_number }}"
|
||||
IS_DRY_RUN: "${{ github.event.client_payload.is_dry_run == '' && github.event.inputs.is_dry_run || github.event.client_payload.is_dry_run }}"
|
||||
run: |
|
||||
echo Repository dispatch event version: ${{ github.event.client_payload.version_number }}
|
||||
echo Repository dispatch event dry run: ${{ github.event.client_payload.is_dry_run }}
|
||||
echo Workflow dispatch event version: ${{ github.event.inputs.version_number }}
|
||||
echo Workflow dispatch event dry run: ${{ github.event.inputs.is_dry_run }}
|
||||
echo ::set-output name=VERSION_NUMBER::$VERSION_NUMBER
|
||||
echo ::set-output name=IS_DRY_RUN::$IS_DRY_RUN
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.8"
|
||||
|
||||
- name: Install python dependencies
|
||||
run: |
|
||||
python3 -m venv env
|
||||
source env/bin/activate
|
||||
pip install --upgrade pip
|
||||
|
||||
- name: Create PR branch
|
||||
if: ${{ steps.variables.outputs.IS_DRY_RUN == 'true' }}
|
||||
run: |
|
||||
git checkout -b bumping-version/${{steps.variables.outputs.VERSION_NUMBER}}_$GITHUB_RUN_ID
|
||||
git push origin bumping-version/${{steps.variables.outputs.VERSION_NUMBER}}_$GITHUB_RUN_ID
|
||||
git branch --set-upstream-to=origin/bumping-version/${{steps.variables.outputs.VERSION_NUMBER}}_$GITHUB_RUN_ID bumping-version/${{steps.variables.outputs.VERSION_NUMBER}}_$GITHUB_RUN_ID
|
||||
|
||||
# - name: Generate Docker requirements
|
||||
# run: |
|
||||
# source env/bin/activate
|
||||
# pip install -r requirements.txt
|
||||
# pip freeze -l > docker/requirements/requirements.txt
|
||||
# git status
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
source env/bin/activate
|
||||
pip install -r dev-requirements.txt
|
||||
env/bin/bumpversion --allow-dirty --new-version ${{steps.variables.outputs.VERSION_NUMBER}} major
|
||||
git status
|
||||
|
||||
- name: Commit version bump directly
|
||||
uses: EndBug/add-and-commit@v7
|
||||
if: ${{ steps.variables.outputs.IS_DRY_RUN == 'false' }}
|
||||
with:
|
||||
author_name: 'Github Build Bot'
|
||||
author_email: 'buildbot@fishtownanalytics.com'
|
||||
message: 'Bumping version to ${{steps.variables.outputs.VERSION_NUMBER}}'
|
||||
|
||||
- name: Commit version bump to branch
|
||||
uses: EndBug/add-and-commit@v7
|
||||
if: ${{ steps.variables.outputs.IS_DRY_RUN == 'true' }}
|
||||
with:
|
||||
author_name: 'Github Build Bot'
|
||||
author_email: 'buildbot@fishtownanalytics.com'
|
||||
message: 'Bumping version to ${{steps.variables.outputs.VERSION_NUMBER}}'
|
||||
branch: 'bumping-version/${{steps.variables.outputs.VERSION_NUMBER}}_${{GITHUB.RUN_ID}}'
|
||||
push: 'origin origin/bumping-version/${{steps.variables.outputs.VERSION_NUMBER}}_${{GITHUB.RUN_ID}}'
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
if: ${{ steps.variables.outputs.IS_DRY_RUN == 'true' }}
|
||||
with:
|
||||
author: 'Github Build Bot <buildbot@fishtownanalytics.com>'
|
||||
draft: true
|
||||
base: ${{github.ref}}
|
||||
title: 'Bumping version to ${{steps.variables.outputs.VERSION_NUMBER}}'
|
||||
branch: 'bumping-version/${{steps.variables.outputs.VERSION_NUMBER}}_${{GITHUB.RUN_ID}}'
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -8,7 +8,8 @@ __pycache__/
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
env*/
|
||||
dbt_env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
@@ -42,6 +43,7 @@ htmlcov/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
.env
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
@@ -83,3 +85,12 @@ target/
|
||||
|
||||
# pycharm
|
||||
.idea/
|
||||
venv/
|
||||
|
||||
# AWS credentials
|
||||
.aws/
|
||||
|
||||
.DS_Store
|
||||
|
||||
# vscode
|
||||
.vscode/
|
||||
|
||||
48
ARCHITECTURE.md
Normal file
48
ARCHITECTURE.md
Normal file
@@ -0,0 +1,48 @@
|
||||
The core function of dbt is SQL compilation and execution. Users create projects of dbt resources (models, tests, seeds, snapshots, ...), defined in SQL and YAML files, and they invoke dbt to create, update, or query associated views and tables. Today, dbt makes heavy use of Jinja2 to enable the templating of SQL, and to construct a DAG (Directed Acyclic Graph) from all of the resources in a project. Users can also extend their projects by installing resources (including Jinja macros) from other projects, called "packages."
|
||||
|
||||
## dbt-core
|
||||
|
||||
Most of the python code in the repository is within the `core/dbt` directory. Currently the main subdirectories are:
|
||||
|
||||
- [`adapters`](core/dbt/adapters/README.md): Define base classes for behavior that is likely to differ across databases
|
||||
- [`clients`](core/dbt/clients/README.md): Interface with dependencies (agate, jinja) or across operating systems
|
||||
- [`config`](core/dbt/config/README.md): Reconcile user-supplied configuration from connection profiles, project files, and Jinja macros
|
||||
- [`context`](core/dbt/context/README.md): Build and expose dbt-specific Jinja functionality
|
||||
- [`contracts`](core/dbt/contracts/README.md): Define Python objects (dataclasses) that dbt expects to create and validate
|
||||
- [`deps`](core/dbt/deps/README.md): Package installation and dependency resolution
|
||||
- [`events`](core/dbt/events/README.md): Logging events
|
||||
- [`graph`](core/dbt/graph/README.md): Produce a `networkx` DAG of project resources, and selecting those resources given user-supplied criteria
|
||||
- [`include`](core/dbt/include/README.md): The dbt "global project," which defines default implementations of Jinja2 macros
|
||||
- [`parser`](core/dbt/parser/README.md): Read project files, validate, construct python objects
|
||||
- [`task`](core/dbt/task/README.md): Set forth the actions that dbt can perform when invoked
|
||||
|
||||
### Invoking dbt
|
||||
|
||||
The "tasks" map to top-level dbt commands. So `dbt run` => task.run.RunTask, etc. Some are more like abstract base classes (GraphRunnableTask, for example) but all the concrete types outside of task should map to tasks. Currently one executes at a time. The tasks kick off their “Runners” and those do execute in parallel. The parallelism is managed via a thread pool, in GraphRunnableTask.
|
||||
|
||||
core/dbt/include/index.html
|
||||
This is the docs website code. It comes from the dbt-docs repository, and is generated when a release is packaged.
|
||||
|
||||
## Adapters
|
||||
|
||||
dbt uses an adapter-plugin pattern to extend support to different databases, warehouses, query engines, etc. For testing and development purposes, the dbt-postgres plugin lives alongside the dbt-core codebase, in the [`plugins`](plugins) subdirectory. Like other adapter plugins, it is a self-contained codebase and package that builds on top of dbt-core.
|
||||
|
||||
Each adapter is a mix of python, Jinja2, and SQL. The adapter code also makes heavy use of Jinja2 to wrap modular chunks of SQL functionality, define default implementations, and allow plugins to override it.
|
||||
|
||||
Each adapter plugin is a standalone python package that includes:
|
||||
|
||||
- `dbt/include/[name]`: A "sub-global" dbt project, of YAML and SQL files, that reimplements Jinja macros to use the adapter's supported SQL syntax
|
||||
- `dbt/adapters/[name]`: Python modules that inherit, and optionally reimplement, the base adapter classes defined in dbt-core
|
||||
- `setup.py`
|
||||
|
||||
The Postgres adapter code is the most central, and many of its implementations are used as the default defined in the dbt-core global project. The greater the distance of a data technology from Postgres, the more its adapter plugin may need to reimplement.
|
||||
|
||||
## Testing dbt
|
||||
|
||||
The [`test/`](test/) subdirectory includes unit and integration tests that run as continuous integration checks against open pull requests. Unit tests check mock inputs and outputs of specific python functions. Integration tests perform end-to-end dbt invocations against real adapters (Postgres, Redshift, Snowflake, BigQuery) and assert that the results match expectations. See [the contributing guide](CONTRIBUTING.md) for a step-by-step walkthrough of setting up a local development and testing environment.
|
||||
|
||||
## Everything else
|
||||
|
||||
- [docker](docker/): All dbt versions are published as Docker images on DockerHub. This subfolder contains the `Dockerfile` (constant) and `requirements.txt` (one for each version).
|
||||
- [etc](etc/): Images for README
|
||||
- [scripts](scripts/): Helper scripts for testing, releasing, and producing JSON schemas. These are not included in distributions of dbt, nor are they rigorously tested—they're just handy tools for the dbt maintainers :)
|
||||
2851
CHANGELOG.md
2851
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
279
CONTRIBUTING.md
279
CONTRIBUTING.md
@@ -1,194 +1,229 @@
|
||||
# Getting started with dbt
|
||||
# Contributing to `dbt`
|
||||
|
||||
1. [About this document](#about-this-document)
|
||||
2. [Proposing a change](#proposing-a-change)
|
||||
3. [Getting the code](#getting-the-code)
|
||||
4. [Setting up an environment](#setting-up-an-environment)
|
||||
5. [Running `dbt` in development](#running-dbt-in-development)
|
||||
6. [Testing](#testing)
|
||||
7. [Submitting a Pull Request](#submitting-a-pull-request)
|
||||
|
||||
## About this document
|
||||
|
||||
This document is a guide intended for folks interested in contributing to dbt. It is not intended as a guide for end users of dbt (though if it is helpful, that's great!) and it assumes a certain level of familiarity with Python concepts such as virtualenvs, `pip`, python modules, filesystems, and so on. This guide also documents the process by which community-contributed Pull Requests can be incorporated into this repository. This guide assumes you are using macOS or Linux and are comfortable with the command line. If you get stuck while reading this guide, drop us a line in the #development channel on [slack](slack.getdbt.com).
|
||||
This document is a guide intended for folks interested in contributing to `dbt-core`. Below, we document the process by which members of the community should create issues and submit pull requests (PRs) in this repository. It is not intended as a guide for using `dbt-core`, and it assumes a certain level of familiarity with Python concepts such as virtualenvs, `pip`, python modules, filesystems, and so on. This guide assumes you are using macOS or Linux and are comfortable with the command line.
|
||||
|
||||
## Contributing a change
|
||||
If you're new to python development or contributing to open-source software, we encourage you to read this document from start to finish. If you get stuck, drop us a line in the `#dbt-core-development` channel on [slack](https://community.getdbt.com).
|
||||
|
||||
dbt is Apache 2.0-licensed open source software. dbt is the software that it is today because community members like you have opened issues, provided feedback, and contributed to the knowledge loop for the entire communtiy. Whether you are a seasoned open source contributor or a first time committer, you are welcomed and encouraged to contribute code, documentation, ideas, or problem statements to this project.
|
||||
#### Adapters
|
||||
|
||||
### Defining the problem
|
||||
|
||||
If you have an idea for a new feature or if you've discovered a bug in dbt, the first step is to open an issue. Please check the list of [open issues](https://github.com/fishtown-analytics/dbt/issues) before creating a new one. If you find a relevant issue, please add a comment to the open issue instead of creating a new one. There are hundreds of open issues in this repository and it can be hard to know where to look for a relevant open issue. **The dbt maintainers are always happy to point contributors in the right direction**, so please err on the side of documenting your idea in a new issue if you are unsure where a problem statement belongs.
|
||||
|
||||
**Note:** All community-contributed Pull Requests _must_ be associated with an open issue. If you submit a Pull Request that does not pertain to an open issue, you will be asked to create an issue describing the problem before the Pull Request can be reviewed.
|
||||
|
||||
### Discussing the idea
|
||||
|
||||
After creating an issue, a dbt maintainer will follow up with you to explore your idea further and advise on how to implement the suggested changes. In many cases, community members will chime in with their own thoughts on the problem statement. If you as the issue creator are interested in submitting a Pull Request to address the issue, you should indicate this in the body of the issue. The dbt maintainers are _always_ happy to help contributors with the implementation of fixes and features, so please also indicate if there's anything you're unsure about or could use guidance around in the issue.
|
||||
|
||||
### Submitting a change
|
||||
|
||||
If the issue is appropriately well-scoped and describes a beneficial change to the dbt codebase, then anyone may submit a Pull Request to implement the functionality described in the issue (see the sections below on how to do this).
|
||||
|
||||
In some cases, the right resolution to an open issue might be tangential to the dbt codebase. The right path forward might be a documentation update or a change that can be made in user-space. In other cases, the issue might describe functionality that the dbt maintainers are unwilling or unable to incorporate into the dbt codebase. When it is determined that an open issue describes functionality that will not translate to a code change in the dbt repository, the issue will be tagged with the `wontfix` label (see below) and closed.
|
||||
|
||||
### Using issue labels
|
||||
|
||||
The dbt maintainers use labels to categorize open issues. Some labels indicate the databases impacted by the issue, while others describe the domain in the dbt codebase germane to the discussion. While most of these labels are self-explanatory (eg. `snowflake` or `bigquery`), there are others that are worth describing.
|
||||
|
||||
| tag | description |
|
||||
| --- | ----------- |
|
||||
| bug | This issue represents a defect or regression in dbt |
|
||||
| enhancement | This issue represents net-new functionality in dbt |
|
||||
| good first issue | This issue does not require deep knowledge of the dbt codebase to implement. This issue is appropriate for a first-time contributor to implement. |
|
||||
| snoozed | This issue describes a good idea, but one which will probably not be addressed in a six-month time horizon. The dbt maintainers will revist these issues periodically and re-prioritize them accordingly. |
|
||||
| triage | This is a new issue which has not yet been reviewed by a dbt maintainer. This label is removed when a maintainer reviews and responds to the issue. |
|
||||
| stale | This is an old issue which has not recently been updated. Stale issues will periodically be closed by dbt maintainers, but they can be re-opened if the discussion is restarted. |
|
||||
| wontfix | This issue does not require a code change in the dbt repository, or the maintainers are unwilling/unable to merge a Pull Request which implements the behavior described in the issue. |
|
||||
If you have an issue or code change suggestion related to a specific database [adapter](https://docs.getdbt.com/docs/available-adapters); please refer to that supported databases seperate repo for those contributions.
|
||||
|
||||
### Signing the CLA
|
||||
|
||||
All contributors to dbt must sign the [Contributor License Agreement](https://docs.getdbt.com/docs/contributor-license-agreements) to have their Pull Request merged into the dbt codebase. If you are unable to sign the CLA, then the dbt maintainers will unfortunately be unable to merge your Pull Request.
|
||||
Please note that all contributors to `dbt-core` must sign the [Contributor License Agreement](https://docs.getdbt.com/docs/contributor-license-agreements) to have their Pull Request merged into the `dbt-core` codebase. If you are unable to sign the CLA, then the `dbt-core` maintainers will unfortunately be unable to merge your Pull Request. You are, however, welcome to open issues and comment on existing ones.
|
||||
|
||||
## Proposing a change
|
||||
|
||||
`dbt-core` is Apache 2.0-licensed open source software. `dbt-core` is what it is today because community members like you have opened issues, provided feedback, and contributed to the knowledge loop for the entire communtiy. Whether you are a seasoned open source contributor or a first-time committer, we welcome and encourage you to contribute code, documentation, ideas, or problem statements to this project.
|
||||
|
||||
### Defining the problem
|
||||
|
||||
If you have an idea for a new feature or if you've discovered a bug in `dbt-core`, the first step is to open an issue. Please check the list of [open issues](https://github.com/dbt-labs/dbt-core/issues) before creating a new one. If you find a relevant issue, please add a comment to the open issue instead of creating a new one. There are hundreds of open issues in this repository and it can be hard to know where to look for a relevant open issue. **The `dbt-core` maintainers are always happy to point contributors in the right direction**, so please err on the side of documenting your idea in a new issue if you are unsure where a problem statement belongs.
|
||||
|
||||
> **Note:** All community-contributed Pull Requests _must_ be associated with an open issue. If you submit a Pull Request that does not pertain to an open issue, you will be asked to create an issue describing the problem before the Pull Request can be reviewed.
|
||||
|
||||
### Discussing the idea
|
||||
|
||||
After you open an issue, a `dbt-core` maintainer will follow up by commenting on your issue (usually within 1-3 days) to explore your idea further and advise on how to implement the suggested changes. In many cases, community members will chime in with their own thoughts on the problem statement. If you as the issue creator are interested in submitting a Pull Request to address the issue, you should indicate this in the body of the issue. The `dbt-core` maintainers are _always_ happy to help contributors with the implementation of fixes and features, so please also indicate if there's anything you're unsure about or could use guidance around in the issue.
|
||||
|
||||
### Submitting a change
|
||||
|
||||
If an issue is appropriately well scoped and describes a beneficial change to the `dbt-core` codebase, then anyone may submit a Pull Request to implement the functionality described in the issue. See the sections below on how to do this.
|
||||
|
||||
The `dbt-core` maintainers will add a `good first issue` label if an issue is suitable for a first-time contributor. This label often means that the required code change is small, limited to one database adapter, or a net-new addition that does not impact existing functionality. You can see the list of currently open issues on the [Contribute](https://github.com/dbt-labs/dbt-core/contribute) page.
|
||||
|
||||
Here's a good workflow:
|
||||
- Comment on the open issue, expressing your interest in contributing the required code change
|
||||
- Outline your planned implementation. If you want help getting started, ask!
|
||||
- Follow the steps outlined below to develop locally. Once you have opened a PR, one of the `dbt-core` maintainers will work with you to review your code.
|
||||
- Add a test! Tests are crucial for both fixes and new features alike. We want to make sure that code works as intended, and that it avoids any bugs previously encountered. Currently, the best resource for understanding `dbt-core`'s [unit](test/unit) and [integration](test/integration) tests is the tests themselves. One of the maintainers can help by pointing out relevant examples.
|
||||
|
||||
In some cases, the right resolution to an open issue might be tangential to the `dbt-core` codebase. The right path forward might be a documentation update or a change that can be made in user-space. In other cases, the issue might describe functionality that the `dbt-core` maintainers are unwilling or unable to incorporate into the `dbt-core` codebase. When it is determined that an open issue describes functionality that will not translate to a code change in the `dbt-core` repository, the issue will be tagged with the `wontfix` label (see below) and closed.
|
||||
|
||||
### Using issue labels
|
||||
|
||||
The `dbt-core` maintainers use labels to categorize open issues. Most labels describe the domain in the `dbt-core` codebase germane to the discussion.
|
||||
|
||||
| tag | description |
|
||||
| --- | ----------- |
|
||||
| [triage](https://github.com/dbt-labs/dbt-core/labels/triage) | This is a new issue which has not yet been reviewed by a `dbt-core` maintainer. This label is removed when a maintainer reviews and responds to the issue. |
|
||||
| [bug](https://github.com/dbt-labs/dbt-core/labels/bug) | This issue represents a defect or regression in `dbt-core` |
|
||||
| [enhancement](https://github.com/dbt-labs/dbt-core/labels/enhancement) | This issue represents net-new functionality in `dbt-core` |
|
||||
| [good first issue](https://github.com/dbt-labs/dbt-core/labels/good%20first%20issue) | This issue does not require deep knowledge of the `dbt-core` codebase to implement. This issue is appropriate for a first-time contributor. |
|
||||
| [help wanted](https://github.com/dbt-labs/dbt-core/labels/help%20wanted) / [discussion](https://github.com/dbt-labs/dbt-core/labels/discussion) | Conversation around this issue in ongoing, and there isn't yet a clear path forward. Input from community members is most welcome. |
|
||||
| [duplicate](https://github.com/dbt-labs/dbt-core/issues/duplicate) | This issue is functionally identical to another open issue. The `dbt-core` maintainers will close this issue and encourage community members to focus conversation on the other one. |
|
||||
| [snoozed](https://github.com/dbt-labs/dbt-core/labels/snoozed) | This issue describes a good idea, but one which will probably not be addressed in a six-month time horizon. The `dbt-core` maintainers will revist these issues periodically and re-prioritize them accordingly. |
|
||||
| [stale](https://github.com/dbt-labs/dbt-core/labels/stale) | This is an old issue which has not recently been updated. Stale issues will periodically be closed by `dbt-core` maintainers, but they can be re-opened if the discussion is restarted. |
|
||||
| [wontfix](https://github.com/dbt-labs/dbt-core/labels/wontfix) | This issue does not require a code change in the `dbt-core` repository, or the maintainers are unwilling/unable to merge a Pull Request which implements the behavior described in the issue. |
|
||||
|
||||
#### Branching Strategy
|
||||
|
||||
`dbt-core` has three types of branches:
|
||||
|
||||
- **Trunks** are where active development of the next release takes place. There is one trunk named `main` at the time of writing this, and will be the default branch of the repository.
|
||||
- **Release Branches** track a specific, not yet complete release of `dbt-core`. Each minor version release has a corresponding release branch. For example, the `0.11.x` series of releases has a branch called `0.11.latest`. This allows us to release new patch versions under `0.11` without necessarily needing to pull them into the latest version of `dbt-core`.
|
||||
- **Feature Branches** track individual features and fixes. On completion they should be merged into the trunk branch or a specific release branch.
|
||||
|
||||
## Getting the code
|
||||
|
||||
### Installing git
|
||||
|
||||
You will need `git` in order to download and modify the dbt source code. On macOS, the best way to download git is to just install Xcode.
|
||||
You will need `git` in order to download and modify the `dbt-core` source code. On macOS, the best way to download git is to just install [Xcode](https://developer.apple.com/support/xcode/).
|
||||
|
||||
### External contributors
|
||||
|
||||
If you are not a member of the `fishtown-analytics` GitHub organization, you can contribute to dbt by forking the dbt repository. For a detailed overview on forking, check out the [GitHub docs on forking](https://help.github.com/en/articles/fork-a-repo). In short, you will need to:
|
||||
If you are not a member of the `dbt-labs` GitHub organization, you can contribute to `dbt-core` by forking the `dbt-core` repository. For a detailed overview on forking, check out the [GitHub docs on forking](https://help.github.com/en/articles/fork-a-repo). In short, you will need to:
|
||||
|
||||
1. fork the dbt repository
|
||||
2. clone your fork
|
||||
1. fork the `dbt-core` repository
|
||||
2. clone your fork locally
|
||||
3. check out a new branch for your proposed changes
|
||||
4. push changes to your fork
|
||||
5. open a pull request against `fishtown-analytics/dbt` from your forked repository
|
||||
5. open a pull request against `dbt-labs/dbt` from your forked repository
|
||||
|
||||
### Core contributors
|
||||
|
||||
If you are a member of the `fishtown-analytics` GitHub organization, you will have push access to the dbt repo. Rather than
|
||||
forking dbt to make your changes, just clone the repository and push directly to a branch.
|
||||
### dbt Labs contributors
|
||||
|
||||
If you are a member of the `dbt-labs` GitHub organization, you will have push access to the `dbt-core` repo. Rather than forking `dbt-core` to make your changes, just clone the repository, check out a new branch, and push directly to that branch.
|
||||
|
||||
## Setting up an environment
|
||||
|
||||
To begin developing code in dbt, you should set up the following:
|
||||
There are some tools that will be helpful to you in developing locally. While this is the list relevant for `dbt-core` development, many of these tools are used commonly across open-source python projects.
|
||||
|
||||
### virtualenv
|
||||
### Tools
|
||||
|
||||
We strongly recommend using virtual environments when developing code in dbt. We recommend creating this virtualenv
|
||||
in the root of the dbt repository. To create a new virtualenv, run:
|
||||
```
|
||||
A short list of tools used in `dbt-core` testing that will be helpful to your understanding:
|
||||
|
||||
- [`tox`](https://tox.readthedocs.io/en/latest/) to manage virtualenvs across python versions. We currently target the latest patch releases for Python 3.7, Python 3.8, and Python 3.9
|
||||
- [`pytest`](https://docs.pytest.org/en/latest/) to discover/run tests
|
||||
- [`make`](https://users.cs.duke.edu/~ola/courses/programming/Makefiles/Makefiles.html) - but don't worry too much, nobody _really_ understands how make works and our Makefile is super simple
|
||||
- [`flake8`](https://flake8.pycqa.org/en/latest/) for code linting
|
||||
- [`mypy`](https://mypy.readthedocs.io/en/stable/) for static type checking
|
||||
- [Github Actions](https://github.com/features/actions)
|
||||
|
||||
A deep understanding of these tools in not required to effectively contribute to `dbt-core`, but we recommend checking out the attached documentation if you're interested in learning more about them.
|
||||
|
||||
#### virtual environments
|
||||
|
||||
We strongly recommend using virtual environments when developing code in `dbt-core`. We recommend creating this virtualenv
|
||||
in the root of the `dbt-core` repository. To create a new virtualenv, run:
|
||||
```sh
|
||||
python3 -m venv env
|
||||
source env/bin/activate
|
||||
```
|
||||
|
||||
This will create and activate a new Python virtual environment.
|
||||
|
||||
### docker and docker-compose
|
||||
#### docker and docker-compose
|
||||
|
||||
Docker and docker-compose are both used in testing. For macOS, the easiest thing to do is to [download docker for mac](https://store.docker.com/editions/community/docker-ce-desktop-mac). You'll need to make an account. On Linux, you can use one of the packages [here](https://docs.docker.com/install/#server). We recommend installing from docker.com instead of from your package manager. On Linux you also have to install docker-compose separately, follow [these instructions](https://docs.docker.com/compose/install/#install-compose).
|
||||
Docker and docker-compose are both used in testing. Specific instructions for you OS can be found [here](https://docs.docker.com/get-docker/).
|
||||
|
||||
|
||||
### Installing postgres locally (optional)
|
||||
#### postgres (optional)
|
||||
|
||||
For testing, and later in the examples in this document, you may want to have `psql` available so you can poke around in the database and see what happened. We recommend that you use [homebrew](https://brew.sh/) for that on macOS, and your package manager on Linux. You can install any version of the postgres client that you'd like. On macOS, with homebrew setup, you can run:
|
||||
|
||||
```
|
||||
```sh
|
||||
brew install postgresql
|
||||
```
|
||||
|
||||
## Running dbt in development
|
||||
## Running `dbt-core` in development
|
||||
|
||||
### Installation
|
||||
|
||||
First make sure that you set up your `virtualenv` as described in section _Setting up an environment_. Next, install dbt (and it's dependencies) with:
|
||||
First make sure that you set up your `virtualenv` as described in [Setting up an environment](#setting-up-an-environment). Also ensure you have the latest version of pip installed with `pip install --upgrade pip`. Next, install `dbt-core` (and its dependencies) with:
|
||||
|
||||
```
|
||||
pip install -r editable_requirements.txt
|
||||
```sh
|
||||
make dev
|
||||
# or
|
||||
pip install -r dev-requirements.txt -r editable-requirements.txt
|
||||
```
|
||||
|
||||
When dbt is installed from source in this way, any changes you make to the dbt source code will be reflected immediately in your next `dbt` run.
|
||||
When `dbt-core` is installed this way, any changes you make to the `dbt-core` source code will be reflected immediately in your next `dbt-core` run.
|
||||
|
||||
### Running dbt
|
||||
|
||||
With your virtualenv activated, the `dbt` script should point back to the source code you've cloned on your machine. You can verify this by running `which dbt`. This command should show you a path to an executable in your virtualenv.
|
||||
### Running `dbt-core`
|
||||
|
||||
With your virtualenv activated, the `dbt-core` script should point back to the source code you've cloned on your machine. You can verify this by running `which dbt`. This command should show you a path to an executable in your virtualenv.
|
||||
|
||||
Configure your [profile](https://docs.getdbt.com/docs/configure-your-profile) as necessary to connect to your target databases. It may be a good idea to add a new profile pointing to a local postgres instance, or a specific test sandbox within your data warehouse if appropriate.
|
||||
|
||||
## Testing
|
||||
|
||||
Getting the dbt integration tests set up in your local environment will be very helpful as you start to make changes to your local version of dbt. The section that follows outlines some helpful tips for setting up the test environment.
|
||||
Getting the `dbt-core` integration tests set up in your local environment will be very helpful as you start to make changes to your local version of `dbt-core`. The section that follows outlines some helpful tips for setting up the test environment.
|
||||
|
||||
### Tools
|
||||
Although `dbt-core` works with a number of different databases, you won't need to supply credentials for every one of these databases in your test environment. Instead you can test all dbt-core code changes with Python and Postgres.
|
||||
|
||||
A short list of tools used in dbt testing that will be helpful to your understanding:
|
||||
### Initial setup
|
||||
|
||||
- [virtualenv](https://virtualenv.pypa.io/en/stable/) to manage dependencies
|
||||
- [tox](https://tox.readthedocs.io/en/latest/) to manage virtualenvs across python versions
|
||||
- [pytest](https://docs.pytest.org/en/latest/) to discover/run tests
|
||||
- [make](https://users.cs.duke.edu/~ola/courses/programming/Makefiles/Makefiles.html) - but don't worry too much, nobody _really_ understands how make works and our Makefile is super simple
|
||||
- [flake8](https://gitlab.com/pycqa/flake8) for code linting
|
||||
- [CircleCI](https://circleci.com/product/) and [Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/)
|
||||
|
||||
A deep understanding of these tools in not required to effectively contribute to dbt, but we recommend checking out the attached documentation if you're interested in learning more about them.
|
||||
|
||||
|
||||
### Running tests via Docker
|
||||
|
||||
dbt's unit and integration tests run in Docker. Because dbt works with a number of different databases, you will need to supply credentials for one or more of these databases in your test environment. Most organizations don't have access to each of a BigQuery, Redshift, Snowflake, and Postgres database, so it's likely that you will be unable to run every integration test locally. Fortunately, Fishtown Analytics provides a CI environment with access to sandboxed Redshift, Snowflake, BigQuery, and Postgres databases. See the section on _Submitting a Pull Request_ below for more information on this CI setup.
|
||||
|
||||
|
||||
#### Specifying your test credentials
|
||||
|
||||
dbt uses test credentials specified in a `test.env` file in the root of the repository. This `test.env` file is git-ignored, but please be _extra_ careful to never check in credentials or other sensitive information when developing against dbt. To create your `test.env` file, copy the provided sample file, then supply your relevant credentials:
|
||||
|
||||
```
|
||||
cp test.env.sample test.env
|
||||
atom test.env # supply your credentials
|
||||
```
|
||||
|
||||
We recommend starting with dbt's Postgres tests. These tests cover most of the functionality in dbt, are the fastest to run, and are the easiest to set up. dbt's test suite runs Postgres in a Docker container, so no setup should be required to run these tests. If you additionally want to test Snowflake, Bigquery, or Redshift locally you'll need to get credentials and add them to the `test.env` file.
|
||||
|
||||
#### Running tests
|
||||
|
||||
dbt's unit tests and Python linter can be run with:
|
||||
|
||||
```
|
||||
make test-unit
|
||||
```
|
||||
|
||||
To run the Postgres + Python 3.6 integration tests, you'll have to do one extra step of setting up the test database:
|
||||
We recommend starting with `dbt-core`'s Postgres tests. These tests cover most of the functionality in `dbt-core`, are the fastest to run, and are the easiest to set up. To run the Postgres integration tests, you'll have to do one extra step of setting up the test database:
|
||||
|
||||
```sh
|
||||
make setup-db
|
||||
```
|
||||
or, alternatively:
|
||||
```sh
|
||||
docker-compose up -d database
|
||||
PGHOST=localhost PGUSER=root PGPASSWORD=password PGDATABASE=postgres bash test/setup_db.sh
|
||||
```
|
||||
|
||||
To run a quick test for Python3 integration tests on Postgres, you can run:
|
||||
### Test commands
|
||||
|
||||
There are a few methods for running tests locally.
|
||||
|
||||
#### Makefile
|
||||
|
||||
There are multiple targets in the Makefile to run common test suites and code
|
||||
checks, most notably:
|
||||
|
||||
```sh
|
||||
# Runs unit tests with py38 and code checks in parallel.
|
||||
make test
|
||||
# Runs postgres integration tests with py38 in "fail fast" mode.
|
||||
make integration
|
||||
```
|
||||
make test-quick
|
||||
> These make targets assume you have a recent version of [`tox`](https://tox.readthedocs.io/en/latest/) installed locally,
|
||||
> unless you use choose a Docker container to run tests. Run `make help` for more info.
|
||||
|
||||
Check out the other targets in the Makefile to see other commonly used test
|
||||
suites.
|
||||
|
||||
#### `tox`
|
||||
|
||||
[`tox`](https://tox.readthedocs.io/en/latest/) takes care of managing virtualenvs and install dependencies in order to run
|
||||
tests. You can also run tests in parallel, for example, you can run unit tests
|
||||
for Python 3.7, Python 3.8, Python 3.9, `flake8` checks, and `mypy` checks in
|
||||
parallel with `tox -p`. Also, you can run unit tests for specific python versions
|
||||
with `tox -e py37`. The configuration for these tests in located in `tox.ini`.
|
||||
|
||||
#### `pytest`
|
||||
|
||||
Finally, you can also run a specific test or group of tests using [`pytest`](https://docs.pytest.org/en/latest/) directly. With a virtualenv
|
||||
active and dev dependencies installed you can do things like:
|
||||
```sh
|
||||
# run specific postgres integration tests
|
||||
python -m pytest -m profile_postgres test/integration/001_simple_copy_test
|
||||
# run all unit tests in a file
|
||||
python -m pytest test/unit/test_graph.py
|
||||
# run a specific unit test
|
||||
python -m pytest test/unit/test_graph.py::GraphTest::test__dependency_list
|
||||
```
|
||||
> [Here](https://docs.pytest.org/en/reorganize-docs/new-docs/user/commandlineuseful.html)
|
||||
> is a list of useful command-line options for `pytest` to use while developing.
|
||||
## Submitting a Pull Request
|
||||
|
||||
To run tests for a specific database, invoke `tox` directly with the required flags:
|
||||
```
|
||||
# Run Postgres py36 tests
|
||||
docker-compose run test tox -e integration-postgres-py36 -- -x
|
||||
dbt Labs provides a CI environment to test changes to specific adapters, and periodic maintenance checks of `dbt-core` through Github Actions. For example, if you submit a pull request to the `dbt-redshift` repo, GitHub will trigger automated code checks and tests against Redshift.
|
||||
|
||||
# Run Snowflake py36 tests
|
||||
docker-compose run test tox -e integration-snowflake-py36 -- -x
|
||||
A `dbt-core` maintainer will review your PR. They may suggest code revision for style or clarity, or request that you add unit or integration test(s). These are good things! We believe that, with a little bit of help, anyone can contribute high-quality code.
|
||||
- First time contributors should note code checks + unit tests require a maintainer to approve.
|
||||
|
||||
# Run BigQuery py36 tests
|
||||
docker-compose run test tox -e integration-bigquery-py36 -- -x
|
||||
|
||||
# Run Redshift py36 tests
|
||||
docker-compose run test tox -e integration-redshift-py36 -- -x
|
||||
```
|
||||
|
||||
See the `Makefile` contents for more some other examples of ways to run `tox`.
|
||||
|
||||
### Submitting a Pull Request
|
||||
|
||||
Fishtown Analytics provides a sandboxed Redshift, Snowflake, and BigQuery database for use in a CI environment.
|
||||
|
||||
When pull requests are submitted to the `fishtown-analytics/dbt` repo, GitHub will trigger automated tests in CircleCI and Azure Pipelines. If the PR submitter is a member of the `fishtown-analytics` GitHub organization, then the credentials for these databases will be automatically supplied as environment variables in the CI test suite.
|
||||
|
||||
**If the PR submitter is not a member of the `fishtown-analytics` organization, then these environment variables will not be automatically supplied in the CI environment**. Once a core maintainer has taken a look at the Pull Request, they will kick off the test suite with the required credentials.
|
||||
|
||||
Once your tests are passing and your PR has been reviewed, a dbt maintainer will merge your changes into the active development branch! And that's it! Happy developing :tada:
|
||||
Once all tests are passing and your PR has been approved, a `dbt-core` maintainer will merge your changes into the active development branch. And that's it! Happy developing :tada:
|
||||
|
||||
46
Dockerfile
46
Dockerfile
@@ -1,46 +0,0 @@
|
||||
FROM ubuntu:18.04
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get dist-upgrade -y && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
netcat postgresql curl git ssh software-properties-common \
|
||||
make build-essential ca-certificates libpq-dev && \
|
||||
add-apt-repository ppa:deadsnakes/ppa && \
|
||||
apt-get install -y \
|
||||
python python-dev python-pip \
|
||||
python3.6 python3.6-dev python3-pip python3.6-venv \
|
||||
python3.7 python3.7-dev python3.7-venv \
|
||||
python3.8 python3.8-dev python3.8-venv && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
RUN useradd -mU dbt_test_user
|
||||
RUN mkdir /usr/app && chown dbt_test_user /usr/app
|
||||
RUN mkdir /home/tox && chown dbt_test_user /home/tox
|
||||
|
||||
WORKDIR /usr/app
|
||||
VOLUME /usr/app
|
||||
|
||||
RUN pip3 install tox wheel
|
||||
|
||||
RUN python2.7 -m pip install virtualenv wheel && \
|
||||
python2.7 -m virtualenv /home/tox/venv2.7 && \
|
||||
/home/tox/venv2.7/bin/python -m pip install -U pip tox
|
||||
|
||||
RUN python3.6 -m pip install -U pip wheel && \
|
||||
python3.6 -m venv /home/tox/venv3.6 && \
|
||||
/home/tox/venv3.6/bin/python -m pip install -U pip tox
|
||||
|
||||
RUN python3.7 -m pip install -U pip wheel && \
|
||||
python3.7 -m venv /home/tox/venv3.7 && \
|
||||
/home/tox/venv3.7/bin/python -m pip install -U pip tox
|
||||
|
||||
RUN python3.8 -m pip install -U pip wheel && \
|
||||
python3.8 -m venv /home/tox/venv3.8 && \
|
||||
/home/tox/venv3.8/bin/python -m pip install -U pip tox
|
||||
|
||||
USER dbt_test_user
|
||||
|
||||
ENV PYTHONIOENCODING=utf-8
|
||||
ENV LANG C.UTF-8
|
||||
80
Dockerfile.test
Normal file
80
Dockerfile.test
Normal file
@@ -0,0 +1,80 @@
|
||||
##
|
||||
# This dockerfile is used for local development and adapter testing only.
|
||||
# See `/docker` for a generic and production-ready docker file
|
||||
##
|
||||
|
||||
FROM ubuntu:20.04
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
software-properties-common \
|
||||
&& add-apt-repository ppa:git-core/ppa -y \
|
||||
&& apt-get dist-upgrade -y \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
netcat \
|
||||
postgresql \
|
||||
curl \
|
||||
git \
|
||||
ssh \
|
||||
software-properties-common \
|
||||
make \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
libpq-dev \
|
||||
libsasl2-dev \
|
||||
libsasl2-2 \
|
||||
libsasl2-modules-gssapi-mit \
|
||||
libyaml-dev \
|
||||
unixodbc-dev \
|
||||
&& add-apt-repository ppa:deadsnakes/ppa \
|
||||
&& apt-get install -y \
|
||||
python \
|
||||
python-dev \
|
||||
python3-pip \
|
||||
python3.6 \
|
||||
python3.6-dev \
|
||||
python3-pip \
|
||||
python3.6-venv \
|
||||
python3.7 \
|
||||
python3.7-dev \
|
||||
python3.7-venv \
|
||||
python3.8 \
|
||||
python3.8-dev \
|
||||
python3.8-venv \
|
||||
python3.9 \
|
||||
python3.9-dev \
|
||||
python3.9-venv \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
ARG DOCKERIZE_VERSION=v0.6.1
|
||||
RUN curl -LO https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
|
||||
&& tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
|
||||
&& rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
|
||||
|
||||
RUN pip3 install -U tox wheel six setuptools
|
||||
|
||||
# These args are passed in via docker-compose, which reads then from the .env file.
|
||||
# On Linux, run `make .env` to create the .env file for the current user.
|
||||
# On MacOS and Windows, these can stay unset.
|
||||
ARG USER_ID
|
||||
ARG GROUP_ID
|
||||
|
||||
RUN if [ ${USER_ID:-0} -ne 0 ] && [ ${GROUP_ID:-0} -ne 0 ]; then \
|
||||
groupadd -g ${GROUP_ID} dbt_test_user && \
|
||||
useradd -m -l -u ${USER_ID} -g ${GROUP_ID} dbt_test_user; \
|
||||
else \
|
||||
useradd -mU -l dbt_test_user; \
|
||||
fi
|
||||
RUN mkdir /usr/app && chown dbt_test_user /usr/app
|
||||
RUN mkdir /home/tox && chown dbt_test_user /home/tox
|
||||
|
||||
WORKDIR /usr/app
|
||||
VOLUME /usr/app
|
||||
|
||||
USER dbt_test_user
|
||||
|
||||
ENV PYTHONIOENCODING=utf-8
|
||||
ENV LANG C.UTF-8
|
||||
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
Copyright 2021 dbt Labs, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
86
Makefile
86
Makefile
@@ -1,29 +1,70 @@
|
||||
.PHONY: install test test-unit test-integration
|
||||
.DEFAULT_GOAL:=help
|
||||
|
||||
changed_tests := `git status --porcelain | grep '^\(M\| M\|A\| A\)' | awk '{ print $$2 }' | grep '\/test_[a-zA-Z_\-\.]\+.py'`
|
||||
# Optional flag to run target in a docker container.
|
||||
# (example `make test USE_DOCKER=true`)
|
||||
ifeq ($(USE_DOCKER),true)
|
||||
DOCKER_CMD := docker-compose run --rm test
|
||||
endif
|
||||
|
||||
install:
|
||||
pip install -e .
|
||||
.PHONY: dev
|
||||
dev: ## Installs dbt-* packages in develop mode along with development dependencies.
|
||||
pip install -r dev-requirements.txt -r editable-requirements.txt
|
||||
|
||||
test:
|
||||
@echo "Full test run starting..."
|
||||
@time docker-compose run test tox
|
||||
.PHONY: mypy
|
||||
mypy: .env ## Runs mypy for static type checking.
|
||||
$(DOCKER_CMD) tox -e mypy
|
||||
|
||||
test-unit:
|
||||
@echo "Unit test run starting..."
|
||||
@time docker-compose run test tox -e unit-py36,flake8
|
||||
.PHONY: flake8
|
||||
flake8: .env ## Runs flake8 to enforce style guide.
|
||||
$(DOCKER_CMD) tox -e flake8
|
||||
|
||||
test-integration:
|
||||
@echo "Integration test run starting..."
|
||||
@time docker-compose run test tox -e integration-postgres-py36integration-redshift-py36,integration-snowflake-py36,integration-bigquery-py36
|
||||
.PHONY: lint
|
||||
lint: .env ## Runs all code checks in parallel.
|
||||
$(DOCKER_CMD) tox -p -e flake8,mypy
|
||||
|
||||
test-quick:
|
||||
@echo "Integration test run starting..."
|
||||
@time docker-compose run test tox -e integration-postgres-py36 -- -x
|
||||
.PHONY: unit
|
||||
unit: .env ## Runs unit tests with py38.
|
||||
$(DOCKER_CMD) tox -e py38
|
||||
|
||||
clean:
|
||||
.PHONY: test
|
||||
test: .env ## Runs unit tests with py38 and code checks in parallel.
|
||||
$(DOCKER_CMD) tox -p -e py38,flake8,mypy
|
||||
|
||||
.PHONY: integration
|
||||
integration: .env integration-postgres ## Alias for integration-postgres.
|
||||
|
||||
.PHONY: integration-fail-fast
|
||||
integration-fail-fast: .env integration-postgres-fail-fast ## Alias for integration-postgres-fail-fast.
|
||||
|
||||
.PHONY: integration-postgres
|
||||
integration-postgres: .env ## Runs postgres integration tests with py38.
|
||||
$(DOCKER_CMD) tox -e py38-postgres -- -nauto
|
||||
|
||||
.PHONY: integration-postgres-fail-fast
|
||||
integration-postgres-fail-fast: .env ## Runs postgres integration tests with py38 in "fail fast" mode.
|
||||
$(DOCKER_CMD) tox -e py38-postgres -- -x -nauto
|
||||
|
||||
.PHONY: setup-db
|
||||
setup-db: ## Setup Postgres database with docker-compose for system testing.
|
||||
docker-compose up -d database
|
||||
PGHOST=localhost PGUSER=root PGPASSWORD=password PGDATABASE=postgres bash test/setup_db.sh
|
||||
|
||||
# This rule creates a file named .env that is used by docker-compose for passing
|
||||
# the USER_ID and GROUP_ID arguments to the Docker image.
|
||||
.env: ## Setup step for using using docker-compose with make target.
|
||||
@touch .env
|
||||
ifneq ($(OS),Windows_NT)
|
||||
ifneq ($(shell uname -s), Darwin)
|
||||
@echo USER_ID=$(shell id -u) > .env
|
||||
@echo GROUP_ID=$(shell id -g) >> .env
|
||||
endif
|
||||
endif
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## Resets development environment.
|
||||
rm -f .coverage
|
||||
rm -rf .eggs/
|
||||
rm -f .env
|
||||
rm -rf .tox/
|
||||
rm -rf build/
|
||||
rm -rf dbt.egg-info/
|
||||
@@ -34,3 +75,14 @@ clean:
|
||||
rm -rf target/
|
||||
find . -type f -name '*.pyc' -delete
|
||||
find . -type d -name '__pycache__' -depth -delete
|
||||
|
||||
.PHONY: help
|
||||
help: ## Show this help message.
|
||||
@echo 'usage: make [target] [USE_DOCKER=true]'
|
||||
@echo
|
||||
@echo 'targets:'
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
@echo
|
||||
@echo 'options:'
|
||||
@echo 'use USE_DOCKER=true to run target in a docker container'
|
||||
|
||||
|
||||
48
README.md
48
README.md
@@ -1,28 +1,18 @@
|
||||
<p align="center">
|
||||
<img src="https://github.com/fishtown-analytics/dbt/blob/master/etc/dbt-horizontal.png?raw=true" alt="dbt logo"/>
|
||||
<img src="https://raw.githubusercontent.com/dbt-labs/dbt-core/fa1ea14ddfb1d5ae319d5141844910dd53ab2834/etc/dbt-core.svg" alt="dbt logo" width="750"/>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://codeclimate.com/github/fishtown-analytics/dbt">
|
||||
<img src="https://codeclimate.com/github/fishtown-analytics/dbt/badges/gpa.svg" alt="Code Climate"/>
|
||||
<a href="https://github.com/dbt-labs/dbt-core/actions/workflows/main.yml">
|
||||
<img src="https://github.com/dbt-labs/dbt-core/actions/workflows/main.yml/badge.svg?event=push" alt="Unit Tests Badge"/>
|
||||
</a>
|
||||
<a href="https://circleci.com/gh/fishtown-analytics/dbt/tree/master">
|
||||
<img src="https://circleci.com/gh/fishtown-analytics/dbt/tree/master.svg?style=svg" alt="CircleCI" />
|
||||
</a>
|
||||
<a href="https://ci.appveyor.com/project/DrewBanin/dbt/branch/development">
|
||||
<img src="https://ci.appveyor.com/api/projects/status/v01rwd3q91jnwp9m/branch/development?svg=true" alt="AppVeyor" />
|
||||
</a>
|
||||
<a href="https://slack.getdbt.com">
|
||||
<img src="https://slack.getdbt.com/badge.svg" alt="Slack" />
|
||||
<a href="https://github.com/dbt-labs/dbt-core/actions/workflows/integration.yml">
|
||||
<img src="https://github.com/dbt-labs/dbt-core/actions/workflows/integration.yml/badge.svg?event=push" alt="Integration Tests Badge"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
**[dbt](https://www.getdbt.com/)** (data build tool) enables data analysts and engineers to transform their data using the same practices that software engineers use to build applications.
|
||||
**[dbt](https://www.getdbt.com/)** enables data analysts and engineers to transform their data using the same practices that software engineers use to build applications.
|
||||
|
||||
dbt is the T in ELT. Organize, cleanse, denormalize, filter, rename, and pre-aggregate the raw data in your warehouse so that it's ready for analysis.
|
||||
|
||||

|
||||
|
||||
dbt can be used to [aggregate pageviews into sessions](https://github.com/fishtown-analytics/snowplow), calculate [ad spend ROI](https://github.com/fishtown-analytics/facebook-ads), or report on [email campaign performance](https://github.com/fishtown-analytics/mailchimp).
|
||||

|
||||
|
||||
## Understanding dbt
|
||||
|
||||
@@ -30,29 +20,23 @@ Analysts using dbt can transform their data by simply writing select statements,
|
||||
|
||||
These select statements, or "models", form a dbt project. Models frequently build on top of one another – dbt makes it easy to [manage relationships](https://docs.getdbt.com/docs/ref) between models, and [visualize these relationships](https://docs.getdbt.com/docs/documentation), as well as assure the quality of your transformations through [testing](https://docs.getdbt.com/docs/testing).
|
||||
|
||||

|
||||

|
||||
|
||||
## Getting started
|
||||
|
||||
- [Install dbt](https://docs.getdbt.com/docs/installation)
|
||||
- Read the [documentation](https://docs.getdbt.com/).
|
||||
- Productionize your dbt project with [dbt Cloud](https://www.getdbt.com)
|
||||
- [Install dbt](https://docs.getdbt.com/docs/installation)
|
||||
- Read the [introduction](https://docs.getdbt.com/docs/introduction/) and [viewpoint](https://docs.getdbt.com/docs/about/viewpoint/)
|
||||
|
||||
## Find out more
|
||||
## Join the dbt Community
|
||||
|
||||
- Check out the [Introduction to dbt](https://dbt.readme.io/docs/introduction).
|
||||
- Read the [dbt Viewpoint](https://dbt.readme.io/docs/viewpoint).
|
||||
|
||||
## Join thousands of analysts in the dbt community
|
||||
|
||||
- Join the [chat](http://slack.getdbt.com/) on Slack.
|
||||
- Find community posts on [dbt Discourse](https://discourse.getdbt.com).
|
||||
- Be part of the conversation in the [dbt Community Slack](http://community.getdbt.com/)
|
||||
- Read more on the [dbt Community Discourse](https://discourse.getdbt.com)
|
||||
|
||||
## Reporting bugs and contributing code
|
||||
|
||||
- Want to report a bug or request a feature? Let us know on [Slack](http://slack.getdbt.com/), or open [an issue](https://github.com/fishtown-analytics/dbt/issues/new).
|
||||
- Want to help us build dbt? Check out the [Contributing Getting Started Guide](/CONTRIBUTING.md)
|
||||
- Want to report a bug or request a feature? Let us know on [Slack](http://community.getdbt.com/), or open [an issue](https://github.com/dbt-labs/dbt-core/issues/new)
|
||||
- Want to help us build dbt? Check out the [Contributing Guide](https://github.com/dbt-labs/dbt-core/blob/HEAD/CONTRIBUTING.md)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Everyone interacting in the dbt project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [PyPA Code of Conduct](https://www.pypa.io/en/latest/code-of-conduct/).
|
||||
Everyone interacting in the dbt project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [dbt Code of Conduct](https://community.getdbt.com/code-of-conduct).
|
||||
|
||||
92
RELEASE.md
92
RELEASE.md
@@ -1,92 +0,0 @@
|
||||
### Release Procedure :shipit:
|
||||
|
||||
#### Branching Strategy
|
||||
|
||||
dbt has three types of branches:
|
||||
|
||||
- **Trunks** track the latest release of a minor version of dbt. Historically, we used the `master` branch as the trunk. Each minor version release has a corresponding trunk. For example, the `0.11.x` series of releases has a branch called `0.11.latest`. This allows us to release new patch versions under `0.11` without necessarily needing to pull them into the latest version of dbt.
|
||||
- **Release Branches** track a specific, not yet complete release of dbt. These releases are codenamed since we don't always know what their semantic version will be. Example: `dev/lucretia-mott` became `0.11.1`.
|
||||
- **Feature Branches** track individual features and fixes. On completion they should be merged into a release branch.
|
||||
|
||||
#### Git & PyPI
|
||||
|
||||
1. Update CHANGELOG.md with the most recent changes
|
||||
2. If this is a release candidate, you want to create it off of your release branch. If it's an actual release, you must first merge to a master branch. Open a Pull Request in Github to merge it into the appropriate trunk (`X.X.latest`)
|
||||
3. Bump the version using `bumpversion`:
|
||||
- Dry run first by running `bumpversion --new-version <desired-version> <part>` and checking the diff. If it looks correct, clean up the chanages and move on:
|
||||
- Alpha releases: `bumpversion --commit --no-tag --new-version 0.10.2a1 num`
|
||||
- Patch releases: `bumpversion --commit --no-tag --new-version 0.10.2 patch`
|
||||
- Minor releases: `bumpversion --commit --no-tag --new-version 0.11.0 minor`
|
||||
- Major releases: `bumpversion --commit --no-tag --new-version 1.0.0 major`
|
||||
4. (If this is a not a release candidate) Merge to `x.x.latest` and (optionally) `master`.
|
||||
5. Update the default branch to the next dev release branch.
|
||||
6. Build source distributions for all packages by running `./scripts/build-sdists.sh`. Note that this will clean out your `dist/` folder, so if you have important stuff in there, don't run it!!!
|
||||
7. Deploy to pypi
|
||||
- `twine upload dist/*`
|
||||
8. Deploy to homebrew (see below)
|
||||
9. Deploy to conda-forge (see below)
|
||||
10. Git release notes (points to changelog)
|
||||
11. Post to slack (point to changelog)
|
||||
|
||||
After releasing a new version, it's important to merge the changes back into the other outstanding release branches. This avoids merge conflicts moving forward.
|
||||
|
||||
In some cases, where the branches have diverged wildly, it's ok to skip this step. But this means that the changes you just released won't be included in future releases.
|
||||
|
||||
#### Homebrew Release Process
|
||||
|
||||
1. Clone the `homebrew-dbt` repository:
|
||||
|
||||
```
|
||||
git clone git@github.com:fishtown-analytics/homebrew-dbt.git
|
||||
```
|
||||
|
||||
2. For ALL releases (prereleases and version releases), copy the relevant formula. To copy from the latest version release of dbt, do:
|
||||
|
||||
```bash
|
||||
cp Formula/dbt.rb Formula/dbt@{NEW-VERSION}.rb
|
||||
```
|
||||
|
||||
To copy from a different version, simply copy the corresponding file.
|
||||
|
||||
3. Open the file, and edit the following:
|
||||
- the name of the ruby class: this is important, homebrew won't function properly if the class name is wrong. Check historical versions to figure out the right name.
|
||||
- under the `bottle` section, remove all of the hashes (lines starting with `sha256`)
|
||||
|
||||
4. Create a **Python 3.7** virtualenv, activate it, and then install two packages: `homebrew-pypi-poet`, and the version of dbt you are preparing. I use:
|
||||
|
||||
```
|
||||
pyenv virtualenv 3.7.0 homebrew-dbt-{VERSION}
|
||||
pyenv activate homebrew-dbt-{VERSION}
|
||||
pip install dbt=={VERSION} homebrew-pypi-poet
|
||||
```
|
||||
|
||||
homebrew-pypi-poet is a program that generates a valid homebrew formula for an installed pip package. You want to use it to generate a diff against the existing formula. Then you want to apply the diff for the dependency packages only -- e.g. it will tell you that `google-api-core` has been updated and that you need to use the latest version.
|
||||
|
||||
5. reinstall, test, and audit dbt. if the test or audit fails, fix the formula with step 1.
|
||||
|
||||
```bash
|
||||
brew uninstall --force Formula/{YOUR-FILE}.rb
|
||||
brew install Formula/{YOUR-FILE}.rb
|
||||
brew test dbt
|
||||
brew audit --strict dbt
|
||||
```
|
||||
|
||||
6. Ask Connor to bottle the change (only his laptop can do it!)
|
||||
|
||||
#### Conda Forge Release Process
|
||||
|
||||
1. Clone the fork of `conda-forge/dbt-feedstock` [here](https://github.com/fishtown-analytics/dbt-feedstock)
|
||||
```bash
|
||||
git clone git@github.com:fishtown-analytics/dbt-feedstock.git
|
||||
|
||||
```
|
||||
2. Update the version and sha256 in `recipe/meta.yml`. To calculate the sha256, run:
|
||||
|
||||
```bash
|
||||
wget https://github.com/fishtown-analytics/dbt/archive/v{version}.tar.gz
|
||||
openssl sha256 v{version}.tar.gz
|
||||
```
|
||||
|
||||
3. Push the changes and create a PR against `conda-forge/dbt-feedstock`
|
||||
|
||||
4. Confirm that all automated conda-forge tests are passing
|
||||
@@ -1,125 +0,0 @@
|
||||
# Python package
|
||||
# Create and test a Python package on multiple Python versions.
|
||||
# Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/python
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- master
|
||||
- dev/*
|
||||
- pr/*
|
||||
|
||||
jobs:
|
||||
- job: UnitTest
|
||||
pool:
|
||||
vmImage: 'vs2017-win2016'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.7'
|
||||
architecture: 'x64'
|
||||
|
||||
- script: python -m pip install --upgrade pip && pip install tox
|
||||
displayName: 'Install dependencies'
|
||||
|
||||
- script: python -m tox -e pywin-unit
|
||||
displayName: Run unit tests
|
||||
|
||||
- job: PostgresIntegrationTest
|
||||
pool:
|
||||
vmImage: 'vs2017-win2016'
|
||||
dependsOn: UnitTest
|
||||
|
||||
steps:
|
||||
- pwsh: |
|
||||
choco install postgresql --params '/Password:password' --params-global --version 10.6
|
||||
Set-Content "c:\program files\postgresql\10\data\pg_hba.conf" "host all all ::1/128 trust"
|
||||
Add-Content "c:\program files\postgresql\10\data\pg_hba.conf" "host all all 127.0.0.1/32 trust"
|
||||
# the service name is "postgresql-x64-10", conveniently it's both the display name and the actual name
|
||||
Restart-Service postgresql-x64-10
|
||||
|
||||
& "C:\program files\postgresql\10\bin\createdb.exe" -U postgres dbt
|
||||
& "C:\program files\postgresql\10\bin\psql.exe" -U postgres -c "CREATE ROLE root WITH PASSWORD 'password';"
|
||||
& "C:\program files\postgresql\10\bin\psql.exe" -U postgres -c "ALTER ROLE root WITH LOGIN;"
|
||||
& "C:\program files\postgresql\10\bin\psql.exe" -U postgres -c "GRANT CREATE, CONNECT ON DATABASE dbt TO root WITH GRANT OPTION;"
|
||||
& "C:\program files\postgresql\10\bin\psql.exe" -U postgres -c "CREATE ROLE noaccess WITH PASSWORD 'password' NOSUPERUSER;"
|
||||
& "C:\program files\postgresql\10\bin\psql.exe" -U postgres -c "ALTER ROLE noaccess WITH LOGIN;"
|
||||
& "C:\program files\postgresql\10\bin\psql.exe" -U postgres -c "GRANT CONNECT ON DATABASE dbt TO noaccess;"
|
||||
displayName: Install postgresql and set up database
|
||||
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.7'
|
||||
architecture: 'x64'
|
||||
|
||||
- script: python -m pip install --upgrade pip && pip install tox
|
||||
displayName: 'Install dependencies'
|
||||
|
||||
- script: python -m tox -e pywin-postgres
|
||||
displayName: Run integration tests
|
||||
|
||||
# These three are all similar except secure environment variables, which MUST be passed along to their tasks,
|
||||
# but there's probably a better way to do this!
|
||||
- job: SnowflakeIntegrationTest
|
||||
pool:
|
||||
vmImage: 'vs2017-win2016'
|
||||
dependsOn: PostgresIntegrationTest
|
||||
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.7'
|
||||
architecture: 'x64'
|
||||
|
||||
- script: python -m pip install --upgrade pip && pip install tox
|
||||
displayName: 'Install dependencies'
|
||||
|
||||
- script: python -m tox -e pywin-snowflake
|
||||
env:
|
||||
SNOWFLAKE_TEST_ACCOUNT: $(SNOWFLAKE_TEST_ACCOUNT)
|
||||
SNOWFLAKE_TEST_PASSWORD: $(SNOWFLAKE_TEST_PASSWORD)
|
||||
SNOWFLAKE_TEST_USER: $(SNOWFLAKE_TEST_USER)
|
||||
SNOWFLAKE_TEST_WAREHOUSE: $(SNOWFLAKE_TEST_WAREHOUSE)
|
||||
displayName: Run integration tests
|
||||
|
||||
- job: BigQueryIntegrationTest
|
||||
pool:
|
||||
vmImage: 'vs2017-win2016'
|
||||
dependsOn: PostgresIntegrationTest
|
||||
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.7'
|
||||
architecture: 'x64'
|
||||
|
||||
- script: python -m pip install --upgrade pip && pip install tox
|
||||
displayName: 'Install dependencies'
|
||||
- script: python -m tox -e pywin-bigquery
|
||||
env:
|
||||
BIGQUERY_SERVICE_ACCOUNT_JSON: $(BIGQUERY_SERVICE_ACCOUNT_JSON)
|
||||
displayName: Run integration tests
|
||||
|
||||
- job: RedshiftIntegrationTest
|
||||
pool:
|
||||
vmImage: 'vs2017-win2016'
|
||||
dependsOn: PostgresIntegrationTest
|
||||
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.7'
|
||||
architecture: 'x64'
|
||||
|
||||
- script: python -m pip install --upgrade pip && pip install tox
|
||||
displayName: 'Install dependencies'
|
||||
|
||||
- script: python -m tox -e pywin-redshift
|
||||
env:
|
||||
REDSHIFT_TEST_DBNAME: $(REDSHIFT_TEST_DBNAME)
|
||||
REDSHIFT_TEST_PASS: $(REDSHIFT_TEST_PASS)
|
||||
REDSHIFT_TEST_USER: $(REDSHIFT_TEST_USER)
|
||||
REDSHIFT_TEST_PORT: $(REDSHIFT_TEST_PORT)
|
||||
REDSHIFT_TEST_HOST: $(REDSHIFT_TEST_HOST)
|
||||
displayName: Run integration tests
|
||||
73
converter.py
73
converter.py
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import json
|
||||
import yaml
|
||||
import sys
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import dbt.clients.registry as registry
|
||||
|
||||
|
||||
def yaml_type(fname):
|
||||
with open(fname) as f:
|
||||
return yaml.load(f)
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--project", type=yaml_type, default="dbt_project.yml")
|
||||
parser.add_argument("--namespace", required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def get_full_name(args):
|
||||
return "{}/{}".format(args.namespace, args.project["name"])
|
||||
|
||||
|
||||
def init_project_in_packages(args, packages):
|
||||
full_name = get_full_name(args)
|
||||
if full_name not in packages:
|
||||
packages[full_name] = {
|
||||
"name": args.project["name"],
|
||||
"namespace": args.namespace,
|
||||
"latest": args.project["version"],
|
||||
"assets": {},
|
||||
"versions": {},
|
||||
}
|
||||
return packages[full_name]
|
||||
|
||||
|
||||
def add_version_to_package(args, project_json):
|
||||
project_json["versions"][args.project["version"]] = {
|
||||
"id": "{}/{}".format(get_full_name(args), args.project["version"]),
|
||||
"name": args.project["name"],
|
||||
"version": args.project["version"],
|
||||
"description": "",
|
||||
"published_at": datetime.now(timezone.utc).astimezone().isoformat(),
|
||||
"packages": args.project.get("packages") or [],
|
||||
"works_with": [],
|
||||
"_source": {
|
||||
"type": "github",
|
||||
"url": "",
|
||||
"readme": "",
|
||||
},
|
||||
"downloads": {
|
||||
"tarball": "",
|
||||
"format": "tgz",
|
||||
"sha1": "",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
packages = registry.packages()
|
||||
project_json = init_project_in_packages(args, packages)
|
||||
if args.project["version"] in project_json["versions"]:
|
||||
raise Exception("Version {} already in packages JSON"
|
||||
.format(args.project["version"]),
|
||||
file=sys.stderr)
|
||||
add_version_to_package(args, project_json)
|
||||
print(json.dumps(packages, indent=2))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1 +1 @@
|
||||
recursive-include dbt/include *.py *.sql *.yml *.html *.md
|
||||
recursive-include dbt/include *.py *.sql *.yml *.html *.md .gitkeep .gitignore
|
||||
|
||||
42
core/README.md
Normal file
42
core/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/dbt-labs/dbt-core/fa1ea14ddfb1d5ae319d5141844910dd53ab2834/etc/dbt-core.svg" alt="dbt logo" width="750"/>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/dbt-labs/dbt-core/actions/workflows/main.yml">
|
||||
<img src="https://github.com/dbt-labs/dbt-core/actions/workflows/main.yml/badge.svg?event=push" alt="Unit Tests Badge"/>
|
||||
</a>
|
||||
<a href="https://github.com/dbt-labs/dbt-core/actions/workflows/integration.yml">
|
||||
<img src="https://github.com/dbt-labs/dbt-core/actions/workflows/integration.yml/badge.svg?event=push" alt="Integration Tests Badge"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
**[dbt](https://www.getdbt.com/)** enables data analysts and engineers to transform their data using the same practices that software engineers use to build applications.
|
||||
|
||||

|
||||
|
||||
## Understanding dbt
|
||||
|
||||
Analysts using dbt can transform their data by simply writing select statements, while dbt handles turning these statements into tables and views in a data warehouse.
|
||||
|
||||
These select statements, or "models", form a dbt project. Models frequently build on top of one another – dbt makes it easy to [manage relationships](https://docs.getdbt.com/docs/ref) between models, and [visualize these relationships](https://docs.getdbt.com/docs/documentation), as well as assure the quality of your transformations through [testing](https://docs.getdbt.com/docs/testing).
|
||||
|
||||

|
||||
|
||||
## Getting started
|
||||
|
||||
- [Install dbt](https://docs.getdbt.com/docs/installation)
|
||||
- Read the [introduction](https://docs.getdbt.com/docs/introduction/) and [viewpoint](https://docs.getdbt.com/docs/about/viewpoint/)
|
||||
|
||||
## Join the dbt Community
|
||||
|
||||
- Be part of the conversation in the [dbt Community Slack](http://community.getdbt.com/)
|
||||
- Read more on the [dbt Community Discourse](https://discourse.getdbt.com)
|
||||
|
||||
## Reporting bugs and contributing code
|
||||
|
||||
- Want to report a bug or request a feature? Let us know on [Slack](http://community.getdbt.com/), or open [an issue](https://github.com/dbt-labs/dbt-core/issues/new)
|
||||
- Want to help us build dbt? Check out the [Contributing Guide](https://github.com/dbt-labs/dbt-core/blob/HEAD/CONTRIBUTING.md)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Everyone interacting in the dbt project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [dbt Code of Conduct](https://community.getdbt.com/code-of-conduct).
|
||||
52
core/dbt/README.md
Normal file
52
core/dbt/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# core/dbt directory README
|
||||
|
||||
## The following are individual files in this directory.
|
||||
|
||||
### deprecations.py
|
||||
|
||||
### flags.py
|
||||
|
||||
### main.py
|
||||
|
||||
### tracking.py
|
||||
|
||||
### version.py
|
||||
|
||||
### lib.py
|
||||
|
||||
### node_types.py
|
||||
|
||||
### helper_types.py
|
||||
|
||||
### links.py
|
||||
|
||||
### semver.py
|
||||
|
||||
### ui.py
|
||||
|
||||
### compilation.py
|
||||
|
||||
### dataclass_schema.py
|
||||
|
||||
### exceptions.py
|
||||
|
||||
### hooks.py
|
||||
|
||||
### logger.py
|
||||
|
||||
### profiler.py
|
||||
|
||||
### utils.py
|
||||
|
||||
|
||||
## The subdirectories will be documented in a README in the subdirectory
|
||||
* config
|
||||
* include
|
||||
* adapters
|
||||
* context
|
||||
* deps
|
||||
* graph
|
||||
* task
|
||||
* clients
|
||||
* events
|
||||
|
||||
1
core/dbt/adapters/README.md
Normal file
1
core/dbt/adapters/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Adapters README
|
||||
@@ -4,7 +4,11 @@
|
||||
from dbt.contracts.connection import Credentials # noqa
|
||||
from dbt.adapters.base.meta import available # noqa
|
||||
from dbt.adapters.base.connections import BaseConnectionManager # noqa
|
||||
from dbt.adapters.base.relation import BaseRelation, RelationType # noqa
|
||||
from dbt.adapters.base.relation import ( # noqa
|
||||
BaseRelation,
|
||||
RelationType,
|
||||
SchemaSearchMap,
|
||||
)
|
||||
from dbt.adapters.base.column import Column # noqa
|
||||
from dbt.adapters.base.impl import BaseAdapter # noqa
|
||||
from dbt.adapters.base.impl import AdapterConfig, BaseAdapter # noqa
|
||||
from dbt.adapters.base.plugin import AdapterPlugin # noqa
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
from dataclasses import dataclass
|
||||
import re
|
||||
from typing import Dict, ClassVar, Any, Optional
|
||||
|
||||
from hologram import JsonSchemaMixin
|
||||
|
||||
from typing import TypeVar, Dict, ClassVar, Any, Optional, Type
|
||||
|
||||
Self = TypeVar('Self', bound='Column')
|
||||
from dbt.exceptions import RuntimeException
|
||||
|
||||
|
||||
@dataclass
|
||||
class Column(JsonSchemaMixin):
|
||||
class Column:
|
||||
TYPE_LABELS: ClassVar[Dict[str, str]] = {
|
||||
'STRING': 'TEXT',
|
||||
'TIMESTAMP': 'TIMESTAMP',
|
||||
@@ -26,7 +24,7 @@ class Column(JsonSchemaMixin):
|
||||
return cls.TYPE_LABELS.get(dtype.upper(), dtype)
|
||||
|
||||
@classmethod
|
||||
def create(cls: Type[Self], name, label_or_dtype: str) -> Self:
|
||||
def create(cls, name, label_or_dtype: str) -> 'Column':
|
||||
column_type = cls.translate_type(label_or_dtype)
|
||||
return cls(name, column_type)
|
||||
|
||||
@@ -52,12 +50,31 @@ class Column(JsonSchemaMixin):
|
||||
return self.dtype.lower() in ['text', 'character varying', 'character',
|
||||
'varchar']
|
||||
|
||||
def is_number(self):
|
||||
return any([self.is_integer(), self.is_numeric(), self.is_float()])
|
||||
|
||||
def is_float(self):
|
||||
return self.dtype.lower() in [
|
||||
# floats
|
||||
'real', 'float4', 'float', 'double precision', 'float8'
|
||||
]
|
||||
|
||||
def is_integer(self) -> bool:
|
||||
return self.dtype.lower() in [
|
||||
# real types
|
||||
'smallint', 'integer', 'bigint',
|
||||
'smallserial', 'serial', 'bigserial',
|
||||
# aliases
|
||||
'int2', 'int4', 'int8',
|
||||
'serial2', 'serial4', 'serial8',
|
||||
]
|
||||
|
||||
def is_numeric(self) -> bool:
|
||||
return self.dtype.lower() in ['numeric', 'number']
|
||||
return self.dtype.lower() in ['numeric', 'decimal']
|
||||
|
||||
def string_size(self) -> int:
|
||||
if not self.is_string():
|
||||
raise RuntimeError("Called string_size() on non-string field!")
|
||||
raise RuntimeException("Called string_size() on non-string field!")
|
||||
|
||||
if self.dtype == 'text' or self.char_size is None:
|
||||
# char_size should never be None. Handle it reasonably just in case
|
||||
@@ -65,7 +82,7 @@ class Column(JsonSchemaMixin):
|
||||
else:
|
||||
return int(self.char_size)
|
||||
|
||||
def can_expand_to(self: Self, other_column: Self) -> bool:
|
||||
def can_expand_to(self, other_column: 'Column') -> bool:
|
||||
"""returns True if this column can be expanded to the size of the
|
||||
other column"""
|
||||
if not self.is_string() or not other_column.is_string():
|
||||
@@ -91,3 +108,46 @@ class Column(JsonSchemaMixin):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<Column {} ({})>".format(self.name, self.data_type)
|
||||
|
||||
@classmethod
|
||||
def from_description(cls, name: str, raw_data_type: str) -> 'Column':
|
||||
match = re.match(r'([^(]+)(\([^)]+\))?', raw_data_type)
|
||||
if match is None:
|
||||
raise RuntimeException(
|
||||
f'Could not interpret data type "{raw_data_type}"'
|
||||
)
|
||||
data_type, size_info = match.groups()
|
||||
char_size = None
|
||||
numeric_precision = None
|
||||
numeric_scale = None
|
||||
if size_info is not None:
|
||||
# strip out the parentheses
|
||||
size_info = size_info[1:-1]
|
||||
parts = size_info.split(',')
|
||||
if len(parts) == 1:
|
||||
try:
|
||||
char_size = int(parts[0])
|
||||
except ValueError:
|
||||
raise RuntimeException(
|
||||
f'Could not interpret data_type "{raw_data_type}": '
|
||||
f'could not convert "{parts[0]}" to an integer'
|
||||
)
|
||||
elif len(parts) == 2:
|
||||
try:
|
||||
numeric_precision = int(parts[0])
|
||||
except ValueError:
|
||||
raise RuntimeException(
|
||||
f'Could not interpret data_type "{raw_data_type}": '
|
||||
f'could not convert "{parts[0]}" to an integer'
|
||||
)
|
||||
try:
|
||||
numeric_scale = int(parts[1])
|
||||
except ValueError:
|
||||
raise RuntimeException(
|
||||
f'Could not interpret data_type "{raw_data_type}": '
|
||||
f'could not convert "{parts[1]}" to an integer'
|
||||
)
|
||||
|
||||
return cls(
|
||||
name, data_type, char_size, numeric_precision, numeric_scale
|
||||
)
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
import abc
|
||||
import os
|
||||
from multiprocessing import RLock
|
||||
# multiprocessing.RLock is a function returning this type
|
||||
from multiprocessing.synchronize import RLock
|
||||
from threading import get_ident
|
||||
from typing import (
|
||||
Dict, Tuple, Hashable, Optional, ContextManager, List
|
||||
Dict, Tuple, Hashable, Optional, ContextManager, List, Union
|
||||
)
|
||||
|
||||
import agate
|
||||
|
||||
import dbt.exceptions
|
||||
import dbt.flags
|
||||
from dbt.contracts.connection import (
|
||||
Connection, Identifier, ConnectionState, AdapterRequiredConfig, LazyHandle
|
||||
Connection, Identifier, ConnectionState,
|
||||
AdapterRequiredConfig, LazyHandle, AdapterResponse
|
||||
)
|
||||
from dbt.contracts.graph.manifest import Manifest
|
||||
from dbt.adapters.base.query_headers import (
|
||||
QueryStringSetter, MacroQueryStringSetter,
|
||||
MacroQueryStringSetter,
|
||||
)
|
||||
from dbt.logger import GLOBAL_LOGGER as logger
|
||||
from dbt.events.functions import fire_event
|
||||
from dbt.events.types import (
|
||||
NewConnection,
|
||||
ConnectionReused,
|
||||
ConnectionLeftOpen,
|
||||
ConnectionLeftOpen2,
|
||||
ConnectionClosed,
|
||||
ConnectionClosed2,
|
||||
Rollback,
|
||||
RollbackFailed
|
||||
)
|
||||
from dbt import flags
|
||||
|
||||
|
||||
class BaseConnectionManager(metaclass=abc.ABCMeta):
|
||||
@@ -37,14 +50,11 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
|
||||
def __init__(self, profile: AdapterRequiredConfig):
|
||||
self.profile = profile
|
||||
self.thread_connections: Dict[Hashable, Connection] = {}
|
||||
self.lock: RLock = dbt.flags.MP_CONTEXT.RLock()
|
||||
self.query_header = QueryStringSetter(self.profile)
|
||||
self.lock: RLock = flags.MP_CONTEXT.RLock()
|
||||
self.query_header: Optional[MacroQueryStringSetter] = None
|
||||
|
||||
def set_query_header(self, manifest=None) -> None:
|
||||
if manifest is not None:
|
||||
self.query_header = MacroQueryStringSetter(self.profile, manifest)
|
||||
else:
|
||||
self.query_header = QueryStringSetter(self.profile)
|
||||
def set_query_header(self, manifest: Manifest) -> None:
|
||||
self.query_header = MacroQueryStringSetter(self.profile, manifest)
|
||||
|
||||
@staticmethod
|
||||
def get_thread_identifier() -> Hashable:
|
||||
@@ -61,7 +71,7 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
|
||||
)
|
||||
return self.thread_connections[key]
|
||||
|
||||
def set_thread_connection(self, conn):
|
||||
def set_thread_connection(self, conn: Connection) -> None:
|
||||
key = self.get_thread_identifier()
|
||||
if key in self.thread_connections:
|
||||
raise dbt.exceptions.InternalException(
|
||||
@@ -89,6 +99,11 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
|
||||
self.begin()
|
||||
self.commit()
|
||||
|
||||
def rollback_if_open(self) -> None:
|
||||
conn = self.get_if_exists()
|
||||
if conn is not None and conn.handle and conn.transaction_open:
|
||||
self._rollback(conn)
|
||||
|
||||
@abc.abstractmethod
|
||||
def exception_handler(self, sql: str) -> ContextManager:
|
||||
"""Create a context manager that handles exceptions caused by database
|
||||
@@ -131,20 +146,12 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
|
||||
if conn.name == conn_name and conn.state == 'open':
|
||||
return conn
|
||||
|
||||
logger.debug(
|
||||
'Acquiring new {} connection "{}".'.format(self.TYPE, conn_name))
|
||||
fire_event(NewConnection(conn_name=conn_name, conn_type=self.TYPE))
|
||||
|
||||
if conn.state == 'open':
|
||||
logger.debug(
|
||||
'Re-using an available connection from the pool (formerly {}).'
|
||||
.format(conn.name)
|
||||
)
|
||||
fire_event(ConnectionReused(conn_name=conn_name))
|
||||
else:
|
||||
logger.debug(
|
||||
'Opening a new connection, currently in state {}'
|
||||
.format(conn.state)
|
||||
)
|
||||
conn.handle = LazyHandle(type(self))
|
||||
conn.handle = LazyHandle(self.open)
|
||||
|
||||
conn.name = conn_name
|
||||
return conn
|
||||
@@ -177,11 +184,9 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
|
||||
return
|
||||
|
||||
try:
|
||||
if conn.state == 'open':
|
||||
if conn.transaction_open is True:
|
||||
self._rollback(conn)
|
||||
else:
|
||||
self.close(conn)
|
||||
# always close the connection. close() calls _rollback() if there
|
||||
# is an open transaction
|
||||
self.close(conn)
|
||||
except Exception:
|
||||
# if rollback or close failed, remove our busted connection
|
||||
self.clear_thread_connection()
|
||||
@@ -191,11 +196,9 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
|
||||
with self.lock:
|
||||
for connection in self.thread_connections.values():
|
||||
if connection.state not in {'closed', 'init'}:
|
||||
logger.debug("Connection '{}' was left open."
|
||||
.format(connection.name))
|
||||
fire_event(ConnectionLeftOpen(conn_name=connection.name))
|
||||
else:
|
||||
logger.debug("Connection '{}' was properly closed."
|
||||
.format(connection.name))
|
||||
fire_event(ConnectionClosed(conn_name=connection.name))
|
||||
self.close(connection)
|
||||
|
||||
# garbage collect these connections
|
||||
@@ -221,54 +224,40 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
|
||||
try:
|
||||
connection.handle.rollback()
|
||||
except Exception:
|
||||
logger.debug(
|
||||
'Failed to rollback {}'.format(connection.name),
|
||||
exc_info=True
|
||||
)
|
||||
fire_event(RollbackFailed(conn_name=connection.name))
|
||||
|
||||
@classmethod
|
||||
def _close_handle(cls, connection: Connection) -> None:
|
||||
"""Perform the actual close operation."""
|
||||
# On windows, sometimes connection handles don't have a close() attr.
|
||||
if hasattr(connection.handle, 'close'):
|
||||
logger.debug('On {}: Close'.format(connection.name))
|
||||
fire_event(ConnectionClosed2(conn_name=connection.name))
|
||||
connection.handle.close()
|
||||
else:
|
||||
logger.debug('On {}: No close available on handle'
|
||||
.format(connection.name))
|
||||
fire_event(ConnectionLeftOpen2(conn_name=connection.name))
|
||||
|
||||
@classmethod
|
||||
def _rollback(cls, connection: Connection) -> None:
|
||||
"""Roll back the given connection."""
|
||||
if dbt.flags.STRICT_MODE:
|
||||
if not isinstance(connection, Connection):
|
||||
raise dbt.exceptions.CompilerException(
|
||||
f'In _rollback, got {connection} - not a Connection!'
|
||||
)
|
||||
|
||||
if connection.transaction_open is False:
|
||||
raise dbt.exceptions.InternalException(
|
||||
'Tried to rollback transaction on connection "{}", but '
|
||||
'it does not have one open!'.format(connection.name))
|
||||
f'Tried to rollback transaction on connection '
|
||||
f'"{connection.name}", but it does not have one open!'
|
||||
)
|
||||
|
||||
logger.debug('On {}: ROLLBACK'.format(connection.name))
|
||||
fire_event(Rollback(conn_name=connection.name))
|
||||
cls._rollback_handle(connection)
|
||||
|
||||
connection.transaction_open = False
|
||||
|
||||
@classmethod
|
||||
def close(cls, connection: Connection) -> Connection:
|
||||
if dbt.flags.STRICT_MODE:
|
||||
if not isinstance(connection, Connection):
|
||||
raise dbt.exceptions.CompilerException(
|
||||
f'In close, got {connection} - not a Connection!'
|
||||
)
|
||||
|
||||
# if the connection is in closed or init, there's nothing to do
|
||||
if connection.state in {ConnectionState.CLOSED, ConnectionState.INIT}:
|
||||
return connection
|
||||
|
||||
if connection.transaction_open and connection.handle:
|
||||
fire_event(Rollback(conn_name=connection.name))
|
||||
cls._rollback_handle(connection)
|
||||
connection.transaction_open = False
|
||||
|
||||
@@ -284,12 +273,14 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
|
||||
self.commit()
|
||||
|
||||
def _add_query_comment(self, sql: str) -> str:
|
||||
if self.query_header is None:
|
||||
return sql
|
||||
return self.query_header.add(sql)
|
||||
|
||||
@abc.abstractmethod
|
||||
def execute(
|
||||
self, sql: str, auto_begin: bool = False, fetch: bool = False
|
||||
) -> Tuple[str, agate.Table]:
|
||||
) -> Tuple[Union[str, AdapterResponse], agate.Table]:
|
||||
"""Execute the given SQL.
|
||||
|
||||
:param str sql: The sql to execute.
|
||||
@@ -297,7 +288,7 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
|
||||
transaction, automatically begin one.
|
||||
:param bool fetch: If set, fetch results.
|
||||
:return: A tuple of the status and the results (empty if fetch=False).
|
||||
:rtype: Tuple[str, agate.Table]
|
||||
:rtype: Tuple[Union[str, AdapterResponse], agate.Table]
|
||||
"""
|
||||
raise dbt.exceptions.NotImplementedException(
|
||||
'`execute` is not implemented for this adapter!'
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import abc
|
||||
from concurrent.futures import as_completed, Future
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
from itertools import chain
|
||||
from typing import (
|
||||
Optional, Tuple, Callable, Container, FrozenSet, Type, Dict, Any, List,
|
||||
Mapping, Iterator, Union
|
||||
Optional, Tuple, Callable, Iterable, Type, Dict, Any, List, Mapping,
|
||||
Iterator, Union, Set
|
||||
)
|
||||
|
||||
import agate
|
||||
@@ -14,24 +16,30 @@ from dbt.exceptions import (
|
||||
get_relation_returned_multiple_results,
|
||||
InternalException, NotImplementedException, RuntimeException,
|
||||
)
|
||||
import dbt.flags
|
||||
|
||||
from dbt import deprecations
|
||||
from dbt.clients.agate_helper import empty_table
|
||||
from dbt.contracts.graph.compiled import CompileResultNode, CompiledSeedNode
|
||||
from dbt.contracts.graph.manifest import Manifest
|
||||
from dbt.adapters.protocol import (
|
||||
AdapterConfig,
|
||||
ConnectionManagerProtocol,
|
||||
)
|
||||
from dbt.clients.agate_helper import empty_table, merge_tables, table_from_rows
|
||||
from dbt.clients.jinja import MacroGenerator
|
||||
from dbt.contracts.graph.compiled import (
|
||||
CompileResultNode, CompiledSeedNode
|
||||
)
|
||||
from dbt.contracts.graph.manifest import Manifest, MacroManifest
|
||||
from dbt.contracts.graph.parsed import ParsedSeedNode
|
||||
from dbt.node_types import NodeType
|
||||
from dbt.logger import GLOBAL_LOGGER as logger
|
||||
from dbt.utils import filter_null_values
|
||||
from dbt.exceptions import warn_or_error
|
||||
from dbt.events.functions import fire_event
|
||||
from dbt.events.types import CacheMiss, ListRelations
|
||||
from dbt.utils import filter_null_values, executor
|
||||
|
||||
from dbt.adapters.base.connections import BaseConnectionManager, Connection
|
||||
from dbt.adapters.base.connections import Connection, AdapterResponse
|
||||
from dbt.adapters.base.meta import AdapterMeta, available
|
||||
from dbt.adapters.base.relation import (
|
||||
ComponentName, BaseRelation, InformationSchema
|
||||
ComponentName, BaseRelation, InformationSchema, SchemaSearchMap
|
||||
)
|
||||
from dbt.adapters.base import Column as BaseColumn
|
||||
from dbt.adapters.cache import RelationsCache
|
||||
from dbt.adapters.cache import RelationsCache, _make_key
|
||||
|
||||
|
||||
SeedModel = Union[ParsedSeedNode, CompiledSeedNode]
|
||||
@@ -50,21 +58,6 @@ def _expect_row_value(key: str, row: agate.Row):
|
||||
return row[key]
|
||||
|
||||
|
||||
def _relations_filter_schemas(
|
||||
schemas: Container[str]
|
||||
) -> Callable[[agate.Row], bool]:
|
||||
def test(row):
|
||||
referenced_schema = _expect_row_value('referenced_schema', row)
|
||||
dependent_schema = _expect_row_value('dependent_schema', row)
|
||||
# handle the null schema
|
||||
if referenced_schema is not None:
|
||||
referenced_schema = referenced_schema.lower()
|
||||
if dependent_schema is not None:
|
||||
dependent_schema = dependent_schema.lower()
|
||||
return referenced_schema in schemas or dependent_schema in schemas
|
||||
return test
|
||||
|
||||
|
||||
def _catalog_filter_schemas(manifest: Manifest) -> Callable[[agate.Row], bool]:
|
||||
"""Return a function that takes a row and decides if the row should be
|
||||
included in the catalog output.
|
||||
@@ -117,52 +110,6 @@ def _relation_name(rel: Optional[BaseRelation]) -> str:
|
||||
return str(rel)
|
||||
|
||||
|
||||
class SchemaSearchMap(dict):
|
||||
"""A utility class to keep track of what information_schema tables to
|
||||
search for what schemas
|
||||
"""
|
||||
def add(self, relation: BaseRelation):
|
||||
key = relation.information_schema_only()
|
||||
if key not in self:
|
||||
self[key] = set()
|
||||
self[key].add(relation.schema.lower())
|
||||
|
||||
def search(self):
|
||||
for information_schema_name, schemas in self.items():
|
||||
for schema in schemas:
|
||||
yield information_schema_name, schema
|
||||
|
||||
def schemas_searched(self):
|
||||
result = set()
|
||||
for information_schema_name, schemas in self.items():
|
||||
result.update(
|
||||
(information_schema_name.database, schema)
|
||||
for schema in schemas
|
||||
)
|
||||
return result
|
||||
|
||||
def flatten(self):
|
||||
new = self.__class__()
|
||||
|
||||
# make sure we don't have duplicates
|
||||
seen = {r.database.lower() for r in self if r.database}
|
||||
if len(seen) > 1:
|
||||
raise_compiler_error(str(seen))
|
||||
|
||||
for information_schema_name, schema in self.search():
|
||||
path = {
|
||||
'database': information_schema_name.database,
|
||||
'schema': schema
|
||||
}
|
||||
new.add(information_schema_name.incorporate(
|
||||
path=path,
|
||||
quote_policy={'database': False},
|
||||
include_policy={'database': False},
|
||||
))
|
||||
|
||||
return new
|
||||
|
||||
|
||||
class BaseAdapter(metaclass=AdapterMeta):
|
||||
"""The BaseAdapter provides an abstract base class for adapters.
|
||||
|
||||
@@ -201,17 +148,17 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
"""
|
||||
Relation: Type[BaseRelation] = BaseRelation
|
||||
Column: Type[BaseColumn] = BaseColumn
|
||||
ConnectionManager: Type[BaseConnectionManager]
|
||||
ConnectionManager: Type[ConnectionManagerProtocol]
|
||||
|
||||
# A set of clobber config fields accepted by this adapter
|
||||
# for use in materializations
|
||||
AdapterSpecificConfigs: FrozenSet[str] = frozenset()
|
||||
AdapterSpecificConfigs: Type[AdapterConfig] = AdapterConfig
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.cache = RelationsCache()
|
||||
self.connections = self.ConnectionManager(config)
|
||||
self._internal_manifest_lazy: Optional[Manifest] = None
|
||||
self._macro_manifest_lazy: Optional[MacroManifest] = None
|
||||
|
||||
###
|
||||
# Methods that pass through to the connection manager
|
||||
@@ -231,6 +178,9 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
def commit_if_has_connection(self) -> None:
|
||||
self.connections.commit_if_has_connection()
|
||||
|
||||
def debug_query(self) -> None:
|
||||
self.execute('select 1 as id')
|
||||
|
||||
def nice_connection_name(self) -> str:
|
||||
conn = self.connections.get_if_exists()
|
||||
if conn is None or conn.name is None:
|
||||
@@ -242,12 +192,14 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
self, name: str, node: Optional[CompileResultNode] = None
|
||||
) -> Iterator[None]:
|
||||
try:
|
||||
self.connections.query_header.set(name, node)
|
||||
if self.connections.query_header is not None:
|
||||
self.connections.query_header.set(name, node)
|
||||
self.acquire_connection(name)
|
||||
yield
|
||||
finally:
|
||||
self.release_connection()
|
||||
self.connections.query_header.reset()
|
||||
if self.connections.query_header is not None:
|
||||
self.connections.query_header.reset()
|
||||
|
||||
@contextmanager
|
||||
def connection_for(
|
||||
@@ -259,7 +211,7 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
@available.parse(lambda *a, **k: ('', empty_table()))
|
||||
def execute(
|
||||
self, sql: str, auto_begin: bool = False, fetch: bool = False
|
||||
) -> Tuple[str, agate.Table]:
|
||||
) -> Tuple[Union[str, AdapterResponse], agate.Table]:
|
||||
"""Execute the given SQL. This is a thin wrapper around
|
||||
ConnectionManager.execute.
|
||||
|
||||
@@ -268,7 +220,7 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
transaction, automatically begin one.
|
||||
:param bool fetch: If set, fetch results.
|
||||
:return: A tuple of the status and the results (empty if fetch=False).
|
||||
:rtype: Tuple[str, agate.Table]
|
||||
:rtype: Tuple[Union[str, AdapterResponse], agate.Table]
|
||||
"""
|
||||
return self.connections.execute(
|
||||
sql=sql,
|
||||
@@ -276,6 +228,21 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
fetch=fetch
|
||||
)
|
||||
|
||||
@available.parse(lambda *a, **k: ('', empty_table()))
|
||||
def get_partitions_metadata(
|
||||
self, table: str
|
||||
) -> Tuple[agate.Table]:
|
||||
"""Obtain partitions metadata for a BigQuery partitioned table.
|
||||
|
||||
:param str table_id: a partitioned table id, in standard SQL format.
|
||||
:return: a partition metadata tuple, as described in
|
||||
https://cloud.google.com/bigquery/docs/creating-partitioned-tables#getting_partition_metadata_using_meta_tables.
|
||||
:rtype: agate.Table
|
||||
"""
|
||||
return self.connections.get_partitions_metadata(
|
||||
table=table
|
||||
)
|
||||
|
||||
###
|
||||
# Methods that should never be overridden
|
||||
###
|
||||
@@ -290,45 +257,63 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
return cls.ConnectionManager.TYPE
|
||||
|
||||
@property
|
||||
def _internal_manifest(self) -> Manifest:
|
||||
if self._internal_manifest_lazy is None:
|
||||
return self.load_internal_manifest()
|
||||
return self._internal_manifest_lazy
|
||||
def _macro_manifest(self) -> MacroManifest:
|
||||
if self._macro_manifest_lazy is None:
|
||||
return self.load_macro_manifest()
|
||||
return self._macro_manifest_lazy
|
||||
|
||||
def check_internal_manifest(self) -> Optional[Manifest]:
|
||||
def check_macro_manifest(self) -> Optional[MacroManifest]:
|
||||
"""Return the internal manifest (used for executing macros) if it's
|
||||
been initialized, otherwise return None.
|
||||
"""
|
||||
return self._internal_manifest_lazy
|
||||
return self._macro_manifest_lazy
|
||||
|
||||
def load_internal_manifest(self) -> Manifest:
|
||||
if self._internal_manifest_lazy is None:
|
||||
def load_macro_manifest(self) -> MacroManifest:
|
||||
if self._macro_manifest_lazy is None:
|
||||
# avoid a circular import
|
||||
from dbt.parser.manifest import load_internal_manifest
|
||||
manifest = load_internal_manifest(self.config)
|
||||
self._internal_manifest_lazy = manifest
|
||||
return self._internal_manifest_lazy
|
||||
from dbt.parser.manifest import ManifestLoader
|
||||
manifest = ManifestLoader.load_macros(
|
||||
self.config, self.connections.set_query_header
|
||||
)
|
||||
self._macro_manifest_lazy = manifest
|
||||
return self._macro_manifest_lazy
|
||||
|
||||
def clear_macro_manifest(self):
|
||||
if self._macro_manifest_lazy is not None:
|
||||
self._macro_manifest_lazy = None
|
||||
|
||||
###
|
||||
# Caching methods
|
||||
###
|
||||
def _schema_is_cached(self, database: str, schema: str) -> bool:
|
||||
def _schema_is_cached(self, database: Optional[str], schema: str) -> bool:
|
||||
"""Check if the schema is cached, and by default logs if it is not."""
|
||||
|
||||
if dbt.flags.USE_CACHE is False:
|
||||
return False
|
||||
elif (database, schema) not in self.cache:
|
||||
logger.debug(
|
||||
'On "{}": cache miss for schema "{}.{}", this is inefficient'
|
||||
.format(self.nice_connection_name(), database, schema)
|
||||
if (database, schema) not in self.cache:
|
||||
fire_event(
|
||||
CacheMiss(
|
||||
conn_name=self.nice_connection_name(),
|
||||
database=database,
|
||||
schema=schema
|
||||
)
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def _get_cache_schemas(
|
||||
self, manifest: Manifest, exec_only: bool = False
|
||||
) -> SchemaSearchMap:
|
||||
def _get_cache_schemas(self, manifest: Manifest) -> Set[BaseRelation]:
|
||||
"""Get the set of schema relations that the cache logic needs to
|
||||
populate. This means only executable nodes are included.
|
||||
"""
|
||||
# the cache only cares about executable nodes
|
||||
return {
|
||||
self.Relation.create_from(self.config, node).without_identifier()
|
||||
for node in manifest.nodes.values()
|
||||
if (
|
||||
node.is_relational and not node.is_ephemeral_model
|
||||
)
|
||||
}
|
||||
|
||||
def _get_catalog_schemas(self, manifest: Manifest) -> SchemaSearchMap:
|
||||
"""Get a mapping of each node's "information_schema" relations to a
|
||||
set of all schemas expected in that information_schema.
|
||||
|
||||
@@ -338,9 +323,13 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
lowercase strings.
|
||||
"""
|
||||
info_schema_name_map = SchemaSearchMap()
|
||||
for node in manifest.nodes.values():
|
||||
if exec_only and node.resource_type not in NodeType.executable():
|
||||
continue
|
||||
nodes: Iterator[CompileResultNode] = chain(
|
||||
[node for node in manifest.nodes.values() if (
|
||||
node.is_relational and not node.is_ephemeral_model
|
||||
)],
|
||||
manifest.sources.values(),
|
||||
)
|
||||
for node in nodes:
|
||||
relation = self.Relation.create_from(self.config, node)
|
||||
info_schema_name_map.add(relation)
|
||||
# result is a map whose keys are information_schema Relations without
|
||||
@@ -353,19 +342,31 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
"""Populate the relations cache for the given schemas. Returns an
|
||||
iterable of the schemas populated, as strings.
|
||||
"""
|
||||
if not dbt.flags.USE_CACHE:
|
||||
return
|
||||
cache_schemas = self._get_cache_schemas(manifest)
|
||||
with executor(self.config) as tpe:
|
||||
futures: List[Future[List[BaseRelation]]] = []
|
||||
for cache_schema in cache_schemas:
|
||||
fut = tpe.submit_connected(
|
||||
self,
|
||||
f'list_{cache_schema.database}_{cache_schema.schema}',
|
||||
self.list_relations_without_caching,
|
||||
cache_schema
|
||||
)
|
||||
futures.append(fut)
|
||||
|
||||
info_schema_name_map = self._get_cache_schemas(manifest,
|
||||
exec_only=True)
|
||||
for db, schema in info_schema_name_map.search():
|
||||
for relation in self.list_relations_without_caching(db, schema):
|
||||
self.cache.add(relation)
|
||||
for future in as_completed(futures):
|
||||
# if we can't read the relations we need to just raise anyway,
|
||||
# so just call future.result() and let that raise on failure
|
||||
for relation in future.result():
|
||||
self.cache.add(relation)
|
||||
|
||||
# it's possible that there were no relations in some schemas. We want
|
||||
# to insert the schemas we query into the cache's `.schemas` attribute
|
||||
# so we can check it later
|
||||
self.cache.update_schemas(info_schema_name_map.schemas_searched())
|
||||
cache_update: Set[Tuple[Optional[str], Optional[str]]] = set()
|
||||
for relation in cache_schemas:
|
||||
cache_update.add((relation.database, relation.schema))
|
||||
self.cache.update_schemas(cache_update)
|
||||
|
||||
def set_relations_cache(
|
||||
self, manifest: Manifest, clear: bool = False
|
||||
@@ -373,9 +374,6 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
"""Run a query that gets a populated cache of the relations in the
|
||||
database and set the cache on this adapter.
|
||||
"""
|
||||
if not dbt.flags.USE_CACHE:
|
||||
return
|
||||
|
||||
with self.cache.lock:
|
||||
if clear:
|
||||
self.cache.clear()
|
||||
@@ -389,8 +387,7 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
raise_compiler_error(
|
||||
'Attempted to cache a null relation for {}'.format(name)
|
||||
)
|
||||
if dbt.flags.USE_CACHE:
|
||||
self.cache.add(relation)
|
||||
self.cache.add(relation)
|
||||
# so jinja doesn't render things
|
||||
return ''
|
||||
|
||||
@@ -404,8 +401,7 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
raise_compiler_error(
|
||||
'Attempted to drop a null relation for {}'.format(name)
|
||||
)
|
||||
if dbt.flags.USE_CACHE:
|
||||
self.cache.drop(relation)
|
||||
self.cache.drop(relation)
|
||||
return ''
|
||||
|
||||
@available
|
||||
@@ -426,8 +422,7 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
.format(src_name, dst_name, name)
|
||||
)
|
||||
|
||||
if dbt.flags.USE_CACHE:
|
||||
self.cache.rename(from_relation, to_relation)
|
||||
self.cache.rename(from_relation, to_relation)
|
||||
return ''
|
||||
|
||||
###
|
||||
@@ -509,7 +504,7 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
def get_columns_in_relation(
|
||||
self, relation: BaseRelation
|
||||
) -> List[BaseColumn]:
|
||||
"""Get a list of the columns in the given Relation."""
|
||||
"""Get a list of the columns in the given Relation. """
|
||||
raise NotImplementedException(
|
||||
'`get_columns_in_relation` is not implemented for this adapter!'
|
||||
)
|
||||
@@ -544,15 +539,14 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
|
||||
@abc.abstractmethod
|
||||
def list_relations_without_caching(
|
||||
self, information_schema: BaseRelation, schema: str
|
||||
self, schema_relation: BaseRelation
|
||||
) -> List[BaseRelation]:
|
||||
"""List relations in the given schema, bypassing the cache.
|
||||
|
||||
This is used as the underlying behavior to fill the cache.
|
||||
|
||||
:param Relation information_schema: The information schema to list
|
||||
relations from.
|
||||
:param str schema: The name of the schema to list relations from.
|
||||
:param schema_relation: A relation containing the database and schema
|
||||
as appropraite for the underlying data warehouse
|
||||
:return: The relations in schema
|
||||
:rtype: List[self.Relation]
|
||||
"""
|
||||
@@ -664,25 +658,30 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
|
||||
self.expand_column_types(from_relation, to_relation)
|
||||
|
||||
def list_relations(self, database: str, schema: str) -> List[BaseRelation]:
|
||||
def list_relations(
|
||||
self, database: Optional[str], schema: str
|
||||
) -> List[BaseRelation]:
|
||||
if self._schema_is_cached(database, schema):
|
||||
return self.cache.get_relations(database, schema)
|
||||
|
||||
information_schema = self.Relation.create(
|
||||
schema_relation = self.Relation.create(
|
||||
database=database,
|
||||
schema=schema,
|
||||
identifier='',
|
||||
quote_policy=self.config.quoting
|
||||
).information_schema()
|
||||
).without_identifier()
|
||||
|
||||
# we can't build the relations cache because we don't have a
|
||||
# manifest so we can't run any operations.
|
||||
relations = self.list_relations_without_caching(
|
||||
information_schema, schema
|
||||
schema_relation
|
||||
)
|
||||
fire_event(ListRelations(
|
||||
database=database,
|
||||
schema=schema,
|
||||
relations=[_make_key(x) for x in relations]
|
||||
))
|
||||
|
||||
logger.debug('with database={}, schema={}, relations={}'
|
||||
.format(database, schema, relations))
|
||||
return relations
|
||||
|
||||
def _make_match_kwargs(
|
||||
@@ -759,7 +758,7 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
###
|
||||
@abc.abstractmethod
|
||||
@available.parse_none
|
||||
def create_schema(self, database: str, schema: str):
|
||||
def create_schema(self, relation: BaseRelation):
|
||||
"""Create the given schema if it does not exist."""
|
||||
raise NotImplementedException(
|
||||
'`create_schema` is not implemented for this adapter!'
|
||||
@@ -767,7 +766,7 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
|
||||
@abc.abstractmethod
|
||||
@available.parse_none
|
||||
def drop_schema(self, database: str, schema: str):
|
||||
def drop_schema(self, relation: BaseRelation):
|
||||
"""Drop the given schema (and everything in it) if it exists."""
|
||||
raise NotImplementedException(
|
||||
'`drop_schema` is not implemented for this adapter!'
|
||||
@@ -804,12 +803,11 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
def quote_seed_column(
|
||||
self, column: str, quote_config: Optional[bool]
|
||||
) -> str:
|
||||
# this is the default for now
|
||||
quote_columns: bool = False
|
||||
quote_columns: bool = True
|
||||
if isinstance(quote_config, bool):
|
||||
quote_columns = quote_config
|
||||
elif quote_config is None:
|
||||
deprecations.warn('column-quoting-unset')
|
||||
pass
|
||||
else:
|
||||
raise_compiler_error(
|
||||
f'The seed configuration value of "quote_columns" has an '
|
||||
@@ -907,13 +905,17 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
|
||||
@available
|
||||
@classmethod
|
||||
def convert_type(cls, agate_table, col_idx):
|
||||
def convert_type(
|
||||
cls, agate_table: agate.Table, col_idx: int
|
||||
) -> Optional[str]:
|
||||
return cls.convert_agate_type(agate_table, col_idx)
|
||||
|
||||
@classmethod
|
||||
def convert_agate_type(cls, agate_table, col_idx):
|
||||
agate_type = agate_table.column_types[col_idx]
|
||||
conversions = [
|
||||
def convert_agate_type(
|
||||
cls, agate_table: agate.Table, col_idx: int
|
||||
) -> Optional[str]:
|
||||
agate_type: Type = agate_table.column_types[col_idx]
|
||||
conversions: List[Tuple[Type, Callable[..., str]]] = [
|
||||
(agate.Text, cls.convert_text_type),
|
||||
(agate.Number, cls.convert_number_type),
|
||||
(agate.Boolean, cls.convert_boolean_type),
|
||||
@@ -925,6 +927,8 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
if isinstance(agate_type, agate_cls):
|
||||
return func(agate_table, col_idx)
|
||||
|
||||
return None
|
||||
|
||||
###
|
||||
# Operations involving the manifest
|
||||
###
|
||||
@@ -935,7 +939,7 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
project: Optional[str] = None,
|
||||
context_override: Optional[Dict[str, Any]] = None,
|
||||
kwargs: Dict[str, Any] = None,
|
||||
release: bool = False,
|
||||
text_only_columns: Optional[Iterable[str]] = None,
|
||||
) -> agate.Table:
|
||||
"""Look macro_name up in the manifest and execute its results.
|
||||
|
||||
@@ -948,17 +952,19 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
execution context.
|
||||
:param kwargs: An optional dict of keyword args used to pass to the
|
||||
macro.
|
||||
:param release: If True, release the connection after executing.
|
||||
"""
|
||||
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
if context_override is None:
|
||||
context_override = {}
|
||||
|
||||
if manifest is None:
|
||||
manifest = self._internal_manifest
|
||||
manifest = self._macro_manifest
|
||||
|
||||
macro = manifest.find_macro_by_name(macro_name, project)
|
||||
macro = manifest.find_macro_by_name(
|
||||
macro_name, self.config.project_name, project
|
||||
)
|
||||
if macro is None:
|
||||
if project is None:
|
||||
package_name = 'any package'
|
||||
@@ -969,23 +975,21 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
'dbt could not find a macro with the name "{}" in {}'
|
||||
.format(macro_name, package_name)
|
||||
)
|
||||
# This causes a reference cycle, as dbt.context.runtime.generate()
|
||||
# This causes a reference cycle, as generate_runtime_macro_context()
|
||||
# ends up calling get_adapter, so the import has to be here.
|
||||
import dbt.context.operation
|
||||
macro_context = dbt.context.operation.generate(
|
||||
macro,
|
||||
self.config,
|
||||
manifest
|
||||
from dbt.context.providers import generate_runtime_macro_context
|
||||
macro_context = generate_runtime_macro_context(
|
||||
macro=macro,
|
||||
config=self.config,
|
||||
manifest=manifest,
|
||||
package_name=project
|
||||
)
|
||||
macro_context.update(context_override)
|
||||
|
||||
macro_function = macro.generator(macro_context)
|
||||
macro_function = MacroGenerator(macro, macro_context)
|
||||
|
||||
try:
|
||||
with self.connections.exception_handler(f'macro {macro_name}'):
|
||||
result = macro_function(**kwargs)
|
||||
finally:
|
||||
if release:
|
||||
self.release_connection()
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
@@ -995,27 +999,61 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
"""Filter the table as appropriate for catalog entries. Subclasses can
|
||||
override this to change filtering rules on a per-adapter basis.
|
||||
"""
|
||||
# force database + schema to be strings
|
||||
table = table_from_rows(
|
||||
table.rows,
|
||||
table.column_names,
|
||||
text_only_columns=['table_database', 'table_schema', 'table_name']
|
||||
)
|
||||
return table.where(_catalog_filter_schemas(manifest))
|
||||
|
||||
def _get_catalog_information_schemas(
|
||||
self, manifest: Manifest
|
||||
) -> List[InformationSchema]:
|
||||
return list(self._get_cache_schemas(manifest).keys())
|
||||
def _get_one_catalog(
|
||||
self,
|
||||
information_schema: InformationSchema,
|
||||
schemas: Set[str],
|
||||
manifest: Manifest,
|
||||
) -> agate.Table:
|
||||
|
||||
def get_catalog(self, manifest: Manifest) -> agate.Table:
|
||||
"""Get the catalog for this manifest by running the get catalog macro.
|
||||
Returns an agate.Table of catalog information.
|
||||
"""
|
||||
information_schemas = self._get_catalog_information_schemas(manifest)
|
||||
# make it a list so macros can index into it.
|
||||
kwargs = {'information_schemas': information_schemas}
|
||||
table = self.execute_macro(GET_CATALOG_MACRO_NAME,
|
||||
kwargs=kwargs,
|
||||
release=True)
|
||||
kwargs = {
|
||||
'information_schema': information_schema,
|
||||
'schemas': schemas
|
||||
}
|
||||
table = self.execute_macro(
|
||||
GET_CATALOG_MACRO_NAME,
|
||||
kwargs=kwargs,
|
||||
# pass in the full manifest so we get any local project
|
||||
# overrides
|
||||
manifest=manifest,
|
||||
)
|
||||
|
||||
results = self._catalog_filter_table(table, manifest)
|
||||
return results
|
||||
|
||||
def get_catalog(
|
||||
self, manifest: Manifest
|
||||
) -> Tuple[agate.Table, List[Exception]]:
|
||||
schema_map = self._get_catalog_schemas(manifest)
|
||||
|
||||
with executor(self.config) as tpe:
|
||||
futures: List[Future[agate.Table]] = []
|
||||
for info, schemas in schema_map.items():
|
||||
if len(schemas) == 0:
|
||||
continue
|
||||
name = '.'.join([
|
||||
str(info.database),
|
||||
'information_schema'
|
||||
])
|
||||
|
||||
fut = tpe.submit_connected(
|
||||
self, name,
|
||||
self._get_one_catalog, info, schemas, manifest
|
||||
)
|
||||
futures.append(fut)
|
||||
|
||||
catalogs, exceptions = catch_as_completed(futures)
|
||||
|
||||
return catalogs, exceptions
|
||||
|
||||
def cancel_open_connections(self):
|
||||
"""Cancel all open connections."""
|
||||
return self.connections.cancel_open()
|
||||
@@ -1038,7 +1076,6 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
table = self.execute_macro(
|
||||
FRESHNESS_MACRO_NAME,
|
||||
kwargs=kwargs,
|
||||
release=True,
|
||||
manifest=manifest
|
||||
)
|
||||
# now we have a 1-row table of the maximum `loaded_at_field` value and
|
||||
@@ -1088,3 +1125,127 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
The second parameter is the value returned by pre_mdoel_hook.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_compiler(self):
|
||||
from dbt.compilation import Compiler
|
||||
return Compiler(self.config)
|
||||
|
||||
# Methods used in adapter tests
|
||||
def update_column_sql(
|
||||
self,
|
||||
dst_name: str,
|
||||
dst_column: str,
|
||||
clause: str,
|
||||
where_clause: Optional[str] = None,
|
||||
) -> str:
|
||||
clause = f'update {dst_name} set {dst_column} = {clause}'
|
||||
if where_clause is not None:
|
||||
clause += f' where {where_clause}'
|
||||
return clause
|
||||
|
||||
def timestamp_add_sql(
|
||||
self, add_to: str, number: int = 1, interval: str = 'hour'
|
||||
) -> str:
|
||||
# for backwards compatibility, we're compelled to set some sort of
|
||||
# default. A lot of searching has lead me to believe that the
|
||||
# '+ interval' syntax used in postgres/redshift is relatively common
|
||||
# and might even be the SQL standard's intention.
|
||||
return f"{add_to} + interval '{number} {interval}'"
|
||||
|
||||
def string_add_sql(
|
||||
self, add_to: str, value: str, location='append',
|
||||
) -> str:
|
||||
if location == 'append':
|
||||
return f"{add_to} || '{value}'"
|
||||
elif location == 'prepend':
|
||||
return f"'{value}' || {add_to}"
|
||||
else:
|
||||
raise RuntimeException(
|
||||
f'Got an unexpected location value of "{location}"'
|
||||
)
|
||||
|
||||
def get_rows_different_sql(
|
||||
self,
|
||||
relation_a: BaseRelation,
|
||||
relation_b: BaseRelation,
|
||||
column_names: Optional[List[str]] = None,
|
||||
except_operator: str = 'EXCEPT',
|
||||
) -> str:
|
||||
"""Generate SQL for a query that returns a single row with a two
|
||||
columns: the number of rows that are different between the two
|
||||
relations and the number of mismatched rows.
|
||||
"""
|
||||
# This method only really exists for test reasons.
|
||||
names: List[str]
|
||||
if column_names is None:
|
||||
columns = self.get_columns_in_relation(relation_a)
|
||||
names = sorted((self.quote(c.name) for c in columns))
|
||||
else:
|
||||
names = sorted((self.quote(n) for n in column_names))
|
||||
columns_csv = ', '.join(names)
|
||||
|
||||
sql = COLUMNS_EQUAL_SQL.format(
|
||||
columns=columns_csv,
|
||||
relation_a=str(relation_a),
|
||||
relation_b=str(relation_b),
|
||||
except_op=except_operator,
|
||||
)
|
||||
|
||||
return sql
|
||||
|
||||
|
||||
COLUMNS_EQUAL_SQL = '''
|
||||
with diff_count as (
|
||||
SELECT
|
||||
1 as id,
|
||||
COUNT(*) as num_missing FROM (
|
||||
(SELECT {columns} FROM {relation_a} {except_op}
|
||||
SELECT {columns} FROM {relation_b})
|
||||
UNION ALL
|
||||
(SELECT {columns} FROM {relation_b} {except_op}
|
||||
SELECT {columns} FROM {relation_a})
|
||||
) as a
|
||||
), table_a as (
|
||||
SELECT COUNT(*) as num_rows FROM {relation_a}
|
||||
), table_b as (
|
||||
SELECT COUNT(*) as num_rows FROM {relation_b}
|
||||
), row_count_diff as (
|
||||
select
|
||||
1 as id,
|
||||
table_a.num_rows - table_b.num_rows as difference
|
||||
from table_a, table_b
|
||||
)
|
||||
select
|
||||
row_count_diff.difference as row_count_difference,
|
||||
diff_count.num_missing as num_mismatched
|
||||
from row_count_diff
|
||||
join diff_count using (id)
|
||||
'''.strip()
|
||||
|
||||
|
||||
def catch_as_completed(
|
||||
futures # typing: List[Future[agate.Table]]
|
||||
) -> Tuple[agate.Table, List[Exception]]:
|
||||
|
||||
# catalogs: agate.Table = agate.Table(rows=[])
|
||||
tables: List[agate.Table] = []
|
||||
exceptions: List[Exception] = []
|
||||
|
||||
for future in as_completed(futures):
|
||||
exc = future.exception()
|
||||
# we want to re-raise on ctrl+c and BaseException
|
||||
if exc is None:
|
||||
catalog = future.result()
|
||||
tables.append(catalog)
|
||||
elif (
|
||||
isinstance(exc, KeyboardInterrupt) or
|
||||
not isinstance(exc, Exception)
|
||||
):
|
||||
raise exc
|
||||
else:
|
||||
warn_or_error(
|
||||
f'Encountered an error while generating catalog: {str(exc)}'
|
||||
)
|
||||
# exc is not None, derives from Exception, and isn't ctrl+c
|
||||
exceptions.append(exc)
|
||||
return merge_tables(tables), exceptions
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import abc
|
||||
from functools import wraps
|
||||
from typing import Callable, Optional, Any, FrozenSet, Dict
|
||||
from typing import Callable, Optional, Any, FrozenSet, Dict, Set
|
||||
|
||||
from dbt.deprecations import warn, renamed_method
|
||||
|
||||
@@ -86,16 +86,26 @@ available = _Available()
|
||||
|
||||
|
||||
class AdapterMeta(abc.ABCMeta):
|
||||
_available_: FrozenSet[str]
|
||||
_parse_replacements_: Dict[str, Callable]
|
||||
|
||||
def __new__(mcls, name, bases, namespace, **kwargs):
|
||||
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
|
||||
# mypy does not like the `**kwargs`. But `ABCMeta` itself takes
|
||||
# `**kwargs` in its argspec here (and passes them to `type.__new__`.
|
||||
# I'm not sure there is any benefit to it after poking around a bit,
|
||||
# but having it doesn't hurt on the python side (and omitting it could
|
||||
# hurt for obscure metaclass reasons, for all I know)
|
||||
cls = abc.ABCMeta.__new__( # type: ignore
|
||||
mcls, name, bases, namespace, **kwargs
|
||||
)
|
||||
|
||||
# this is very much inspired by ABCMeta's own implementation
|
||||
|
||||
# dict mapping the method name to whether the model name should be
|
||||
# injected into the arguments. All methods in here are exposed to the
|
||||
# context.
|
||||
available = set()
|
||||
replacements = {}
|
||||
available: Set[str] = set()
|
||||
replacements: Dict[str, Any] = {}
|
||||
|
||||
# collect base class data first
|
||||
for base in bases:
|
||||
@@ -110,7 +120,7 @@ class AdapterMeta(abc.ABCMeta):
|
||||
if parse_replacement is not None:
|
||||
replacements[name] = parse_replacement
|
||||
|
||||
cls._available_: FrozenSet[str] = frozenset(available)
|
||||
cls._available_ = frozenset(available)
|
||||
# should this be a namedtuple so it will be immutable like _available_?
|
||||
cls._parse_replacements_: Dict[str, Callable] = replacements
|
||||
cls._parse_replacements_ = replacements
|
||||
return cls
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
from typing import List, Optional, Type
|
||||
|
||||
from dbt.adapters.base import BaseAdapter, Credentials
|
||||
from dbt.adapters.base import Credentials
|
||||
from dbt.exceptions import CompilationException
|
||||
from dbt.adapters.protocol import AdapterProtocol
|
||||
|
||||
|
||||
def project_name_from_path(include_path: str) -> str:
|
||||
# avoid an import cycle
|
||||
from dbt.config.project import Project
|
||||
partial = Project.partial_load(include_path)
|
||||
if partial.project_name is None:
|
||||
raise CompilationException(
|
||||
f'Invalid project at {include_path}: name not set!'
|
||||
)
|
||||
return partial.project_name
|
||||
|
||||
|
||||
class AdapterPlugin:
|
||||
@@ -12,19 +25,16 @@ class AdapterPlugin:
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
adapter: Type[BaseAdapter],
|
||||
adapter: Type[AdapterProtocol],
|
||||
credentials: Type[Credentials],
|
||||
include_path: str,
|
||||
dependencies: Optional[List[str]] = None
|
||||
):
|
||||
# avoid an import cycle
|
||||
from dbt.config.project import Project
|
||||
|
||||
self.adapter: Type[BaseAdapter] = adapter
|
||||
self.adapter: Type[AdapterProtocol] = adapter
|
||||
self.credentials: Type[Credentials] = credentials
|
||||
self.include_path: str = include_path
|
||||
project = Project.from_project_root(include_path, {})
|
||||
self.project_name: str = project.project_name
|
||||
self.project_name: str = project_name_from_path(include_path)
|
||||
self.dependencies: List[str]
|
||||
if dependencies is None:
|
||||
self.dependencies = []
|
||||
|
||||
@@ -1,35 +1,13 @@
|
||||
from threading import local
|
||||
from typing import Optional, Callable
|
||||
from typing import Optional, Callable, Dict, Any
|
||||
|
||||
from dbt.clients.jinja import QueryStringGenerator
|
||||
|
||||
# this generates an import cycle, as usual
|
||||
from dbt.context.base import QueryHeaderContext
|
||||
from dbt.contracts.connection import AdapterRequiredConfig
|
||||
from dbt.context.manifest import generate_query_header_context
|
||||
from dbt.contracts.connection import AdapterRequiredConfig, QueryComment
|
||||
from dbt.contracts.graph.compiled import CompileResultNode
|
||||
from dbt.contracts.graph.manifest import Manifest
|
||||
from dbt.exceptions import RuntimeException
|
||||
from dbt.helper_types import NoValue
|
||||
|
||||
|
||||
DEFAULT_QUERY_COMMENT = '''
|
||||
{%- set comment_dict = {} -%}
|
||||
{%- do comment_dict.update(
|
||||
app='dbt',
|
||||
dbt_version=dbt_version,
|
||||
profile_name=target.get('profile_name'),
|
||||
target_name=target.get('target_name'),
|
||||
) -%}
|
||||
{%- if node is not none -%}
|
||||
{%- do comment_dict.update(
|
||||
node_id=node.unique_id,
|
||||
) -%}
|
||||
{% else %}
|
||||
{# in the node context, the connection name is the node_id #}
|
||||
{%- do comment_dict.update(connection_name=connection_name) -%}
|
||||
{%- endif -%}
|
||||
{{ return(tojson(comment_dict)) }}
|
||||
'''
|
||||
|
||||
|
||||
class NodeWrapper:
|
||||
@@ -48,55 +26,62 @@ class _QueryComment(local):
|
||||
"""
|
||||
def __init__(self, initial):
|
||||
self.query_comment: Optional[str] = initial
|
||||
self.append = False
|
||||
|
||||
def add(self, sql: str) -> str:
|
||||
if not self.query_comment:
|
||||
return sql
|
||||
else:
|
||||
return '/* {} */\n{}'.format(self.query_comment.strip(), sql)
|
||||
|
||||
def set(self, comment: Optional[str]):
|
||||
if '*/' in comment:
|
||||
if self.append:
|
||||
# replace last ';' with '<comment>;'
|
||||
sql = sql.rstrip()
|
||||
if sql[-1] == ';':
|
||||
sql = sql[:-1]
|
||||
return '{}\n/* {} */;'.format(sql, self.query_comment.strip())
|
||||
|
||||
return '{}\n/* {} */'.format(sql, self.query_comment.strip())
|
||||
|
||||
return '/* {} */\n{}'.format(self.query_comment.strip(), sql)
|
||||
|
||||
def set(self, comment: Optional[str], append: bool):
|
||||
if isinstance(comment, str) and '*/' in comment:
|
||||
# tell the user "no" so they don't hurt themselves by writing
|
||||
# garbage
|
||||
raise RuntimeException(
|
||||
f'query comment contains illegal value "*/": {comment}'
|
||||
)
|
||||
self.query_comment = comment
|
||||
self.append = append
|
||||
|
||||
|
||||
QueryStringFunc = Callable[[str, Optional[CompileResultNode]], str]
|
||||
QueryStringFunc = Callable[[str, Optional[NodeWrapper]], str]
|
||||
|
||||
|
||||
class QueryStringSetter:
|
||||
"""The base query string setter. This is only used once."""
|
||||
def __init__(self, config: AdapterRequiredConfig):
|
||||
class MacroQueryStringSetter:
|
||||
def __init__(self, config: AdapterRequiredConfig, manifest: Manifest):
|
||||
self.manifest = manifest
|
||||
self.config = config
|
||||
|
||||
comment_macro = self._get_comment_macro()
|
||||
self.generator: QueryStringFunc = lambda name, model: ''
|
||||
# if the comment value was None or the empty string, just skip it
|
||||
if comment_macro:
|
||||
assert isinstance(comment_macro, str)
|
||||
macro = '\n'.join((
|
||||
'{%- macro query_comment_macro(connection_name, node) -%}',
|
||||
self._get_comment_macro(),
|
||||
comment_macro,
|
||||
'{% endmacro %}'
|
||||
))
|
||||
ctx = self._get_context()
|
||||
self.generator: QueryStringFunc = QueryStringGenerator(macro, ctx)
|
||||
self.generator = QueryStringGenerator(macro, ctx)
|
||||
self.comment = _QueryComment(None)
|
||||
self.reset()
|
||||
|
||||
def _get_context(self):
|
||||
return QueryHeaderContext(self.config).to_dict()
|
||||
|
||||
def _get_comment_macro(self) -> Optional[str]:
|
||||
# if the query comment is null/empty string, there is no comment at all
|
||||
if not self.config.query_comment:
|
||||
return None
|
||||
else:
|
||||
# else, the default
|
||||
return DEFAULT_QUERY_COMMENT
|
||||
return self.config.query_comment.comment
|
||||
|
||||
def _get_context(self) -> Dict[str, Any]:
|
||||
return generate_query_header_context(self.config, self.manifest)
|
||||
|
||||
def add(self, sql: str) -> str:
|
||||
return self.comment.add(sql)
|
||||
@@ -105,27 +90,12 @@ class QueryStringSetter:
|
||||
self.set('master', None)
|
||||
|
||||
def set(self, name: str, node: Optional[CompileResultNode]):
|
||||
wrapped: Optional[NodeWrapper] = None
|
||||
if node is not None:
|
||||
wrapped = NodeWrapper(node)
|
||||
else:
|
||||
wrapped = None
|
||||
comment_str = self.generator(name, wrapped)
|
||||
self.comment.set(comment_str)
|
||||
|
||||
|
||||
class MacroQueryStringSetter(QueryStringSetter):
|
||||
def __init__(self, config: AdapterRequiredConfig, manifest: Manifest):
|
||||
self.manifest = manifest
|
||||
super().__init__(config)
|
||||
|
||||
def _get_comment_macro(self):
|
||||
if (
|
||||
self.config.query_comment != NoValue() and
|
||||
self.config.query_comment
|
||||
):
|
||||
return self.config.query_comment
|
||||
else:
|
||||
return super()._get_comment_macro()
|
||||
|
||||
def _get_context(self):
|
||||
return QueryHeaderContext(self.config).to_dict(self.manifest.macros)
|
||||
append = False
|
||||
if isinstance(self.config.query_comment, QueryComment):
|
||||
append = self.config.query_comment.append
|
||||
self.comment.set(comment_str, append)
|
||||
|
||||
@@ -1,131 +1,19 @@
|
||||
from dbt.utils import filter_null_values, deep_merge, classproperty
|
||||
from dbt.node_types import NodeType
|
||||
|
||||
import dbt.exceptions
|
||||
|
||||
from collections.abc import Mapping, Hashable
|
||||
from dataclasses import dataclass, fields
|
||||
from collections.abc import Hashable
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
Optional, TypeVar, Generic, Any, Type, Dict, Union, List, Iterator, Tuple
|
||||
Optional, TypeVar, Any, Type, Dict, Union, Iterator, Tuple, Set
|
||||
)
|
||||
from typing_extensions import Protocol
|
||||
|
||||
from hologram import JsonSchemaMixin
|
||||
from hologram.helpers import StrEnum
|
||||
|
||||
from dbt.contracts.util import Replaceable
|
||||
from dbt.contracts.graph.compiled import CompiledNode
|
||||
from dbt.contracts.graph.parsed import ParsedSourceDefinition, ParsedNode
|
||||
from dbt.contracts.relation import (
|
||||
RelationType, ComponentName, HasQuoting, FakeAPIObject, Policy, Path
|
||||
)
|
||||
from dbt.exceptions import InternalException
|
||||
from dbt import deprecations
|
||||
from dbt.node_types import NodeType
|
||||
from dbt.utils import filter_null_values, deep_merge, classproperty
|
||||
|
||||
|
||||
class RelationType(StrEnum):
|
||||
Table = 'table'
|
||||
View = 'view'
|
||||
CTE = 'cte'
|
||||
MaterializedView = 'materializedview'
|
||||
External = 'external'
|
||||
|
||||
|
||||
class ComponentName(StrEnum):
|
||||
Database = 'database'
|
||||
Schema = 'schema'
|
||||
Identifier = 'identifier'
|
||||
|
||||
|
||||
class HasQuoting(Protocol):
|
||||
quoting: Dict[str, bool]
|
||||
|
||||
|
||||
class FakeAPIObject(JsonSchemaMixin, Replaceable, Mapping):
|
||||
# override the mapping truthiness, len is always >1
|
||||
def __bool__(self):
|
||||
return True
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return getattr(self, key)
|
||||
except AttributeError:
|
||||
raise KeyError(key) from None
|
||||
|
||||
def __iter__(self):
|
||||
deprecations.warn('not-a-dictionary', obj=self)
|
||||
for _, name in self._get_fields():
|
||||
yield name
|
||||
|
||||
def __len__(self):
|
||||
deprecations.warn('not-a-dictionary', obj=self)
|
||||
return len(fields(self.__class__))
|
||||
|
||||
def incorporate(self, **kwargs):
|
||||
value = self.to_dict()
|
||||
value = deep_merge(value, kwargs)
|
||||
return self.from_dict(value)
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
@dataclass
|
||||
class _ComponentObject(FakeAPIObject, Generic[T]):
|
||||
database: T
|
||||
schema: T
|
||||
identifier: T
|
||||
|
||||
def get_part(self, key: ComponentName) -> T:
|
||||
if key == ComponentName.Database:
|
||||
return self.database
|
||||
elif key == ComponentName.Schema:
|
||||
return self.schema
|
||||
elif key == ComponentName.Identifier:
|
||||
return self.identifier
|
||||
else:
|
||||
raise ValueError(
|
||||
'Got a key of {}, expected one of {}'
|
||||
.format(key, list(ComponentName))
|
||||
)
|
||||
|
||||
def replace_dict(self, dct: Dict[ComponentName, T]):
|
||||
kwargs: Dict[str, T] = {}
|
||||
for k, v in dct.items():
|
||||
kwargs[str(k)] = v
|
||||
return self.replace(**kwargs)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Policy(_ComponentObject[bool]):
|
||||
database: bool = True
|
||||
schema: bool = True
|
||||
identifier: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class Path(_ComponentObject[Optional[str]]):
|
||||
database: Optional[str]
|
||||
schema: Optional[str]
|
||||
identifier: Optional[str]
|
||||
|
||||
def __post_init__(self):
|
||||
# handle pesky jinja2.Undefined sneaking in here and messing up render
|
||||
if not isinstance(self.database, (type(None), str)):
|
||||
raise dbt.exceptions.CompilationException(
|
||||
'Got an invalid path database: {}'.format(self.database)
|
||||
)
|
||||
if not isinstance(self.schema, (type(None), str)):
|
||||
raise dbt.exceptions.CompilationException(
|
||||
'Got an invalid path schema: {}'.format(self.schema)
|
||||
)
|
||||
if not isinstance(self.identifier, (type(None), str)):
|
||||
raise dbt.exceptions.CompilationException(
|
||||
'Got an invalid path identifier: {}'.format(self.identifier)
|
||||
)
|
||||
|
||||
def get_lowered_part(self, key: ComponentName) -> Optional[str]:
|
||||
part = self.get_part(key)
|
||||
if part is not None:
|
||||
part = part.lower()
|
||||
return part
|
||||
import dbt.exceptions
|
||||
|
||||
|
||||
Self = TypeVar('Self', bound='BaseRelation')
|
||||
@@ -133,8 +21,8 @@ Self = TypeVar('Self', bound='BaseRelation')
|
||||
|
||||
@dataclass(frozen=True, eq=False, repr=False)
|
||||
class BaseRelation(FakeAPIObject, Hashable):
|
||||
type: Optional[RelationType]
|
||||
path: Path
|
||||
type: Optional[RelationType] = None
|
||||
quote_character: str = '"'
|
||||
include_policy: Policy = Policy()
|
||||
quote_policy: Policy = Policy()
|
||||
@@ -157,12 +45,16 @@ class BaseRelation(FakeAPIObject, Hashable):
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
return self.to_dict() == other.to_dict()
|
||||
return self.to_dict(omit_none=True) == other.to_dict(omit_none=True)
|
||||
|
||||
@classmethod
|
||||
def get_default_quote_policy(cls: Type[Self]) -> Policy:
|
||||
def get_default_quote_policy(cls) -> Policy:
|
||||
return cls._get_field_named('quote_policy').default
|
||||
|
||||
@classmethod
|
||||
def get_default_include_policy(cls) -> Policy:
|
||||
return cls._get_field_named('include_policy').default
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""Override `.get` to return a metadata object so we don't break
|
||||
dbt_utils.
|
||||
@@ -197,7 +89,10 @@ class BaseRelation(FakeAPIObject, Hashable):
|
||||
if not self._is_exactish_match(k, v):
|
||||
exact_match = False
|
||||
|
||||
if self.path.get_lowered_part(k) != v.lower():
|
||||
if (
|
||||
self.path.get_lowered_part(k).strip(self.quote_character) !=
|
||||
v.lower().strip(self.quote_character)
|
||||
):
|
||||
approximate_match = False
|
||||
|
||||
if approximate_match and not exact_match:
|
||||
@@ -254,6 +149,16 @@ class BaseRelation(FakeAPIObject, Hashable):
|
||||
def information_schema_only(self) -> 'InformationSchema':
|
||||
return self.information_schema()
|
||||
|
||||
def without_identifier(self) -> 'BaseRelation':
|
||||
"""Return a form of this relation that only has the database and schema
|
||||
set to included. To get the appropriately-quoted form the schema out of
|
||||
the result (for use as part of a query), use `.render()`. To get the
|
||||
raw database or schema name, use `.database` or `.schema`.
|
||||
|
||||
The hash of the returned object is the result of render().
|
||||
"""
|
||||
return self.include(identifier=False).replace_path(identifier=None)
|
||||
|
||||
def _render_iterator(
|
||||
self
|
||||
) -> Iterator[Tuple[Optional[ComponentName], Optional[str]]]:
|
||||
@@ -267,16 +172,11 @@ class BaseRelation(FakeAPIObject, Hashable):
|
||||
yield key, path_part
|
||||
|
||||
def render(self) -> str:
|
||||
parts: List[str] = [
|
||||
part for _, part in self._render_iterator() if part is not None
|
||||
]
|
||||
|
||||
if len(parts) == 0:
|
||||
raise dbt.exceptions.RuntimeException(
|
||||
"No path parts are included! Nothing to render."
|
||||
)
|
||||
|
||||
return '.'.join(parts)
|
||||
# if there is nothing set, this will return the empty string.
|
||||
return '.'.join(
|
||||
part for _, part in self._render_iterator()
|
||||
if part is not None
|
||||
)
|
||||
|
||||
def quoted(self, identifier):
|
||||
return '{quote_char}{identifier}{quote_char}'.format(
|
||||
@@ -288,9 +188,11 @@ class BaseRelation(FakeAPIObject, Hashable):
|
||||
def create_from_source(
|
||||
cls: Type[Self], source: ParsedSourceDefinition, **kwargs: Any
|
||||
) -> Self:
|
||||
source_quoting = source.quoting.to_dict(omit_none=True)
|
||||
source_quoting.pop('column', None)
|
||||
quote_policy = deep_merge(
|
||||
cls.get_default_quote_policy().to_dict(),
|
||||
source.quoting.to_dict(),
|
||||
cls.get_default_quote_policy().to_dict(omit_none=True),
|
||||
source_quoting,
|
||||
kwargs.get('quote_policy', {}),
|
||||
)
|
||||
|
||||
@@ -302,6 +204,23 @@ class BaseRelation(FakeAPIObject, Hashable):
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_ephemeral_prefix(name: str):
|
||||
return f'__dbt__cte__{name}'
|
||||
|
||||
@classmethod
|
||||
def create_ephemeral_from_node(
|
||||
cls: Type[Self],
|
||||
config: HasQuoting,
|
||||
node: Union[ParsedNode, CompiledNode],
|
||||
) -> Self:
|
||||
# Note that ephemeral models are based on the name.
|
||||
identifier = cls.add_ephemeral_prefix(node.name)
|
||||
return cls.create(
|
||||
type=cls.CTE,
|
||||
identifier=identifier,
|
||||
).quote(identifier=False)
|
||||
|
||||
@classmethod
|
||||
def create_from_node(
|
||||
cls: Type[Self],
|
||||
@@ -422,10 +341,13 @@ class BaseRelation(FakeAPIObject, Hashable):
|
||||
return str(RelationType.External)
|
||||
|
||||
@classproperty
|
||||
def RelationType(cls) -> Type[RelationType]:
|
||||
def get_relation_type(cls) -> Type[RelationType]:
|
||||
return RelationType
|
||||
|
||||
|
||||
Info = TypeVar('Info', bound='InformationSchema')
|
||||
|
||||
|
||||
@dataclass(frozen=True, eq=False, repr=False)
|
||||
class InformationSchema(BaseRelation):
|
||||
information_schema_view: Optional[str] = None
|
||||
@@ -470,10 +392,10 @@ class InformationSchema(BaseRelation):
|
||||
|
||||
@classmethod
|
||||
def from_relation(
|
||||
cls: Self,
|
||||
cls: Type[Info],
|
||||
relation: BaseRelation,
|
||||
information_schema_view: Optional[str],
|
||||
) -> Self:
|
||||
) -> Info:
|
||||
include_policy = cls.get_include_policy(
|
||||
relation, information_schema_view
|
||||
)
|
||||
@@ -491,3 +413,47 @@ class InformationSchema(BaseRelation):
|
||||
for k, v in super()._render_iterator():
|
||||
yield k, v
|
||||
yield None, self.information_schema_view
|
||||
|
||||
|
||||
class SchemaSearchMap(Dict[InformationSchema, Set[Optional[str]]]):
|
||||
"""A utility class to keep track of what information_schema tables to
|
||||
search for what schemas. The schema values are all lowercased to avoid
|
||||
duplication.
|
||||
"""
|
||||
def add(self, relation: BaseRelation):
|
||||
key = relation.information_schema_only()
|
||||
if key not in self:
|
||||
self[key] = set()
|
||||
schema: Optional[str] = None
|
||||
if relation.schema is not None:
|
||||
schema = relation.schema.lower()
|
||||
self[key].add(schema)
|
||||
|
||||
def search(
|
||||
self
|
||||
) -> Iterator[Tuple[InformationSchema, Optional[str]]]:
|
||||
for information_schema_name, schemas in self.items():
|
||||
for schema in schemas:
|
||||
yield information_schema_name, schema
|
||||
|
||||
def flatten(self, allow_multiple_databases: bool = False):
|
||||
new = self.__class__()
|
||||
|
||||
# make sure we don't have multiple databases if allow_multiple_databases is set to False
|
||||
if not allow_multiple_databases:
|
||||
seen = {r.database.lower() for r in self if r.database}
|
||||
if len(seen) > 1:
|
||||
dbt.exceptions.raise_compiler_error(str(seen))
|
||||
|
||||
for information_schema_name, schema in self.search():
|
||||
path = {
|
||||
'database': information_schema_name.database,
|
||||
'schema': schema
|
||||
}
|
||||
new.add(information_schema_name.incorporate(
|
||||
path=path,
|
||||
quote_policy={'database': False},
|
||||
include_policy={'database': False},
|
||||
))
|
||||
|
||||
return new
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
from collections import namedtuple
|
||||
from copy import deepcopy
|
||||
from typing import List, Iterable, Optional
|
||||
import threading
|
||||
from copy import deepcopy
|
||||
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
|
||||
|
||||
from dbt.logger import CACHE_LOGGER as logger
|
||||
from dbt.adapters.reference_keys import _make_key, _ReferenceKey
|
||||
import dbt.exceptions
|
||||
|
||||
_ReferenceKey = namedtuple('_ReferenceKey', 'database schema identifier')
|
||||
|
||||
|
||||
def _lower(value: Optional[str]) -> Optional[str]:
|
||||
"""Postgres schemas can be None so we can't just call lower()."""
|
||||
if value is None:
|
||||
return None
|
||||
return value.lower()
|
||||
|
||||
|
||||
def _make_key(relation) -> _ReferenceKey:
|
||||
"""Make _ReferenceKeys with lowercase values for the cache so we don't have
|
||||
to keep track of quoting
|
||||
"""
|
||||
return _ReferenceKey(_lower(relation.database),
|
||||
_lower(relation.schema),
|
||||
_lower(relation.identifier))
|
||||
from dbt.events.functions import fire_event
|
||||
from dbt.events.types import (
|
||||
AddLink,
|
||||
AddRelation,
|
||||
DropCascade,
|
||||
DropMissingRelation,
|
||||
DropRelation,
|
||||
DumpAfterAddGraph,
|
||||
DumpAfterRenameSchema,
|
||||
DumpBeforeAddGraph,
|
||||
DumpBeforeRenameSchema,
|
||||
RenameSchema,
|
||||
TemporaryRelation,
|
||||
UncachedRelation,
|
||||
UpdateReference
|
||||
)
|
||||
from dbt.utils import lowercase
|
||||
from dbt.helper_types import Lazy
|
||||
|
||||
|
||||
def dot_separated(key: _ReferenceKey) -> str:
|
||||
@@ -53,15 +52,15 @@ class _CachedRelation:
|
||||
|
||||
@property
|
||||
def database(self) -> Optional[str]:
|
||||
return _lower(self.inner.database)
|
||||
return lowercase(self.inner.database)
|
||||
|
||||
@property
|
||||
def schema(self) -> Optional[str]:
|
||||
return _lower(self.inner.schema)
|
||||
return lowercase(self.inner.schema)
|
||||
|
||||
@property
|
||||
def identifier(self) -> Optional[str]:
|
||||
return _lower(self.inner.identifier)
|
||||
return lowercase(self.inner.identifier)
|
||||
|
||||
def __copy__(self):
|
||||
new = self.__class__(self.inner)
|
||||
@@ -162,12 +161,6 @@ class _CachedRelation:
|
||||
return [dot_separated(r) for r in self.referenced_by]
|
||||
|
||||
|
||||
def lazy_log(msg, func):
|
||||
if logger.disabled:
|
||||
return
|
||||
logger.debug(msg.format(func()))
|
||||
|
||||
|
||||
class RelationsCache:
|
||||
"""A cache of the relations known to dbt. Keeps track of relationships
|
||||
declared between tables and handles renames/drops as a real database would.
|
||||
@@ -177,25 +170,29 @@ class RelationsCache:
|
||||
The adapters also hold this lock while filling the cache.
|
||||
:attr Set[str] schemas: The set of known/cached schemas, all lowercased.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.relations = {}
|
||||
def __init__(self) -> None:
|
||||
self.relations: Dict[_ReferenceKey, _CachedRelation] = {}
|
||||
self.lock = threading.RLock()
|
||||
self.schemas = set()
|
||||
self.schemas: Set[Tuple[Optional[str], Optional[str]]] = set()
|
||||
|
||||
def add_schema(self, database: str, schema: str):
|
||||
def add_schema(
|
||||
self, database: Optional[str], schema: Optional[str],
|
||||
) -> None:
|
||||
"""Add a schema to the set of known schemas (case-insensitive)
|
||||
|
||||
:param database: The database name to add.
|
||||
:param schema: The schema name to add.
|
||||
"""
|
||||
self.schemas.add((_lower(database), _lower(schema)))
|
||||
self.schemas.add((lowercase(database), lowercase(schema)))
|
||||
|
||||
def drop_schema(self, database: str, schema: str):
|
||||
def drop_schema(
|
||||
self, database: Optional[str], schema: Optional[str],
|
||||
) -> None:
|
||||
"""Drop the given schema and remove it from the set of known schemas.
|
||||
|
||||
Then remove all its contents (and their dependents, etc) as well.
|
||||
"""
|
||||
key = (_lower(database), _lower(schema))
|
||||
key = (lowercase(database), lowercase(schema))
|
||||
if key not in self.schemas:
|
||||
return
|
||||
|
||||
@@ -208,21 +205,21 @@ class RelationsCache:
|
||||
# handle a drop_schema race by using discard() over remove()
|
||||
self.schemas.discard(key)
|
||||
|
||||
def update_schemas(self, schemas: Iterable[str]):
|
||||
def update_schemas(self, schemas: Iterable[Tuple[Optional[str], str]]):
|
||||
"""Add multiple schemas to the set of known schemas (case-insensitive)
|
||||
|
||||
:param schemas: An iterable of the schema names to add.
|
||||
"""
|
||||
self.schemas.update((_lower(d), _lower(s)) for (d, s) in schemas)
|
||||
self.schemas.update((lowercase(d), s.lower()) for (d, s) in schemas)
|
||||
|
||||
def __contains__(self, schema_id):
|
||||
def __contains__(self, schema_id: Tuple[Optional[str], str]):
|
||||
"""A schema is 'in' the relations cache if it is in the set of cached
|
||||
schemas.
|
||||
|
||||
:param Tuple[str, str] schema: The db name and schema name to look up.
|
||||
:param schema_id: The db name and schema name to look up.
|
||||
"""
|
||||
db, schema = schema_id
|
||||
return (_lower(db), _lower(schema)) in self.schemas
|
||||
return (lowercase(db), schema.lower()) in self.schemas
|
||||
|
||||
def dump_graph(self):
|
||||
"""Dump a key-only representation of the schema to a dictionary. Every
|
||||
@@ -238,7 +235,7 @@ class RelationsCache:
|
||||
for k, v in self.relations.items()
|
||||
}
|
||||
|
||||
def _setdefault(self, relation):
|
||||
def _setdefault(self, relation: _CachedRelation):
|
||||
"""Add a relation to the cache, or return it if it already exists.
|
||||
|
||||
:param _CachedRelation relation: The relation to set or get.
|
||||
@@ -275,8 +272,11 @@ class RelationsCache:
|
||||
.format(dependent_key)
|
||||
)
|
||||
|
||||
assert dependent is not None # we just raised!
|
||||
|
||||
referenced.add_reference(dependent)
|
||||
|
||||
# TODO: Is this dead code? I can't seem to find it grepping the codebase.
|
||||
def add_link(self, referenced, dependent):
|
||||
"""Add a link between two relations to the database. If either relation
|
||||
does not exist, it will be added as an "external" relation.
|
||||
@@ -292,33 +292,26 @@ class RelationsCache:
|
||||
:raises InternalError: If either entry does not exist.
|
||||
"""
|
||||
ref_key = _make_key(referenced)
|
||||
dep_key = _make_key(dependent)
|
||||
if (ref_key.database, ref_key.schema) not in self:
|
||||
# if we have not cached the referenced schema at all, we must be
|
||||
# referring to a table outside our control. There's no need to make
|
||||
# a link - we will never drop the referenced relation during a run.
|
||||
logger.debug(
|
||||
'{dep!s} references {ref!s} but {ref.database}.{ref.schema} '
|
||||
'is not in the cache, skipping assumed external relation'
|
||||
.format(dep=dependent, ref=ref_key)
|
||||
)
|
||||
fire_event(UncachedRelation(dep_key=dep_key, ref_key=ref_key))
|
||||
return
|
||||
if ref_key not in self.relations:
|
||||
# Insert a dummy "external" relation.
|
||||
referenced = referenced.replace(
|
||||
type=referenced.RelationType.External
|
||||
type=referenced.External
|
||||
)
|
||||
self.add(referenced)
|
||||
|
||||
dep_key = _make_key(dependent)
|
||||
if dep_key not in self.relations:
|
||||
# Insert a dummy "external" relation.
|
||||
dependent = dependent.replace(
|
||||
type=referenced.RelationType.External
|
||||
type=referenced.External
|
||||
)
|
||||
self.add(dependent)
|
||||
logger.debug(
|
||||
'adding link, {!s} references {!s}'.format(dep_key, ref_key)
|
||||
)
|
||||
fire_event(AddLink(dep_key=dep_key, ref_key=ref_key))
|
||||
with self.lock:
|
||||
self._add_link(ref_key, dep_key)
|
||||
|
||||
@@ -329,14 +322,12 @@ class RelationsCache:
|
||||
:param BaseRelation relation: The underlying relation.
|
||||
"""
|
||||
cached = _CachedRelation(relation)
|
||||
logger.debug('Adding relation: {!s}'.format(cached))
|
||||
|
||||
lazy_log('before adding: {!s}', self.dump_graph)
|
||||
fire_event(AddRelation(relation=_make_key(cached)))
|
||||
fire_event(DumpBeforeAddGraph(dump=Lazy.defer(lambda: self.dump_graph())))
|
||||
|
||||
with self.lock:
|
||||
self._setdefault(cached)
|
||||
|
||||
lazy_log('after adding: {!s}', self.dump_graph)
|
||||
fire_event(DumpAfterAddGraph(dump=Lazy.defer(lambda: self.dump_graph())))
|
||||
|
||||
def _remove_refs(self, keys):
|
||||
"""Removes all references to all entries in keys. This does not
|
||||
@@ -351,20 +342,17 @@ class RelationsCache:
|
||||
for cached in self.relations.values():
|
||||
cached.release_references(keys)
|
||||
|
||||
def _drop_cascade_relation(self, dropped):
|
||||
def _drop_cascade_relation(self, dropped_key):
|
||||
"""Drop the given relation and cascade it appropriately to all
|
||||
dependent relations.
|
||||
|
||||
:param _CachedRelation dropped: An existing _CachedRelation to drop.
|
||||
"""
|
||||
if dropped not in self.relations:
|
||||
logger.debug('dropped a nonexistent relationship: {!s}'
|
||||
.format(dropped))
|
||||
if dropped_key not in self.relations:
|
||||
fire_event(DropMissingRelation(relation=dropped_key))
|
||||
return
|
||||
consequences = self.relations[dropped].collect_consequences()
|
||||
logger.debug(
|
||||
'drop {} is cascading to {}'.format(dropped, consequences)
|
||||
)
|
||||
consequences = self.relations[dropped_key].collect_consequences()
|
||||
fire_event(DropCascade(dropped=dropped_key, consequences=consequences))
|
||||
self._remove_refs(consequences)
|
||||
|
||||
def drop(self, relation):
|
||||
@@ -378,10 +366,10 @@ class RelationsCache:
|
||||
:param str schema: The schema of the relation to drop.
|
||||
:param str identifier: The identifier of the relation to drop.
|
||||
"""
|
||||
dropped = _make_key(relation)
|
||||
logger.debug('Dropping relation: {!s}'.format(dropped))
|
||||
dropped_key = _make_key(relation)
|
||||
fire_event(DropRelation(dropped=dropped_key))
|
||||
with self.lock:
|
||||
self._drop_cascade_relation(dropped)
|
||||
self._drop_cascade_relation(dropped_key)
|
||||
|
||||
def _rename_relation(self, old_key, new_relation):
|
||||
"""Rename a relation named old_key to new_key, updating references.
|
||||
@@ -402,9 +390,8 @@ class RelationsCache:
|
||||
# update all the relations that refer to it
|
||||
for cached in self.relations.values():
|
||||
if cached.is_referenced_by(old_key):
|
||||
logger.debug(
|
||||
'updated reference from {0} -> {2} to {1} -> {2}'
|
||||
.format(old_key, new_key, cached.key())
|
||||
fire_event(
|
||||
UpdateReference(old_key=old_key, new_key=new_key, cached_key=cached.key())
|
||||
)
|
||||
cached.rename_key(old_key, new_key)
|
||||
|
||||
@@ -434,10 +421,7 @@ class RelationsCache:
|
||||
)
|
||||
|
||||
if old_key not in self.relations:
|
||||
logger.debug(
|
||||
'old key {} not found in self.relations, assuming temporary'
|
||||
.format(old_key)
|
||||
)
|
||||
fire_event(TemporaryRelation(key=old_key))
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -455,11 +439,9 @@ class RelationsCache:
|
||||
"""
|
||||
old_key = _make_key(old)
|
||||
new_key = _make_key(new)
|
||||
logger.debug('Renaming relation {!s} to {!s}'.format(
|
||||
old_key, new_key
|
||||
))
|
||||
fire_event(RenameSchema(old_key=old_key, new_key=new_key))
|
||||
|
||||
lazy_log('before rename: {!s}', self.dump_graph)
|
||||
fire_event(DumpBeforeRenameSchema(dump=Lazy.defer(lambda: self.dump_graph())))
|
||||
|
||||
with self.lock:
|
||||
if self._check_rename_constraints(old_key, new_key):
|
||||
@@ -467,22 +449,24 @@ class RelationsCache:
|
||||
else:
|
||||
self._setdefault(_CachedRelation(new))
|
||||
|
||||
lazy_log('after rename: {!s}', self.dump_graph)
|
||||
fire_event(DumpAfterRenameSchema(dump=Lazy.defer(lambda: self.dump_graph())))
|
||||
|
||||
def get_relations(self, database, schema):
|
||||
def get_relations(
|
||||
self, database: Optional[str], schema: Optional[str]
|
||||
) -> List[Any]:
|
||||
"""Case-insensitively yield all relations matching the given schema.
|
||||
|
||||
:param str schema: The case-insensitive schema name to list from.
|
||||
:return List[BaseRelation]: The list of relations with the given
|
||||
schema
|
||||
"""
|
||||
database = _lower(database)
|
||||
schema = _lower(schema)
|
||||
database = lowercase(database)
|
||||
schema = lowercase(schema)
|
||||
with self.lock:
|
||||
results = [
|
||||
r.inner for r in self.relations.values()
|
||||
if (_lower(r.schema) == schema and
|
||||
_lower(r.database) == database)
|
||||
if (lowercase(r.schema) == schema and
|
||||
lowercase(r.database) == database)
|
||||
]
|
||||
|
||||
if None in results:
|
||||
@@ -498,10 +482,10 @@ class RelationsCache:
|
||||
self.schemas.clear()
|
||||
|
||||
def _list_relations_in_schema(
|
||||
self, database: str, schema: str
|
||||
self, database: Optional[str], schema: Optional[str]
|
||||
) -> List[_CachedRelation]:
|
||||
"""Get the relations in a schema. Callers should hold the lock."""
|
||||
key = (_lower(database), _lower(schema))
|
||||
key = (lowercase(database), lowercase(schema))
|
||||
|
||||
to_remove: List[_CachedRelation] = []
|
||||
for cachekey, relation in self.relations.items():
|
||||
|
||||
@@ -1,61 +1,79 @@
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from importlib import import_module
|
||||
from typing import Type, Dict, Any
|
||||
from typing import Type, Dict, Any, List, Optional, Set
|
||||
|
||||
from dbt.exceptions import RuntimeException
|
||||
from dbt.include.global_project import PACKAGES
|
||||
from dbt.logger import GLOBAL_LOGGER as logger
|
||||
from dbt.exceptions import RuntimeException, InternalException
|
||||
from dbt.include.global_project import (
|
||||
PACKAGE_PATH as GLOBAL_PROJECT_PATH,
|
||||
PROJECT_NAME as GLOBAL_PROJECT_NAME,
|
||||
)
|
||||
from dbt.events.functions import fire_event
|
||||
from dbt.events.types import AdapterImportError, PluginLoadError
|
||||
from dbt.contracts.connection import Credentials, AdapterRequiredConfig
|
||||
|
||||
from dbt.adapters.base.impl import BaseAdapter
|
||||
from dbt.adapters.protocol import (
|
||||
AdapterProtocol,
|
||||
AdapterConfig,
|
||||
RelationProtocol,
|
||||
)
|
||||
from dbt.adapters.base.plugin import AdapterPlugin
|
||||
|
||||
|
||||
# TODO: we can't import these because they cause an import cycle.
|
||||
# Profile has to call into load_plugin to get credentials, so adapter/relation
|
||||
# don't work
|
||||
BaseRelation = Any
|
||||
Adapter = AdapterProtocol
|
||||
|
||||
|
||||
Adapter = BaseAdapter
|
||||
|
||||
|
||||
class AdpaterContainer:
|
||||
class AdapterContainer:
|
||||
def __init__(self):
|
||||
self.lock = threading.Lock()
|
||||
self.adapters: Dict[str, Adapter] = {}
|
||||
self.adapter_types: Dict[str, Type[Adapter]] = {}
|
||||
self.plugins: Dict[str, AdapterPlugin] = {}
|
||||
# map package names to their include paths
|
||||
self.packages: Dict[str, Path] = {
|
||||
GLOBAL_PROJECT_NAME: Path(GLOBAL_PROJECT_PATH),
|
||||
}
|
||||
|
||||
def get_adapter_class_by_name(self, name: str) -> Type[Adapter]:
|
||||
def get_plugin_by_name(self, name: str) -> AdapterPlugin:
|
||||
with self.lock:
|
||||
if name in self.adapter_types:
|
||||
return self.adapter_types[name]
|
||||
|
||||
names = ", ".join(self.adapter_types.keys())
|
||||
if name in self.plugins:
|
||||
return self.plugins[name]
|
||||
names = ", ".join(self.plugins.keys())
|
||||
|
||||
message = f"Invalid adapter type {name}! Must be one of {names}"
|
||||
raise RuntimeException(message)
|
||||
|
||||
def get_relation_class_by_name(self, name: str) -> Type[BaseRelation]:
|
||||
def get_adapter_class_by_name(self, name: str) -> Type[Adapter]:
|
||||
plugin = self.get_plugin_by_name(name)
|
||||
return plugin.adapter
|
||||
|
||||
def get_relation_class_by_name(self, name: str) -> Type[RelationProtocol]:
|
||||
adapter = self.get_adapter_class_by_name(name)
|
||||
return adapter.Relation
|
||||
|
||||
def get_config_class_by_name(
|
||||
self, name: str
|
||||
) -> Type[AdapterConfig]:
|
||||
adapter = self.get_adapter_class_by_name(name)
|
||||
return adapter.AdapterSpecificConfigs
|
||||
|
||||
def load_plugin(self, name: str) -> Type[Credentials]:
|
||||
# this doesn't need a lock: in the worst case we'll overwrite PACKAGES
|
||||
# this doesn't need a lock: in the worst case we'll overwrite packages
|
||||
# and adapter_type entries with the same value, as they're all
|
||||
# singletons
|
||||
try:
|
||||
mod = import_module('.' + name, 'dbt.adapters')
|
||||
except ImportError as e:
|
||||
logger.info("Error importing adapter: {}".format(e))
|
||||
raise RuntimeException(
|
||||
"Could not find adapter type {}!".format(name)
|
||||
)
|
||||
if not hasattr(mod, 'Plugin'):
|
||||
raise RuntimeException(
|
||||
f'Could not find plugin in {name} plugin module'
|
||||
)
|
||||
plugin: AdapterPlugin = mod.Plugin # type: ignore
|
||||
# mypy doesn't think modules have any attributes.
|
||||
mod: Any = import_module('.' + name, 'dbt.adapters')
|
||||
except ModuleNotFoundError as exc:
|
||||
# if we failed to import the target module in particular, inform
|
||||
# the user about it via a runtime error
|
||||
if exc.name == 'dbt.adapters.' + name:
|
||||
fire_event(AdapterImportError(exc=exc))
|
||||
raise RuntimeException(f'Could not find adapter type {name}!')
|
||||
# otherwise, the error had to have come from some underlying
|
||||
# library. Log the stack trace.
|
||||
|
||||
fire_event(PluginLoadError())
|
||||
raise
|
||||
plugin: AdapterPlugin = mod.Plugin
|
||||
plugin_type = plugin.adapter.type()
|
||||
|
||||
if plugin_type != name:
|
||||
@@ -66,9 +84,9 @@ class AdpaterContainer:
|
||||
|
||||
with self.lock:
|
||||
# things do hold the lock to iterate over it so we need it to add
|
||||
self.adapter_types[name] = plugin.adapter
|
||||
self.plugins[name] = plugin
|
||||
|
||||
PACKAGES[plugin.project_name] = plugin.include_path
|
||||
self.packages[plugin.project_name] = Path(plugin.include_path)
|
||||
|
||||
for dep in plugin.dependencies:
|
||||
self.load_plugin(dep)
|
||||
@@ -106,8 +124,58 @@ class AdpaterContainer:
|
||||
for adapter in self.adapters.values():
|
||||
adapter.cleanup_connections()
|
||||
|
||||
def get_adapter_plugins(self, name: Optional[str]) -> List[AdapterPlugin]:
|
||||
"""Iterate over the known adapter plugins. If a name is provided,
|
||||
iterate in dependency order over the named plugin and its dependencies.
|
||||
"""
|
||||
if name is None:
|
||||
return list(self.plugins.values())
|
||||
|
||||
FACTORY: AdpaterContainer = AdpaterContainer()
|
||||
plugins: List[AdapterPlugin] = []
|
||||
seen: Set[str] = set()
|
||||
plugin_names: List[str] = [name]
|
||||
while plugin_names:
|
||||
plugin_name = plugin_names[0]
|
||||
plugin_names = plugin_names[1:]
|
||||
try:
|
||||
plugin = self.plugins[plugin_name]
|
||||
except KeyError:
|
||||
raise InternalException(
|
||||
f'No plugin found for {plugin_name}'
|
||||
) from None
|
||||
plugins.append(plugin)
|
||||
seen.add(plugin_name)
|
||||
if plugin.dependencies is None:
|
||||
continue
|
||||
for dep in plugin.dependencies:
|
||||
if dep not in seen:
|
||||
plugin_names.append(dep)
|
||||
return plugins
|
||||
|
||||
def get_adapter_package_names(self, name: Optional[str]) -> List[str]:
|
||||
package_names: List[str] = [
|
||||
p.project_name for p in self.get_adapter_plugins(name)
|
||||
]
|
||||
package_names.append(GLOBAL_PROJECT_NAME)
|
||||
return package_names
|
||||
|
||||
def get_include_paths(self, name: Optional[str]) -> List[Path]:
|
||||
paths = []
|
||||
for package_name in self.get_adapter_package_names(name):
|
||||
try:
|
||||
path = self.packages[package_name]
|
||||
except KeyError:
|
||||
raise InternalException(
|
||||
f'No internal package listing found for {package_name}'
|
||||
)
|
||||
paths.append(path)
|
||||
return paths
|
||||
|
||||
def get_adapter_type_names(self, name: Optional[str]) -> List[str]:
|
||||
return [p.adapter.type() for p in self.get_adapter_plugins(name)]
|
||||
|
||||
|
||||
FACTORY: AdapterContainer = AdapterContainer()
|
||||
|
||||
|
||||
def register_adapter(config: AdapterRequiredConfig) -> None:
|
||||
@@ -131,13 +199,29 @@ def cleanup_connections():
|
||||
FACTORY.cleanup_connections()
|
||||
|
||||
|
||||
def get_adapter_class_by_name(name: str) -> Type[BaseAdapter]:
|
||||
def get_adapter_class_by_name(name: str) -> Type[AdapterProtocol]:
|
||||
return FACTORY.get_adapter_class_by_name(name)
|
||||
|
||||
|
||||
def get_relation_class_by_name(name: str) -> Type[BaseRelation]:
|
||||
def get_config_class_by_name(name: str) -> Type[AdapterConfig]:
|
||||
return FACTORY.get_config_class_by_name(name)
|
||||
|
||||
|
||||
def get_relation_class_by_name(name: str) -> Type[RelationProtocol]:
|
||||
return FACTORY.get_relation_class_by_name(name)
|
||||
|
||||
|
||||
def load_plugin(name: str) -> Type[Credentials]:
|
||||
return FACTORY.load_plugin(name)
|
||||
|
||||
|
||||
def get_include_paths(name: Optional[str]) -> List[Path]:
|
||||
return FACTORY.get_include_paths(name)
|
||||
|
||||
|
||||
def get_adapter_package_names(name: Optional[str]) -> List[str]:
|
||||
return FACTORY.get_adapter_package_names(name)
|
||||
|
||||
|
||||
def get_adapter_type_names(name: Optional[str]) -> List[str]:
|
||||
return FACTORY.get_adapter_type_names(name)
|
||||
|
||||
163
core/dbt/adapters/protocol.py
Normal file
163
core/dbt/adapters/protocol.py
Normal file
@@ -0,0 +1,163 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
Type, Hashable, Optional, ContextManager, List, Generic, TypeVar, ClassVar,
|
||||
Tuple, Union, Dict, Any
|
||||
)
|
||||
from typing_extensions import Protocol
|
||||
|
||||
import agate
|
||||
|
||||
from dbt.contracts.connection import (
|
||||
Connection, AdapterRequiredConfig, AdapterResponse
|
||||
)
|
||||
from dbt.contracts.graph.compiled import (
|
||||
CompiledNode, ManifestNode, NonSourceCompiledNode
|
||||
)
|
||||
from dbt.contracts.graph.parsed import ParsedNode, ParsedSourceDefinition
|
||||
from dbt.contracts.graph.model_config import BaseConfig
|
||||
from dbt.contracts.graph.manifest import Manifest
|
||||
from dbt.contracts.relation import Policy, HasQuoting
|
||||
|
||||
from dbt.graph import Graph
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdapterConfig(BaseConfig):
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionManagerProtocol(Protocol):
|
||||
TYPE: str
|
||||
|
||||
|
||||
class ColumnProtocol(Protocol):
|
||||
pass
|
||||
|
||||
|
||||
Self = TypeVar('Self', bound='RelationProtocol')
|
||||
|
||||
|
||||
class RelationProtocol(Protocol):
|
||||
@classmethod
|
||||
def get_default_quote_policy(cls) -> Policy:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def create_from(
|
||||
cls: Type[Self],
|
||||
config: HasQuoting,
|
||||
node: Union[CompiledNode, ParsedNode, ParsedSourceDefinition],
|
||||
) -> Self:
|
||||
...
|
||||
|
||||
|
||||
class CompilerProtocol(Protocol):
|
||||
def compile(self, manifest: Manifest, write=True) -> Graph:
|
||||
...
|
||||
|
||||
def compile_node(
|
||||
self,
|
||||
node: ManifestNode,
|
||||
manifest: Manifest,
|
||||
extra_context: Optional[Dict[str, Any]] = None,
|
||||
) -> NonSourceCompiledNode:
|
||||
...
|
||||
|
||||
|
||||
AdapterConfig_T = TypeVar(
|
||||
'AdapterConfig_T', bound=AdapterConfig
|
||||
)
|
||||
ConnectionManager_T = TypeVar(
|
||||
'ConnectionManager_T', bound=ConnectionManagerProtocol
|
||||
)
|
||||
Relation_T = TypeVar(
|
||||
'Relation_T', bound=RelationProtocol
|
||||
)
|
||||
Column_T = TypeVar(
|
||||
'Column_T', bound=ColumnProtocol
|
||||
)
|
||||
Compiler_T = TypeVar('Compiler_T', bound=CompilerProtocol)
|
||||
|
||||
|
||||
class AdapterProtocol(
|
||||
Protocol,
|
||||
Generic[
|
||||
AdapterConfig_T,
|
||||
ConnectionManager_T,
|
||||
Relation_T,
|
||||
Column_T,
|
||||
Compiler_T,
|
||||
]
|
||||
):
|
||||
AdapterSpecificConfigs: ClassVar[Type[AdapterConfig_T]]
|
||||
Column: ClassVar[Type[Column_T]]
|
||||
Relation: ClassVar[Type[Relation_T]]
|
||||
ConnectionManager: ClassVar[Type[ConnectionManager_T]]
|
||||
connections: ConnectionManager_T
|
||||
|
||||
def __init__(self, config: AdapterRequiredConfig):
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def type(cls) -> str:
|
||||
pass
|
||||
|
||||
def set_query_header(self, manifest: Manifest) -> None:
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def get_thread_identifier() -> Hashable:
|
||||
...
|
||||
|
||||
def get_thread_connection(self) -> Connection:
|
||||
...
|
||||
|
||||
def set_thread_connection(self, conn: Connection) -> None:
|
||||
...
|
||||
|
||||
def get_if_exists(self) -> Optional[Connection]:
|
||||
...
|
||||
|
||||
def clear_thread_connection(self) -> None:
|
||||
...
|
||||
|
||||
def clear_transaction(self) -> None:
|
||||
...
|
||||
|
||||
def exception_handler(self, sql: str) -> ContextManager:
|
||||
...
|
||||
|
||||
def set_connection_name(self, name: Optional[str] = None) -> Connection:
|
||||
...
|
||||
|
||||
def cancel_open(self) -> Optional[List[str]]:
|
||||
...
|
||||
|
||||
def open(cls, connection: Connection) -> Connection:
|
||||
...
|
||||
|
||||
def release(self) -> None:
|
||||
...
|
||||
|
||||
def cleanup_all(self) -> None:
|
||||
...
|
||||
|
||||
def begin(self) -> None:
|
||||
...
|
||||
|
||||
def commit(self) -> None:
|
||||
...
|
||||
|
||||
def close(cls, connection: Connection) -> Connection:
|
||||
...
|
||||
|
||||
def commit_if_has_connection(self) -> None:
|
||||
...
|
||||
|
||||
def execute(
|
||||
self, sql: str, auto_begin: bool = False, fetch: bool = False
|
||||
) -> Tuple[Union[str, AdapterResponse], agate.Table]:
|
||||
...
|
||||
|
||||
def get_compiler(self) -> Compiler_T:
|
||||
...
|
||||
24
core/dbt/adapters/reference_keys.py
Normal file
24
core/dbt/adapters/reference_keys.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# this module exists to resolve circular imports with the events module
|
||||
|
||||
from collections import namedtuple
|
||||
from typing import Optional
|
||||
|
||||
|
||||
_ReferenceKey = namedtuple('_ReferenceKey', 'database schema identifier')
|
||||
|
||||
|
||||
def lowercase(value: Optional[str]) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
else:
|
||||
return value.lower()
|
||||
|
||||
|
||||
def _make_key(relation) -> _ReferenceKey:
|
||||
"""Make _ReferenceKeys with lowercase values for the cache so we don't have
|
||||
to keep track of quoting
|
||||
"""
|
||||
# databases and schemas can both be None
|
||||
return _ReferenceKey(lowercase(relation.database),
|
||||
lowercase(relation.schema),
|
||||
lowercase(relation.identifier))
|
||||
@@ -1,14 +1,17 @@
|
||||
import abc
|
||||
import time
|
||||
from typing import List, Optional, Tuple, Any, Iterable, Dict
|
||||
from typing import List, Optional, Tuple, Any, Iterable, Dict, Union
|
||||
|
||||
import agate
|
||||
|
||||
import dbt.clients.agate_helper
|
||||
import dbt.exceptions
|
||||
from dbt.contracts.connection import Connection
|
||||
from dbt.adapters.base import BaseConnectionManager
|
||||
from dbt.logger import GLOBAL_LOGGER as logger
|
||||
from dbt.contracts.connection import (
|
||||
Connection, ConnectionState, AdapterResponse
|
||||
)
|
||||
from dbt.events.functions import fire_event
|
||||
from dbt.events.types import ConnectionUsed, SQLQuery, SQLCommit, SQLQueryStatus
|
||||
|
||||
|
||||
class SQLConnectionManager(BaseConnectionManager):
|
||||
@@ -17,7 +20,7 @@ class SQLConnectionManager(BaseConnectionManager):
|
||||
Methods to implement:
|
||||
- exception_handler
|
||||
- cancel
|
||||
- get_status
|
||||
- get_response
|
||||
- open
|
||||
"""
|
||||
@abc.abstractmethod
|
||||
@@ -37,7 +40,10 @@ class SQLConnectionManager(BaseConnectionManager):
|
||||
|
||||
# if the connection failed, the handle will be None so we have
|
||||
# nothing to cancel.
|
||||
if connection.handle is not None:
|
||||
if (
|
||||
connection.handle is not None and
|
||||
connection.state == ConnectionState.OPEN
|
||||
):
|
||||
self.cancel(connection)
|
||||
if connection.name is not None:
|
||||
names.append(connection.name)
|
||||
@@ -53,9 +59,7 @@ class SQLConnectionManager(BaseConnectionManager):
|
||||
connection = self.get_thread_connection()
|
||||
if auto_begin and connection.transaction_open is False:
|
||||
self.begin()
|
||||
|
||||
logger.debug('Using {} connection "{}".'
|
||||
.format(self.TYPE, connection.name))
|
||||
fire_event(ConnectionUsed(conn_type=self.TYPE, conn_name=connection.name))
|
||||
|
||||
with self.exception_handler(sql):
|
||||
if abridge_sql_log:
|
||||
@@ -63,29 +67,26 @@ class SQLConnectionManager(BaseConnectionManager):
|
||||
else:
|
||||
log_sql = sql
|
||||
|
||||
logger.debug(
|
||||
'On {connection_name}: {sql}',
|
||||
connection_name=connection.name,
|
||||
sql=log_sql,
|
||||
)
|
||||
fire_event(SQLQuery(conn_name=connection.name, sql=log_sql))
|
||||
pre = time.time()
|
||||
|
||||
cursor = connection.handle.cursor()
|
||||
cursor.execute(sql, bindings)
|
||||
|
||||
logger.debug(
|
||||
"SQL status: {status} in {elapsed:0.2f} seconds",
|
||||
status=self.get_status(cursor),
|
||||
elapsed=(time.time() - pre)
|
||||
fire_event(
|
||||
SQLQueryStatus(
|
||||
status=str(self.get_response(cursor)),
|
||||
elapsed=round((time.time() - pre), 2)
|
||||
)
|
||||
)
|
||||
|
||||
return connection, cursor
|
||||
|
||||
@abc.abstractclassmethod
|
||||
def get_status(cls, cursor: Any) -> str:
|
||||
def get_response(cls, cursor: Any) -> Union[AdapterResponse, str]:
|
||||
"""Get the status of the cursor."""
|
||||
raise dbt.exceptions.NotImplementedException(
|
||||
'`get_status` is not implemented for this adapter!'
|
||||
'`get_response` is not implemented for this adapter!'
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -94,7 +95,14 @@ class SQLConnectionManager(BaseConnectionManager):
|
||||
column_names: Iterable[str],
|
||||
rows: Iterable[Any]
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
||||
unique_col_names = dict()
|
||||
for idx in range(len(column_names)):
|
||||
col_name = column_names[idx]
|
||||
if col_name in unique_col_names:
|
||||
unique_col_names[col_name] += 1
|
||||
column_names[idx] = f'{col_name}_{unique_col_names[col_name]}'
|
||||
else:
|
||||
unique_col_names[column_names[idx]] = 1
|
||||
return [dict(zip(column_names, row)) for row in rows]
|
||||
|
||||
@classmethod
|
||||
@@ -107,19 +115,22 @@ class SQLConnectionManager(BaseConnectionManager):
|
||||
rows = cursor.fetchall()
|
||||
data = cls.process_results(column_names, rows)
|
||||
|
||||
return dbt.clients.agate_helper.table_from_data(data, column_names)
|
||||
return dbt.clients.agate_helper.table_from_data_flat(
|
||||
data,
|
||||
column_names
|
||||
)
|
||||
|
||||
def execute(
|
||||
self, sql: str, auto_begin: bool = False, fetch: bool = False
|
||||
) -> Tuple[str, agate.Table]:
|
||||
) -> Tuple[Union[AdapterResponse, str], agate.Table]:
|
||||
sql = self._add_query_comment(sql)
|
||||
_, cursor = self.add_query(sql, auto_begin)
|
||||
status = self.get_status(cursor)
|
||||
response = self.get_response(cursor)
|
||||
if fetch:
|
||||
table = self.get_result_from_cursor(cursor)
|
||||
else:
|
||||
table = dbt.clients.agate_helper.empty_table()
|
||||
return status, table
|
||||
return response, table
|
||||
|
||||
def add_begin_query(self):
|
||||
return self.add_query('BEGIN', auto_begin=False)
|
||||
@@ -129,13 +140,6 @@ class SQLConnectionManager(BaseConnectionManager):
|
||||
|
||||
def begin(self):
|
||||
connection = self.get_thread_connection()
|
||||
|
||||
if dbt.flags.STRICT_MODE:
|
||||
if not isinstance(connection, Connection):
|
||||
raise dbt.exceptions.CompilerException(
|
||||
f'In begin, got {connection} - not a Connection!'
|
||||
)
|
||||
|
||||
if connection.transaction_open is True:
|
||||
raise dbt.exceptions.InternalException(
|
||||
'Tried to begin a new transaction on connection "{}", but '
|
||||
@@ -148,18 +152,12 @@ class SQLConnectionManager(BaseConnectionManager):
|
||||
|
||||
def commit(self):
|
||||
connection = self.get_thread_connection()
|
||||
if dbt.flags.STRICT_MODE:
|
||||
if not isinstance(connection, Connection):
|
||||
raise dbt.exceptions.CompilerException(
|
||||
f'In commit, got {connection} - not a Connection!'
|
||||
)
|
||||
|
||||
if connection.transaction_open is False:
|
||||
raise dbt.exceptions.InternalException(
|
||||
'Tried to commit transaction on connection "{}", but '
|
||||
'it does not have one open!'.format(connection.name))
|
||||
|
||||
logger.debug('On {}: COMMIT'.format(connection.name))
|
||||
fire_event(SQLCommit(conn_name=connection.name))
|
||||
self.add_commit_query()
|
||||
|
||||
connection.transaction_open = False
|
||||
|
||||
@@ -4,12 +4,14 @@ from typing import Any, Optional, Tuple, Type, List
|
||||
import dbt.clients.agate_helper
|
||||
from dbt.contracts.connection import Connection
|
||||
import dbt.exceptions
|
||||
import dbt.flags
|
||||
from dbt.adapters.base import BaseAdapter, available
|
||||
from dbt.adapters.cache import _make_key
|
||||
from dbt.adapters.sql import SQLConnectionManager
|
||||
from dbt.logger import GLOBAL_LOGGER as logger
|
||||
from dbt.events.functions import fire_event
|
||||
from dbt.events.types import ColTypeChange, SchemaCreation, SchemaDrop
|
||||
|
||||
from dbt.adapters.factory import BaseRelation
|
||||
|
||||
from dbt.adapters.base.relation import BaseRelation
|
||||
|
||||
LIST_RELATIONS_MACRO_NAME = 'list_relations_without_caching'
|
||||
GET_COLUMNS_IN_RELATION_MACRO_NAME = 'get_columns_in_relation'
|
||||
@@ -117,8 +119,13 @@ class SQLAdapter(BaseAdapter):
|
||||
target_column.can_expand_to(reference_column):
|
||||
col_string_size = reference_column.string_size()
|
||||
new_type = self.Column.string_type(col_string_size)
|
||||
logger.debug("Changing col type from {} to {} in table {}",
|
||||
target_column.data_type, new_type, current)
|
||||
fire_event(
|
||||
ColTypeChange(
|
||||
orig_type=target_column.data_type,
|
||||
new_type=new_type,
|
||||
table=_make_key(current),
|
||||
)
|
||||
)
|
||||
|
||||
self.alter_column_type(current, column_name, new_type)
|
||||
|
||||
@@ -168,37 +175,37 @@ class SQLAdapter(BaseAdapter):
|
||||
kwargs=kwargs
|
||||
)
|
||||
|
||||
def get_columns_in_relation(self, relation: str):
|
||||
def get_columns_in_relation(self, relation):
|
||||
return self.execute_macro(
|
||||
GET_COLUMNS_IN_RELATION_MACRO_NAME,
|
||||
kwargs={'relation': relation}
|
||||
)
|
||||
|
||||
def create_schema(self, database: str, schema: str) -> None:
|
||||
logger.debug('Creating schema "{}"."{}".', database, schema)
|
||||
def create_schema(self, relation: BaseRelation) -> None:
|
||||
relation = relation.without_identifier()
|
||||
fire_event(SchemaCreation(relation=_make_key(relation)))
|
||||
kwargs = {
|
||||
'database_name': self.quote_as_configured(database, 'database'),
|
||||
'schema_name': self.quote_as_configured(schema, 'schema'),
|
||||
'relation': relation,
|
||||
}
|
||||
self.execute_macro(CREATE_SCHEMA_MACRO_NAME, kwargs=kwargs)
|
||||
self.commit_if_has_connection()
|
||||
# we can't update the cache here, as if the schema already existed we
|
||||
# don't want to (incorrectly) say that it's empty
|
||||
|
||||
def drop_schema(self, database: str, schema: str) -> None:
|
||||
logger.debug('Dropping schema "{}"."{}".', database, schema)
|
||||
def drop_schema(self, relation: BaseRelation) -> None:
|
||||
relation = relation.without_identifier()
|
||||
fire_event(SchemaDrop(relation=_make_key(relation)))
|
||||
kwargs = {
|
||||
'database_name': self.quote_as_configured(database, 'database'),
|
||||
'schema_name': self.quote_as_configured(schema, 'schema'),
|
||||
'relation': relation,
|
||||
}
|
||||
self.execute_macro(DROP_SCHEMA_MACRO_NAME, kwargs=kwargs)
|
||||
# we can update the cache here
|
||||
self.cache.drop_schema(database, schema)
|
||||
self.cache.drop_schema(relation.database, relation.schema)
|
||||
|
||||
def list_relations_without_caching(
|
||||
self, information_schema, schema
|
||||
self, schema_relation: BaseRelation,
|
||||
) -> List[BaseRelation]:
|
||||
kwargs = {'information_schema': information_schema, 'schema': schema}
|
||||
kwargs = {'schema_relation': schema_relation}
|
||||
results = self.execute_macro(
|
||||
LIST_RELATIONS_MACRO_NAME,
|
||||
kwargs=kwargs
|
||||
@@ -212,9 +219,9 @@ class SQLAdapter(BaseAdapter):
|
||||
}
|
||||
for _database, name, _schema, _type in results:
|
||||
try:
|
||||
_type = self.Relation.RelationType(_type)
|
||||
_type = self.Relation.get_relation_type(_type)
|
||||
except ValueError:
|
||||
_type = self.Relation.RelationType.External
|
||||
_type = self.Relation.External
|
||||
relations.append(self.Relation.create(
|
||||
database=_database,
|
||||
schema=_schema,
|
||||
|
||||
1
core/dbt/clients/README.md
Normal file
1
core/dbt/clients/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Clients README
|
||||
@@ -69,7 +69,7 @@ BLOCK_START_PATTERN = regex(''.join((
|
||||
|
||||
RAW_BLOCK_PATTERN = regex(''.join((
|
||||
r'(?:\s*\{\%\-|\{\%)\s*raw\s*(?:\-\%\}\s*|\%\})',
|
||||
r'(?:.*)',
|
||||
r'(?:.*?)',
|
||||
r'(?:\s*\{\%\-|\{\%)\s*endraw\s*(?:\-\%\}\s*|\%\})',
|
||||
)))
|
||||
|
||||
@@ -92,6 +92,18 @@ class TagIterator:
|
||||
self._parenthesis_stack = []
|
||||
self.pos = 0
|
||||
|
||||
def linepos(self, end=None) -> str:
|
||||
"""Given an absolute position in the input data, return a pair of
|
||||
line number + relative position to the start of the line.
|
||||
"""
|
||||
end_val: int = self.pos if end is None else end
|
||||
data = self.data[:end_val]
|
||||
# if not found, rfind returns -1, and -1+1=0, which is perfect!
|
||||
last_line_start = data.rfind('\n') + 1
|
||||
# it's easy to forget this, but line numbers are 1-indexed
|
||||
line_number = data.count('\n') + 1
|
||||
return f'{line_number}:{end_val - last_line_start}'
|
||||
|
||||
def advance(self, new_position):
|
||||
self.pos = new_position
|
||||
|
||||
@@ -320,20 +332,28 @@ class BlockIterator:
|
||||
dbt.exceptions.raise_compiler_error((
|
||||
'Got an unexpected control flow end tag, got {} but '
|
||||
'never saw a preceeding {} (@ {})'
|
||||
).format(tag.block_type_name, expected, tag.start))
|
||||
).format(
|
||||
tag.block_type_name,
|
||||
expected,
|
||||
self.tag_parser.linepos(tag.start)
|
||||
))
|
||||
expected = _CONTROL_FLOW_TAGS[found]
|
||||
if expected != tag.block_type_name:
|
||||
dbt.exceptions.raise_compiler_error((
|
||||
'Got an unexpected control flow end tag, got {} but '
|
||||
'expected {} next (@ {})'
|
||||
).format(tag.block_type_name, expected, tag.start))
|
||||
).format(
|
||||
tag.block_type_name,
|
||||
expected,
|
||||
self.tag_parser.linepos(tag.start)
|
||||
))
|
||||
|
||||
if tag.block_type_name in allowed_blocks:
|
||||
if self.stack:
|
||||
dbt.exceptions.raise_compiler_error((
|
||||
'Got a block definition inside control flow at {}. '
|
||||
'All dbt block definitions must be at the top level'
|
||||
).format(tag.start))
|
||||
).format(self.tag_parser.linepos(tag.start)))
|
||||
if self.current is not None:
|
||||
dbt.exceptions.raise_compiler_error(
|
||||
duplicate_tags.format(outer=self.current, inner=tag)
|
||||
@@ -347,6 +367,7 @@ class BlockIterator:
|
||||
|
||||
elif self.is_current_end(tag):
|
||||
self.last_position = tag.end
|
||||
assert self.current is not None
|
||||
yield BlockTag(
|
||||
block_type_name=self.current.block_type_name,
|
||||
block_name=self.current.block_name,
|
||||
@@ -356,10 +377,11 @@ class BlockIterator:
|
||||
self.current = None
|
||||
|
||||
if self.current:
|
||||
linecount = self.data[:self.current.end].count('\n') + 1
|
||||
dbt.exceptions.raise_compiler_error((
|
||||
'Reached EOF without finding a close block for '
|
||||
'{0.block_type_name} (from {0.end})'
|
||||
).format(self.current))
|
||||
'Reached EOF without finding a close tag for '
|
||||
'{} (searched from line {})'
|
||||
).format(self.current.block_type_name, linecount))
|
||||
|
||||
if collect_raw_data:
|
||||
raw_data = self.data[self.last_position:]
|
||||
|
||||
@@ -1,24 +1,98 @@
|
||||
from codecs import BOM_UTF8
|
||||
|
||||
import agate
|
||||
import datetime
|
||||
import isodate
|
||||
import json
|
||||
import dbt.utils
|
||||
from typing import Iterable, List, Dict, Union, Optional, Any
|
||||
|
||||
from dbt.exceptions import RuntimeException
|
||||
|
||||
|
||||
BOM = BOM_UTF8.decode('utf-8') # '\ufeff'
|
||||
|
||||
DEFAULT_TYPE_TESTER = agate.TypeTester(types=[
|
||||
agate.data_types.Number(null_values=('null', '')),
|
||||
agate.data_types.TimeDelta(null_values=('null', '')),
|
||||
agate.data_types.Date(null_values=('null', '')),
|
||||
agate.data_types.DateTime(null_values=('null', '')),
|
||||
agate.data_types.Boolean(true_values=('true',),
|
||||
false_values=('false',),
|
||||
null_values=('null', '')),
|
||||
agate.data_types.Text(null_values=('null', ''))
|
||||
])
|
||||
|
||||
class Number(agate.data_types.Number):
|
||||
# undo the change in https://github.com/wireservice/agate/pull/733
|
||||
# i.e. do not cast True and False to numeric 1 and 0
|
||||
def cast(self, d):
|
||||
if type(d) == bool:
|
||||
raise agate.exceptions.CastError(
|
||||
'Do not cast True to 1 or False to 0.'
|
||||
)
|
||||
else:
|
||||
return super().cast(d)
|
||||
|
||||
|
||||
def table_from_data(data, column_names):
|
||||
class ISODateTime(agate.data_types.DateTime):
|
||||
def cast(self, d):
|
||||
# this is agate.data_types.DateTime.cast with the "clever" bits removed
|
||||
# so we only handle ISO8601 stuff
|
||||
if isinstance(d, datetime.datetime) or d is None:
|
||||
return d
|
||||
elif isinstance(d, datetime.date):
|
||||
return datetime.datetime.combine(d, datetime.time(0, 0, 0))
|
||||
elif isinstance(d, str):
|
||||
d = d.strip()
|
||||
if d.lower() in self.null_values:
|
||||
return None
|
||||
try:
|
||||
return isodate.parse_datetime(d)
|
||||
except: # noqa
|
||||
pass
|
||||
|
||||
raise agate.exceptions.CastError(
|
||||
'Can not parse value "%s" as datetime.' % d
|
||||
)
|
||||
|
||||
|
||||
def build_type_tester(
|
||||
text_columns: Iterable[str],
|
||||
string_null_values: Optional[Iterable[str]] = ('null', '')
|
||||
) -> agate.TypeTester:
|
||||
|
||||
types = [
|
||||
Number(null_values=('null', '')),
|
||||
agate.data_types.Date(null_values=('null', ''),
|
||||
date_format='%Y-%m-%d'),
|
||||
agate.data_types.DateTime(null_values=('null', ''),
|
||||
datetime_format='%Y-%m-%d %H:%M:%S'),
|
||||
ISODateTime(null_values=('null', '')),
|
||||
agate.data_types.Boolean(true_values=('true',),
|
||||
false_values=('false',),
|
||||
null_values=('null', '')),
|
||||
agate.data_types.Text(null_values=string_null_values)
|
||||
]
|
||||
force = {
|
||||
k: agate.data_types.Text(null_values=string_null_values)
|
||||
for k in text_columns
|
||||
}
|
||||
return agate.TypeTester(force=force, types=types)
|
||||
|
||||
|
||||
DEFAULT_TYPE_TESTER = build_type_tester(())
|
||||
|
||||
|
||||
def table_from_rows(
|
||||
rows: List[Any],
|
||||
column_names: Iterable[str],
|
||||
text_only_columns: Optional[Iterable[str]] = None,
|
||||
) -> agate.Table:
|
||||
if text_only_columns is None:
|
||||
column_types = DEFAULT_TYPE_TESTER
|
||||
else:
|
||||
# If text_only_columns are present, prevent coercing empty string or
|
||||
# literal 'null' strings to a None representation.
|
||||
column_types = build_type_tester(
|
||||
text_only_columns,
|
||||
string_null_values=()
|
||||
)
|
||||
|
||||
return agate.Table(rows, column_names, column_types=column_types)
|
||||
|
||||
|
||||
def table_from_data(data, column_names: Iterable[str]) -> agate.Table:
|
||||
"Convert list of dictionaries into an Agate table"
|
||||
|
||||
# The agate table is generated from a list of dicts, so the column order
|
||||
@@ -33,20 +107,35 @@ def table_from_data(data, column_names):
|
||||
return table.select(column_names)
|
||||
|
||||
|
||||
def table_from_data_flat(data, column_names):
|
||||
"Convert list of dictionaries into an Agate table"
|
||||
def table_from_data_flat(data, column_names: Iterable[str]) -> agate.Table:
|
||||
"""
|
||||
Convert a list of dictionaries into an Agate table. This method does not
|
||||
coerce string values into more specific types (eg. '005' will not be
|
||||
coerced to '5'). Additionally, this method does not coerce values to
|
||||
None (eg. '' or 'null' will retain their string literal representations).
|
||||
"""
|
||||
|
||||
rows = []
|
||||
text_only_columns = set()
|
||||
for _row in data:
|
||||
row = []
|
||||
for value in list(_row.values()):
|
||||
for col_name in column_names:
|
||||
value = _row[col_name]
|
||||
if isinstance(value, (dict, list, tuple)):
|
||||
row.append(json.dumps(value))
|
||||
else:
|
||||
row.append(value)
|
||||
# Represent container types as json strings
|
||||
value = json.dumps(value, cls=dbt.utils.JSONEncoder)
|
||||
text_only_columns.add(col_name)
|
||||
elif isinstance(value, str):
|
||||
text_only_columns.add(col_name)
|
||||
row.append(value)
|
||||
|
||||
rows.append(row)
|
||||
|
||||
return agate.Table(rows, column_names, column_types=DEFAULT_TYPE_TESTER)
|
||||
return table_from_rows(
|
||||
rows=rows,
|
||||
column_names=column_names,
|
||||
text_only_columns=text_only_columns
|
||||
)
|
||||
|
||||
|
||||
def empty_table():
|
||||
@@ -61,8 +150,91 @@ def as_matrix(table):
|
||||
return [r.values() for r in table.rows.values()]
|
||||
|
||||
|
||||
def from_csv(abspath):
|
||||
def from_csv(abspath, text_columns):
|
||||
type_tester = build_type_tester(text_columns=text_columns)
|
||||
with open(abspath, encoding='utf-8') as fp:
|
||||
if fp.read(1) != BOM:
|
||||
fp.seek(0)
|
||||
return agate.Table.from_csv(fp, column_types=DEFAULT_TYPE_TESTER)
|
||||
return agate.Table.from_csv(fp, column_types=type_tester)
|
||||
|
||||
|
||||
class _NullMarker:
|
||||
pass
|
||||
|
||||
|
||||
NullableAgateType = Union[agate.data_types.DataType, _NullMarker]
|
||||
|
||||
|
||||
class ColumnTypeBuilder(Dict[str, NullableAgateType]):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key not in self:
|
||||
super().__setitem__(key, value)
|
||||
return
|
||||
|
||||
existing_type = self[key]
|
||||
if isinstance(existing_type, _NullMarker):
|
||||
# overwrite
|
||||
super().__setitem__(key, value)
|
||||
elif isinstance(value, _NullMarker):
|
||||
# use the existing value
|
||||
return
|
||||
elif not isinstance(value, type(existing_type)):
|
||||
# actual type mismatch!
|
||||
raise RuntimeException(
|
||||
f'Tables contain columns with the same names ({key}), '
|
||||
f'but different types ({value} vs {existing_type})'
|
||||
)
|
||||
|
||||
def finalize(self) -> Dict[str, agate.data_types.DataType]:
|
||||
result: Dict[str, agate.data_types.DataType] = {}
|
||||
for key, value in self.items():
|
||||
if isinstance(value, _NullMarker):
|
||||
# this is what agate would do.
|
||||
result[key] = agate.data_types.Number()
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def _merged_column_types(
|
||||
tables: List[agate.Table]
|
||||
) -> Dict[str, agate.data_types.DataType]:
|
||||
# this is a lot like agate.Table.merge, but with handling for all-null
|
||||
# rows being "any type".
|
||||
new_columns: ColumnTypeBuilder = ColumnTypeBuilder()
|
||||
for table in tables:
|
||||
for i in range(len(table.columns)):
|
||||
column_name: str = table.column_names[i]
|
||||
column_type: NullableAgateType = table.column_types[i]
|
||||
# avoid over-sensitive type inference
|
||||
if all(x is None for x in table.columns[column_name]):
|
||||
column_type = _NullMarker()
|
||||
new_columns[column_name] = column_type
|
||||
|
||||
return new_columns.finalize()
|
||||
|
||||
|
||||
def merge_tables(tables: List[agate.Table]) -> agate.Table:
|
||||
"""This is similar to agate.Table.merge, but it handles rows of all 'null'
|
||||
values more gracefully during merges.
|
||||
"""
|
||||
new_columns = _merged_column_types(tables)
|
||||
column_names = tuple(new_columns.keys())
|
||||
column_types = tuple(new_columns.values())
|
||||
|
||||
rows: List[agate.Row] = []
|
||||
for table in tables:
|
||||
if (
|
||||
table.column_names == column_names and
|
||||
table.column_types == column_types
|
||||
):
|
||||
rows.extend(table.rows)
|
||||
else:
|
||||
for row in table.rows:
|
||||
data = [row.get(name, None) for name in column_names]
|
||||
rows.append(agate.Row(data, column_names))
|
||||
# _is_fork to tell agate that we already made things into `Row`s.
|
||||
return agate.Table(rows, column_names, column_types, _is_fork=True)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
from dbt.logger import GLOBAL_LOGGER as logger
|
||||
import dbt.exceptions
|
||||
from dbt.clients.system import run_cmd
|
||||
|
||||
NOT_INSTALLED_MSG = """
|
||||
dbt requires the gcloud SDK to be installed to authenticate with BigQuery.
|
||||
Please download and install the SDK, or use a Service Account instead.
|
||||
|
||||
https://cloud.google.com/sdk/
|
||||
"""
|
||||
|
||||
|
||||
def gcloud_installed():
|
||||
try:
|
||||
run_cmd('.', ['gcloud', '--version'])
|
||||
return True
|
||||
except OSError as e:
|
||||
logger.debug(e)
|
||||
return False
|
||||
|
||||
|
||||
def setup_default_credentials():
|
||||
if gcloud_installed():
|
||||
run_cmd('.', ["gcloud", "auth", "application-default", "login"])
|
||||
else:
|
||||
raise dbt.exceptions.RuntimeException(NOT_INSTALLED_MSG)
|
||||
@@ -2,17 +2,70 @@ import re
|
||||
import os.path
|
||||
|
||||
from dbt.clients.system import run_cmd, rmdir
|
||||
from dbt.logger import GLOBAL_LOGGER as logger
|
||||
import dbt.exceptions
|
||||
from dbt.events.functions import fire_event
|
||||
from dbt.events.types import (
|
||||
GitSparseCheckoutSubdirectory, GitProgressCheckoutRevision,
|
||||
GitProgressUpdatingExistingDependency, GitProgressPullingNewDependency,
|
||||
GitNothingToDo, GitProgressUpdatedCheckoutRange, GitProgressCheckedOutAt
|
||||
)
|
||||
from dbt.exceptions import (
|
||||
CommandResultError, RuntimeException, bad_package_spec, raise_git_cloning_error,
|
||||
raise_git_cloning_problem
|
||||
)
|
||||
from packaging import version
|
||||
|
||||
|
||||
def clone(repo, cwd, dirname=None, remove_git_dir=False):
|
||||
clone_cmd = ['git', 'clone', '--depth', '1', repo]
|
||||
def _is_commit(revision: str) -> bool:
|
||||
# match SHA-1 git commit
|
||||
return bool(re.match(r"\b[0-9a-f]{40}\b", revision))
|
||||
|
||||
|
||||
def _raise_git_cloning_error(repo, revision, error):
|
||||
stderr = error.stderr.decode('utf-8').strip()
|
||||
if 'usage: git' in stderr:
|
||||
stderr = stderr.split('\nusage: git')[0]
|
||||
if re.match("fatal: destination path '(.+)' already exists", stderr):
|
||||
raise_git_cloning_error(error)
|
||||
|
||||
bad_package_spec(repo, revision, stderr)
|
||||
|
||||
|
||||
def clone(repo, cwd, dirname=None, remove_git_dir=False, revision=None, subdirectory=None):
|
||||
has_revision = revision is not None
|
||||
is_commit = _is_commit(revision or "")
|
||||
|
||||
clone_cmd = ['git', 'clone', '--depth', '1']
|
||||
if subdirectory:
|
||||
fire_event(GitSparseCheckoutSubdirectory(subdir=subdirectory))
|
||||
out, _ = run_cmd(cwd, ['git', '--version'], env={'LC_ALL': 'C'})
|
||||
git_version = version.parse(re.search(r"\d+\.\d+\.\d+", out.decode("utf-8")).group(0))
|
||||
if not git_version >= version.parse("2.25.0"):
|
||||
# 2.25.0 introduces --sparse
|
||||
raise RuntimeError(
|
||||
"Please update your git version to pull a dbt package "
|
||||
"from a subdirectory: your version is {}, >= 2.25.0 needed".format(git_version)
|
||||
)
|
||||
clone_cmd.extend(['--filter=blob:none', '--sparse'])
|
||||
|
||||
if has_revision and not is_commit:
|
||||
clone_cmd.extend(['--branch', revision])
|
||||
|
||||
clone_cmd.append(repo)
|
||||
|
||||
if dirname is not None:
|
||||
clone_cmd.append(dirname)
|
||||
try:
|
||||
result = run_cmd(cwd, clone_cmd, env={'LC_ALL': 'C'})
|
||||
except CommandResultError as exc:
|
||||
_raise_git_cloning_error(repo, revision, exc)
|
||||
|
||||
result = run_cmd(cwd, clone_cmd, env={'LC_ALL': 'C'})
|
||||
if subdirectory:
|
||||
cwd_subdir = os.path.join(cwd, dirname or '')
|
||||
clone_cmd_subdir = ['git', 'sparse-checkout', 'set', subdirectory]
|
||||
try:
|
||||
run_cmd(cwd_subdir, clone_cmd_subdir)
|
||||
except CommandResultError as exc:
|
||||
_raise_git_cloning_error(repo, revision, exc)
|
||||
|
||||
if remove_git_dir:
|
||||
rmdir(os.path.join(dirname, '.git'))
|
||||
@@ -26,33 +79,38 @@ def list_tags(cwd):
|
||||
return tags
|
||||
|
||||
|
||||
def _checkout(cwd, repo, branch):
|
||||
logger.debug(' Checking out branch {}.'.format(branch))
|
||||
def _checkout(cwd, repo, revision):
|
||||
fire_event(GitProgressCheckoutRevision(revision=revision))
|
||||
|
||||
run_cmd(cwd, ['git', 'remote', 'set-branches', 'origin', branch])
|
||||
run_cmd(cwd, ['git', 'fetch', '--tags', '--depth', '1', 'origin', branch])
|
||||
fetch_cmd = ["git", "fetch", "origin", "--depth", "1"]
|
||||
|
||||
tags = list_tags(cwd)
|
||||
|
||||
# Prefer tags to branches if one exists
|
||||
if branch in tags:
|
||||
spec = 'tags/{}'.format(branch)
|
||||
if _is_commit(revision):
|
||||
run_cmd(cwd, fetch_cmd + [revision])
|
||||
else:
|
||||
spec = 'origin/{}'.format(branch)
|
||||
run_cmd(cwd, ['git', 'remote', 'set-branches', 'origin', revision])
|
||||
run_cmd(cwd, fetch_cmd + ["--tags", revision])
|
||||
|
||||
if _is_commit(revision):
|
||||
spec = revision
|
||||
# Prefer tags to branches if one exists
|
||||
elif revision in list_tags(cwd):
|
||||
spec = 'tags/{}'.format(revision)
|
||||
else:
|
||||
spec = 'origin/{}'.format(revision)
|
||||
|
||||
out, err = run_cmd(cwd, ['git', 'reset', '--hard', spec],
|
||||
env={'LC_ALL': 'C'})
|
||||
return out, err
|
||||
|
||||
|
||||
def checkout(cwd, repo, branch=None):
|
||||
if branch is None:
|
||||
branch = 'master'
|
||||
def checkout(cwd, repo, revision=None):
|
||||
if revision is None:
|
||||
revision = 'HEAD'
|
||||
try:
|
||||
return _checkout(cwd, repo, branch)
|
||||
except dbt.exceptions.CommandResultError as exc:
|
||||
return _checkout(cwd, repo, revision)
|
||||
except CommandResultError as exc:
|
||||
stderr = exc.stderr.decode('utf-8').strip()
|
||||
dbt.exceptions.bad_package_spec(repo, branch, stderr)
|
||||
bad_package_spec(repo, revision, stderr)
|
||||
|
||||
|
||||
def get_current_sha(cwd):
|
||||
@@ -66,36 +124,46 @@ def remove_remote(cwd):
|
||||
|
||||
|
||||
def clone_and_checkout(repo, cwd, dirname=None, remove_git_dir=False,
|
||||
branch=None):
|
||||
revision=None, subdirectory=None):
|
||||
exists = None
|
||||
try:
|
||||
_, err = clone(repo, cwd, dirname=dirname,
|
||||
remove_git_dir=remove_git_dir)
|
||||
except dbt.exceptions.CommandResultError as exc:
|
||||
_, err = clone(
|
||||
repo,
|
||||
cwd,
|
||||
dirname=dirname,
|
||||
remove_git_dir=remove_git_dir,
|
||||
subdirectory=subdirectory,
|
||||
)
|
||||
except CommandResultError as exc:
|
||||
err = exc.stderr.decode('utf-8')
|
||||
exists = re.match("fatal: destination path '(.+)' already exists", err)
|
||||
if not exists: # something else is wrong, raise it
|
||||
raise
|
||||
if not exists:
|
||||
raise_git_cloning_problem(repo)
|
||||
|
||||
directory = None
|
||||
start_sha = None
|
||||
if exists:
|
||||
directory = exists.group(1)
|
||||
logger.debug('Updating existing dependency {}.', directory)
|
||||
fire_event(GitProgressUpdatingExistingDependency(dir=directory))
|
||||
else:
|
||||
matches = re.match("Cloning into '(.+)'", err.decode('utf-8'))
|
||||
if matches is None:
|
||||
raise RuntimeException(
|
||||
f'Error cloning {repo} - never saw "Cloning into ..." from git'
|
||||
)
|
||||
directory = matches.group(1)
|
||||
logger.debug('Pulling new dependency {}.', directory)
|
||||
fire_event(GitProgressPullingNewDependency(dir=directory))
|
||||
full_path = os.path.join(cwd, directory)
|
||||
start_sha = get_current_sha(full_path)
|
||||
checkout(full_path, repo, branch)
|
||||
checkout(full_path, repo, revision)
|
||||
end_sha = get_current_sha(full_path)
|
||||
if exists:
|
||||
if start_sha == end_sha:
|
||||
logger.debug(' Already at {}, nothing to do.', start_sha[:7])
|
||||
fire_event(GitNothingToDo(sha=start_sha[:7]))
|
||||
else:
|
||||
logger.debug(' Updated checkout from {} to {}.',
|
||||
start_sha[:7], end_sha[:7])
|
||||
fire_event(GitProgressUpdatedCheckoutRange(
|
||||
start_sha=start_sha[:7], end_sha=end_sha[:7]
|
||||
))
|
||||
else:
|
||||
logger.debug(' Checked out at {}.', end_sha[:7])
|
||||
return directory
|
||||
fire_event(GitProgressCheckedOutAt(end_sha=end_sha[:7]))
|
||||
return os.path.join(directory, subdirectory or '')
|
||||
|
||||
@@ -1,24 +1,38 @@
|
||||
import codecs
|
||||
import linecache
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import threading
|
||||
from ast import literal_eval
|
||||
from contextlib import contextmanager
|
||||
from typing import List, Union, Set, Optional, Dict, Any, Callable, Iterator
|
||||
from itertools import chain, islice
|
||||
from typing import (
|
||||
List, Union, Set, Optional, Dict, Any, Iterator, Type, NoReturn, Tuple,
|
||||
Callable
|
||||
)
|
||||
|
||||
import jinja2
|
||||
import jinja2._compat
|
||||
import jinja2.ext
|
||||
import jinja2.nativetypes # type: ignore
|
||||
import jinja2.nodes
|
||||
import jinja2.parser
|
||||
import jinja2.sandbox
|
||||
|
||||
import dbt.exceptions
|
||||
import dbt.utils
|
||||
from dbt.utils import (
|
||||
get_dbt_macro_name, get_docs_macro_name, get_materialization_macro_name,
|
||||
get_test_macro_name, deep_map_render
|
||||
)
|
||||
|
||||
from dbt.clients._jinja_blocks import BlockIterator, BlockData, BlockTag
|
||||
from dbt.flags import MACRO_DEBUGGING
|
||||
|
||||
from dbt.logger import GLOBAL_LOGGER as logger # noqa
|
||||
from dbt.contracts.graph.compiled import CompiledGenericTestNode
|
||||
from dbt.contracts.graph.parsed import ParsedGenericTestNode
|
||||
from dbt.exceptions import (
|
||||
InternalException, raise_compiler_error, CompilationException,
|
||||
invalid_materialization_argument, MacroReturn, JinjaRenderingException,
|
||||
UndefinedMacroException
|
||||
)
|
||||
from dbt import flags
|
||||
|
||||
|
||||
def _linecache_inject(source, write):
|
||||
@@ -35,17 +49,20 @@ def _linecache_inject(source, write):
|
||||
tmp_file.write(source)
|
||||
filename = tmp_file.name
|
||||
else:
|
||||
filename = codecs.encode(os.urandom(12), 'hex').decode('ascii')
|
||||
# `codecs.encode` actually takes a `bytes` as the first argument if
|
||||
# the second argument is 'hex' - mypy does not know this.
|
||||
rnd = codecs.encode(os.urandom(12), 'hex') # type: ignore
|
||||
filename = rnd.decode('ascii')
|
||||
|
||||
# encode, though I don't think this matters
|
||||
filename = jinja2._compat.encode_filename(filename)
|
||||
# put ourselves in the cache
|
||||
linecache.cache[filename] = (
|
||||
cache_entry = (
|
||||
len(source),
|
||||
None,
|
||||
[line + '\n' for line in source.splitlines()],
|
||||
filename
|
||||
)
|
||||
# linecache does in fact have an attribute `cache`, thanks
|
||||
linecache.cache[filename] = cache_entry # type: ignore
|
||||
return filename
|
||||
|
||||
|
||||
@@ -56,7 +73,7 @@ class MacroFuzzParser(jinja2.parser.Parser):
|
||||
# modified to fuzz macros defined in the same file. this way
|
||||
# dbt can understand the stack of macros being called.
|
||||
# - @cmcarthur
|
||||
node.name = dbt.utils.get_dbt_macro_name(
|
||||
node.name = get_dbt_macro_name(
|
||||
self.parse_assign_target(name_only=True).name)
|
||||
|
||||
self.parse_signature(node)
|
||||
@@ -67,10 +84,7 @@ class MacroFuzzParser(jinja2.parser.Parser):
|
||||
|
||||
class MacroFuzzEnvironment(jinja2.sandbox.SandboxedEnvironment):
|
||||
def _parse(self, source, name, filename):
|
||||
return MacroFuzzParser(
|
||||
self, source, name,
|
||||
jinja2._compat.encode_filename(filename)
|
||||
).parse()
|
||||
return MacroFuzzParser(self, source, name, filename).parse()
|
||||
|
||||
def _compile(self, source, filename):
|
||||
"""Override jinja's compilation to stash the rendered source inside
|
||||
@@ -80,31 +94,119 @@ class MacroFuzzEnvironment(jinja2.sandbox.SandboxedEnvironment):
|
||||
If the value is 'write', also write the files to disk.
|
||||
WARNING: This can write a ton of data if you aren't careful.
|
||||
"""
|
||||
if filename == '<template>' and MACRO_DEBUGGING:
|
||||
write = MACRO_DEBUGGING == 'write'
|
||||
if filename == '<template>' and flags.MACRO_DEBUGGING:
|
||||
write = flags.MACRO_DEBUGGING == 'write'
|
||||
filename = _linecache_inject(source, write)
|
||||
|
||||
return super()._compile(source, filename)
|
||||
return super()._compile(source, filename) # type: ignore
|
||||
|
||||
|
||||
class NativeSandboxEnvironment(MacroFuzzEnvironment):
|
||||
code_generator_class = jinja2.nativetypes.NativeCodeGenerator
|
||||
|
||||
|
||||
class TextMarker(str):
|
||||
"""A special native-env marker that indicates that a value is text and is
|
||||
not to be evaluated. Use this to prevent your numbery-strings from becoming
|
||||
numbers!
|
||||
"""
|
||||
|
||||
|
||||
class NativeMarker(str):
|
||||
"""A special native-env marker that indicates the field should be passed to
|
||||
literal_eval.
|
||||
"""
|
||||
|
||||
|
||||
class BoolMarker(NativeMarker):
|
||||
pass
|
||||
|
||||
|
||||
class NumberMarker(NativeMarker):
|
||||
pass
|
||||
|
||||
|
||||
def _is_number(value) -> bool:
|
||||
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
||||
|
||||
|
||||
def quoted_native_concat(nodes):
|
||||
"""This is almost native_concat from the NativeTemplate, except in the
|
||||
special case of a single argument that is a quoted string and returns a
|
||||
string, the quotes are re-inserted.
|
||||
"""
|
||||
head = list(islice(nodes, 2))
|
||||
|
||||
if not head:
|
||||
return ''
|
||||
|
||||
if len(head) == 1:
|
||||
raw = head[0]
|
||||
if isinstance(raw, TextMarker):
|
||||
return str(raw)
|
||||
elif not isinstance(raw, NativeMarker):
|
||||
# return non-strings as-is
|
||||
return raw
|
||||
else:
|
||||
# multiple nodes become a string.
|
||||
return "".join([str(v) for v in chain(head, nodes)])
|
||||
|
||||
try:
|
||||
result = literal_eval(raw)
|
||||
except (ValueError, SyntaxError, MemoryError):
|
||||
result = raw
|
||||
if isinstance(raw, BoolMarker) and not isinstance(result, bool):
|
||||
raise JinjaRenderingException(
|
||||
f"Could not convert value '{raw!s}' into type 'bool'"
|
||||
)
|
||||
if isinstance(raw, NumberMarker) and not _is_number(result):
|
||||
raise JinjaRenderingException(
|
||||
f"Could not convert value '{raw!s}' into type 'number'"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class NativeSandboxTemplate(jinja2.nativetypes.NativeTemplate): # mypy: ignore
|
||||
environment_class = NativeSandboxEnvironment
|
||||
|
||||
def render(self, *args, **kwargs):
|
||||
"""Render the template to produce a native Python type. If the
|
||||
result is a single node, its value is returned. Otherwise, the
|
||||
nodes are concatenated as strings. If the result can be parsed
|
||||
with :func:`ast.literal_eval`, the parsed value is returned.
|
||||
Otherwise, the string is returned.
|
||||
"""
|
||||
vars = dict(*args, **kwargs)
|
||||
|
||||
try:
|
||||
return quoted_native_concat(
|
||||
self.root_render_func(self.new_context(vars))
|
||||
)
|
||||
except Exception:
|
||||
return self.environment.handle_exception()
|
||||
|
||||
|
||||
NativeSandboxEnvironment.template_class = NativeSandboxTemplate # type: ignore
|
||||
|
||||
|
||||
class TemplateCache:
|
||||
|
||||
def __init__(self):
|
||||
self.file_cache = {}
|
||||
self.file_cache: Dict[str, jinja2.Template] = {}
|
||||
|
||||
def get_node_template(self, node):
|
||||
key = (node.package_name, node.original_file_path)
|
||||
def get_node_template(self, node) -> jinja2.Template:
|
||||
key = node.macro_sql
|
||||
|
||||
if key in self.file_cache:
|
||||
return self.file_cache[key]
|
||||
|
||||
template = get_template(
|
||||
string=node.raw_sql,
|
||||
string=node.macro_sql,
|
||||
ctx={},
|
||||
node=node,
|
||||
)
|
||||
self.file_cache[key] = template
|
||||
|
||||
self.file_cache[key] = template
|
||||
return template
|
||||
|
||||
def clear(self):
|
||||
@@ -129,8 +231,9 @@ class BaseMacroGenerator:
|
||||
template = self.get_template()
|
||||
# make the module. previously we set both vars and local, but that's
|
||||
# redundant: They both end up in the same place
|
||||
# make_module is in jinja2.environment. It returns a TemplateModule
|
||||
module = template.make_module(vars=self.context, shared=False)
|
||||
macro = module.__dict__[dbt.utils.get_dbt_macro_name(name)]
|
||||
macro = module.__dict__[get_dbt_macro_name(name)]
|
||||
module.__dict__.update(self.context)
|
||||
return macro
|
||||
|
||||
@@ -139,11 +242,12 @@ class BaseMacroGenerator:
|
||||
try:
|
||||
yield
|
||||
except (TypeError, jinja2.exceptions.TemplateRuntimeError) as e:
|
||||
dbt.exceptions.raise_compiler_error(str(e))
|
||||
raise_compiler_error(str(e))
|
||||
|
||||
def call_macro(self, *args, **kwargs):
|
||||
# called from __call__ methods
|
||||
if self.context is None:
|
||||
raise dbt.exceptions.InternalException(
|
||||
raise InternalException(
|
||||
'Context is still None in call_macro!'
|
||||
)
|
||||
assert self.context is not None
|
||||
@@ -153,34 +257,79 @@ class BaseMacroGenerator:
|
||||
with self.exception_handler():
|
||||
try:
|
||||
return macro(*args, **kwargs)
|
||||
except dbt.exceptions.MacroReturn as e:
|
||||
except MacroReturn as e:
|
||||
return e.value
|
||||
|
||||
|
||||
class MacroStack(threading.local):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.call_stack = []
|
||||
|
||||
@property
|
||||
def depth(self) -> int:
|
||||
return len(self.call_stack)
|
||||
|
||||
def push(self, name):
|
||||
self.call_stack.append(name)
|
||||
|
||||
def pop(self, name):
|
||||
got = self.call_stack.pop()
|
||||
if got != name:
|
||||
raise InternalException(f'popped {got}, expected {name}')
|
||||
|
||||
|
||||
class MacroGenerator(BaseMacroGenerator):
|
||||
def __init__(self, node, context: Optional[Dict[str, Any]] = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
macro,
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
node: Optional[Any] = None,
|
||||
stack: Optional[MacroStack] = None
|
||||
) -> None:
|
||||
super().__init__(context)
|
||||
self.macro = macro
|
||||
self.node = node
|
||||
self.stack = stack
|
||||
|
||||
def get_template(self):
|
||||
return template_cache.get_node_template(self.node)
|
||||
return template_cache.get_node_template(self.macro)
|
||||
|
||||
def get_name(self) -> str:
|
||||
return self.node.name
|
||||
return self.macro.name
|
||||
|
||||
@contextmanager
|
||||
def exception_handler(self) -> Iterator[None]:
|
||||
try:
|
||||
yield
|
||||
except (TypeError, jinja2.exceptions.TemplateRuntimeError) as e:
|
||||
dbt.exceptions.raise_compiler_error(str(e), self.node)
|
||||
except dbt.exceptions.CompilationException as e:
|
||||
e.stack.append(self.node)
|
||||
raise_compiler_error(str(e), self.macro)
|
||||
except CompilationException as e:
|
||||
e.stack.append(self.macro)
|
||||
raise e
|
||||
|
||||
def __call__(self, context: Dict[str, Any]) -> Callable:
|
||||
self.context = context
|
||||
return self.call_macro
|
||||
# This adds the macro's unique id to the node's 'depends_on'
|
||||
@contextmanager
|
||||
def track_call(self):
|
||||
# This is only called from __call__
|
||||
if self.stack is None or self.node is None:
|
||||
yield
|
||||
else:
|
||||
unique_id = self.macro.unique_id
|
||||
depth = self.stack.depth
|
||||
# only mark depth=0 as a dependency
|
||||
if depth == 0:
|
||||
self.node.depends_on.add_macro(unique_id)
|
||||
self.stack.push(unique_id)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.stack.pop(unique_id)
|
||||
|
||||
# this makes MacroGenerator objects callable like functions
|
||||
def __call__(self, *args, **kwargs):
|
||||
with self.track_call():
|
||||
return self.call_macro(*args, **kwargs)
|
||||
|
||||
|
||||
class QueryStringGenerator(BaseMacroGenerator):
|
||||
@@ -230,11 +379,13 @@ class MaterializationExtension(jinja2.ext.Extension):
|
||||
adapter_name = value.value
|
||||
|
||||
else:
|
||||
dbt.exceptions.invalid_materialization_argument(
|
||||
materialization_name, target.name)
|
||||
invalid_materialization_argument(
|
||||
materialization_name, target.name
|
||||
)
|
||||
|
||||
node.name = dbt.utils.get_materialization_macro_name(
|
||||
materialization_name, adapter_name)
|
||||
node.name = get_materialization_macro_name(
|
||||
materialization_name, adapter_name
|
||||
)
|
||||
|
||||
node.body = parser.parse_statements(('name:endmaterialization',),
|
||||
drop_needle=True)
|
||||
@@ -251,48 +402,42 @@ class DocumentationExtension(jinja2.ext.Extension):
|
||||
|
||||
node.args = []
|
||||
node.defaults = []
|
||||
node.name = dbt.utils.get_docs_macro_name(docs_name)
|
||||
node.name = get_docs_macro_name(docs_name)
|
||||
node.body = parser.parse_statements(('name:enddocs',),
|
||||
drop_needle=True)
|
||||
return node
|
||||
|
||||
|
||||
class TestExtension(jinja2.ext.Extension):
|
||||
tags = ['test']
|
||||
|
||||
def parse(self, parser):
|
||||
node = jinja2.nodes.Macro(lineno=next(parser.stream).lineno)
|
||||
test_name = parser.parse_assign_target(name_only=True).name
|
||||
|
||||
parser.parse_signature(node)
|
||||
node.name = get_test_macro_name(test_name)
|
||||
node.body = parser.parse_statements(('name:endtest',),
|
||||
drop_needle=True)
|
||||
return node
|
||||
|
||||
|
||||
def _is_dunder_name(name):
|
||||
return name.startswith('__') and name.endswith('__')
|
||||
|
||||
|
||||
def create_macro_capture_env(node):
|
||||
|
||||
class ParserMacroCapture(jinja2.Undefined):
|
||||
"""
|
||||
This class sets up the parser to capture macros.
|
||||
"""
|
||||
def create_undefined(node=None):
|
||||
class Undefined(jinja2.Undefined):
|
||||
def __init__(self, hint=None, obj=None, name=None, exc=None):
|
||||
super().__init__(hint=hint, name=name)
|
||||
self.node = node
|
||||
self.name = name
|
||||
self.package_name = node.package_name
|
||||
self.hint = hint
|
||||
# jinja uses these for safety, so we have to override them.
|
||||
# see https://github.com/pallets/jinja/blob/master/jinja2/sandbox.py#L332-L339 # noqa
|
||||
self.unsafe_callable = False
|
||||
self.alters_data = False
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
path = os.path.join(self.node.root_path,
|
||||
self.node.original_file_path)
|
||||
|
||||
logger.debug(
|
||||
'dbt encountered an undefined variable, "{}" in node {}.{} '
|
||||
'(source path: {})'
|
||||
.format(self.name, self.node.package_name,
|
||||
self.node.name, path))
|
||||
|
||||
# match jinja's message
|
||||
dbt.exceptions.raise_compiler_error(
|
||||
"{!r} is undefined".format(self.name),
|
||||
node=self.node
|
||||
)
|
||||
|
||||
def __getitem__(self, name):
|
||||
# Propagate the undefined value if a caller accesses this as if it
|
||||
# were a dictionary
|
||||
@@ -305,73 +450,154 @@ def create_macro_capture_env(node):
|
||||
.format(type(self).__name__, name)
|
||||
)
|
||||
|
||||
self.package_name = self.name
|
||||
self.name = name
|
||||
|
||||
return self
|
||||
return self.__class__(hint=self.hint, name=self.name)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
return ParserMacroCapture
|
||||
def __reduce__(self):
|
||||
raise_compiler_error(f'{self.name} is undefined', node=node)
|
||||
|
||||
return Undefined
|
||||
|
||||
|
||||
def get_environment(node=None, capture_macros=False):
|
||||
args = {
|
||||
NATIVE_FILTERS: Dict[str, Callable[[Any], Any]] = {
|
||||
'as_text': TextMarker,
|
||||
'as_bool': BoolMarker,
|
||||
'as_native': NativeMarker,
|
||||
'as_number': NumberMarker,
|
||||
}
|
||||
|
||||
|
||||
TEXT_FILTERS: Dict[str, Callable[[Any], Any]] = {
|
||||
'as_text': lambda x: x,
|
||||
'as_bool': lambda x: x,
|
||||
'as_native': lambda x: x,
|
||||
'as_number': lambda x: x,
|
||||
}
|
||||
|
||||
|
||||
def get_environment(
|
||||
node=None,
|
||||
capture_macros: bool = False,
|
||||
native: bool = False,
|
||||
) -> jinja2.Environment:
|
||||
args: Dict[str, List[Union[str, Type[jinja2.ext.Extension]]]] = {
|
||||
'extensions': ['jinja2.ext.do']
|
||||
}
|
||||
|
||||
if capture_macros:
|
||||
args['undefined'] = create_macro_capture_env(node)
|
||||
args['undefined'] = create_undefined(node)
|
||||
|
||||
args['extensions'].append(MaterializationExtension)
|
||||
args['extensions'].append(DocumentationExtension)
|
||||
args['extensions'].append(TestExtension)
|
||||
|
||||
return MacroFuzzEnvironment(**args)
|
||||
env_cls: Type[jinja2.Environment]
|
||||
text_filter: Type
|
||||
if native:
|
||||
env_cls = NativeSandboxEnvironment
|
||||
filters = NATIVE_FILTERS
|
||||
else:
|
||||
env_cls = MacroFuzzEnvironment
|
||||
filters = TEXT_FILTERS
|
||||
|
||||
env = env_cls(**args)
|
||||
env.filters.update(filters)
|
||||
|
||||
return env
|
||||
|
||||
|
||||
@contextmanager
|
||||
def catch_jinja(node=None) -> Iterator[None]:
|
||||
try:
|
||||
yield
|
||||
except jinja2.exceptions.TemplateSyntaxError as e:
|
||||
e.translated = False
|
||||
raise CompilationException(str(e), node) from e
|
||||
except jinja2.exceptions.UndefinedError as e:
|
||||
raise UndefinedMacroException(str(e), node) from e
|
||||
except CompilationException as exc:
|
||||
exc.add_node(node)
|
||||
raise
|
||||
|
||||
|
||||
def parse(string):
|
||||
try:
|
||||
with catch_jinja():
|
||||
return get_environment().parse(str(string))
|
||||
|
||||
except (jinja2.exceptions.TemplateSyntaxError,
|
||||
jinja2.exceptions.UndefinedError) as e:
|
||||
e.translated = False
|
||||
dbt.exceptions.raise_compiler_error(str(e))
|
||||
|
||||
|
||||
def get_template(string, ctx, node=None, capture_macros=False):
|
||||
try:
|
||||
env = get_environment(node, capture_macros)
|
||||
def get_template(
|
||||
string: str,
|
||||
ctx: Dict[str, Any],
|
||||
node=None,
|
||||
capture_macros: bool = False,
|
||||
native: bool = False,
|
||||
):
|
||||
with catch_jinja(node):
|
||||
env = get_environment(node, capture_macros, native=native)
|
||||
|
||||
template_source = str(string)
|
||||
return env.from_string(template_source, globals=ctx)
|
||||
|
||||
except (jinja2.exceptions.TemplateSyntaxError,
|
||||
jinja2.exceptions.UndefinedError) as e:
|
||||
e.translated = False
|
||||
dbt.exceptions.raise_compiler_error(str(e), node)
|
||||
|
||||
|
||||
def render_template(template, ctx, node=None):
|
||||
try:
|
||||
def render_template(template, ctx: Dict[str, Any], node=None) -> str:
|
||||
with catch_jinja(node):
|
||||
return template.render(ctx)
|
||||
|
||||
except (jinja2.exceptions.TemplateSyntaxError,
|
||||
jinja2.exceptions.UndefinedError) as e:
|
||||
e.translated = False
|
||||
dbt.exceptions.raise_compiler_error(str(e), node)
|
||||
|
||||
def _requote_result(raw_value: str, rendered: str) -> str:
|
||||
double_quoted = raw_value.startswith('"') and raw_value.endswith('"')
|
||||
single_quoted = raw_value.startswith("'") and raw_value.endswith("'")
|
||||
if double_quoted:
|
||||
quote_char = '"'
|
||||
elif single_quoted:
|
||||
quote_char = "'"
|
||||
else:
|
||||
quote_char = ''
|
||||
return f'{quote_char}{rendered}{quote_char}'
|
||||
|
||||
|
||||
def get_rendered(string, ctx, node=None,
|
||||
capture_macros=False):
|
||||
template = get_template(string, ctx, node,
|
||||
capture_macros=capture_macros)
|
||||
# performance note: Local benmcharking (so take it with a big grain of salt!)
|
||||
# on this indicates that it is is on average slightly slower than
|
||||
# checking two separate patterns, but the standard deviation is smaller with
|
||||
# one pattern. The time difference between the two was ~2 std deviations, which
|
||||
# is small enough that I've just chosen the more readable option.
|
||||
_HAS_RENDER_CHARS_PAT = re.compile(r'({[{%#]|[#}%]})')
|
||||
|
||||
|
||||
def get_rendered(
|
||||
string: str,
|
||||
ctx: Dict[str, Any],
|
||||
node=None,
|
||||
capture_macros: bool = False,
|
||||
native: bool = False,
|
||||
) -> str:
|
||||
# performance optimization: if there are no jinja control characters in the
|
||||
# string, we can just return the input. Fall back to jinja if the type is
|
||||
# not a string or if native rendering is enabled (so '1' -> 1, etc...)
|
||||
# If this is desirable in the native env as well, we could handle the
|
||||
# native=True case by passing the input string to ast.literal_eval, like
|
||||
# the native renderer does.
|
||||
if (
|
||||
not native and
|
||||
isinstance(string, str) and
|
||||
_HAS_RENDER_CHARS_PAT.search(string) is None
|
||||
):
|
||||
return string
|
||||
template = get_template(
|
||||
string,
|
||||
ctx,
|
||||
node,
|
||||
capture_macros=capture_macros,
|
||||
native=native,
|
||||
)
|
||||
return render_template(template, ctx, node)
|
||||
|
||||
|
||||
def undefined_error(msg):
|
||||
def undefined_error(msg) -> NoReturn:
|
||||
raise jinja2.exceptions.UndefinedError(msg)
|
||||
|
||||
|
||||
@@ -398,3 +624,43 @@ def extract_toplevel_blocks(
|
||||
allowed_blocks=allowed_blocks,
|
||||
collect_raw_data=collect_raw_data
|
||||
)
|
||||
|
||||
|
||||
GENERIC_TEST_KWARGS_NAME = '_dbt_generic_test_kwargs'
|
||||
|
||||
|
||||
def add_rendered_test_kwargs(
|
||||
context: Dict[str, Any],
|
||||
node: Union[ParsedGenericTestNode, CompiledGenericTestNode],
|
||||
capture_macros: bool = False,
|
||||
) -> None:
|
||||
"""Render each of the test kwargs in the given context using the native
|
||||
renderer, then insert that value into the given context as the special test
|
||||
keyword arguments member.
|
||||
"""
|
||||
looks_like_func = r'^\s*(env_var|ref|var|source|doc)\s*\(.+\)\s*$'
|
||||
|
||||
def _convert_function(
|
||||
value: Any, keypath: Tuple[Union[str, int], ...]
|
||||
) -> Any:
|
||||
if isinstance(value, str):
|
||||
if keypath == ('column_name',):
|
||||
# special case: Don't render column names as native, make them
|
||||
# be strings
|
||||
return value
|
||||
|
||||
if re.match(looks_like_func, value) is not None:
|
||||
# curly braces to make rendering happy
|
||||
value = f'{{{{ {value} }}}}'
|
||||
|
||||
value = get_rendered(
|
||||
value, context, node, capture_macros=capture_macros,
|
||||
native=True
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
# The test_metadata.kwargs come from the test builder, and were set
|
||||
# when the test node was created in _parse_generic_test.
|
||||
kwargs = deep_map_render(_convert_function, node.test_metadata.kwargs)
|
||||
context[GENERIC_TEST_KWARGS_NAME] = kwargs
|
||||
|
||||
154
core/dbt/clients/jinja_static.py
Normal file
154
core/dbt/clients/jinja_static.py
Normal file
@@ -0,0 +1,154 @@
|
||||
import jinja2
|
||||
from dbt.clients.jinja import get_environment
|
||||
from dbt.exceptions import raise_compiler_error
|
||||
|
||||
|
||||
def statically_extract_macro_calls(string, ctx, db_wrapper=None):
|
||||
# set 'capture_macros' to capture undefined
|
||||
env = get_environment(None, capture_macros=True)
|
||||
parsed = env.parse(string)
|
||||
|
||||
standard_calls = ['source', 'ref', 'config']
|
||||
possible_macro_calls = []
|
||||
for func_call in parsed.find_all(jinja2.nodes.Call):
|
||||
func_name = None
|
||||
if hasattr(func_call, 'node') and hasattr(func_call.node, 'name'):
|
||||
func_name = func_call.node.name
|
||||
else:
|
||||
# func_call for dbt_utils.current_timestamp macro
|
||||
# Call(
|
||||
# node=Getattr(
|
||||
# node=Name(
|
||||
# name='dbt_utils',
|
||||
# ctx='load'
|
||||
# ),
|
||||
# attr='current_timestamp',
|
||||
# ctx='load
|
||||
# ),
|
||||
# args=[],
|
||||
# kwargs=[],
|
||||
# dyn_args=None,
|
||||
# dyn_kwargs=None
|
||||
# )
|
||||
if (hasattr(func_call, 'node') and
|
||||
hasattr(func_call.node, 'node') and
|
||||
type(func_call.node.node).__name__ == 'Name' and
|
||||
hasattr(func_call.node, 'attr')):
|
||||
package_name = func_call.node.node.name
|
||||
macro_name = func_call.node.attr
|
||||
if package_name == 'adapter':
|
||||
if macro_name == 'dispatch':
|
||||
ad_macro_calls = statically_parse_adapter_dispatch(
|
||||
func_call, ctx, db_wrapper)
|
||||
possible_macro_calls.extend(ad_macro_calls)
|
||||
else:
|
||||
# This skips calls such as adapter.parse_index
|
||||
continue
|
||||
else:
|
||||
func_name = f'{package_name}.{macro_name}'
|
||||
else:
|
||||
continue
|
||||
if not func_name:
|
||||
continue
|
||||
if func_name in standard_calls:
|
||||
continue
|
||||
elif ctx.get(func_name):
|
||||
continue
|
||||
else:
|
||||
if func_name not in possible_macro_calls:
|
||||
possible_macro_calls.append(func_name)
|
||||
|
||||
return possible_macro_calls
|
||||
|
||||
|
||||
# Call(
|
||||
# node=Getattr(
|
||||
# node=Name(
|
||||
# name='adapter',
|
||||
# ctx='load'
|
||||
# ),
|
||||
# attr='dispatch',
|
||||
# ctx='load'
|
||||
# ),
|
||||
# args=[
|
||||
# Const(value='test_pkg_and_dispatch')
|
||||
# ],
|
||||
# kwargs=[
|
||||
# Keyword(
|
||||
# key='packages',
|
||||
# value=Call(node=Getattr(node=Name(name='local_utils', ctx='load'),
|
||||
# attr='_get_utils_namespaces', ctx='load'), args=[], kwargs=[],
|
||||
# dyn_args=None, dyn_kwargs=None)
|
||||
# )
|
||||
# ],
|
||||
# dyn_args=None,
|
||||
# dyn_kwargs=None
|
||||
# )
|
||||
def statically_parse_adapter_dispatch(func_call, ctx, db_wrapper):
|
||||
possible_macro_calls = []
|
||||
# This captures an adapter.dispatch('<macro_name>') call.
|
||||
|
||||
func_name = None
|
||||
# macro_name positional argument
|
||||
if len(func_call.args) > 0:
|
||||
func_name = func_call.args[0].value
|
||||
if func_name:
|
||||
possible_macro_calls.append(func_name)
|
||||
|
||||
# packages positional argument
|
||||
macro_namespace = None
|
||||
packages_arg = None
|
||||
packages_arg_type = None
|
||||
|
||||
if len(func_call.args) > 1:
|
||||
packages_arg = func_call.args[1]
|
||||
# This can be a List or a Call
|
||||
packages_arg_type = type(func_call.args[1]).__name__
|
||||
|
||||
# keyword arguments
|
||||
if func_call.kwargs:
|
||||
for kwarg in func_call.kwargs:
|
||||
if kwarg.key == 'macro_name':
|
||||
# This will remain to enable static resolution
|
||||
if type(kwarg.value).__name__ == 'Const':
|
||||
func_name = kwarg.value.value
|
||||
possible_macro_calls.append(func_name)
|
||||
else:
|
||||
raise_compiler_error(f"The macro_name parameter ({kwarg.value.value}) "
|
||||
"to adapter.dispatch was not a string")
|
||||
elif kwarg.key == 'macro_namespace':
|
||||
# This will remain to enable static resolution
|
||||
kwarg_type = type(kwarg.value).__name__
|
||||
if kwarg_type == 'Const':
|
||||
macro_namespace = kwarg.value.value
|
||||
else:
|
||||
raise_compiler_error("The macro_namespace parameter to adapter.dispatch "
|
||||
f"is a {kwarg_type}, not a string")
|
||||
|
||||
# positional arguments
|
||||
if packages_arg:
|
||||
if packages_arg_type == 'List':
|
||||
# This will remain to enable static resolution
|
||||
packages = []
|
||||
for item in packages_arg.items:
|
||||
packages.append(item.value)
|
||||
elif packages_arg_type == 'Const':
|
||||
# This will remain to enable static resolution
|
||||
macro_namespace = packages_arg.value
|
||||
|
||||
if db_wrapper:
|
||||
macro = db_wrapper.dispatch(
|
||||
func_name,
|
||||
macro_namespace=macro_namespace
|
||||
).macro
|
||||
func_name = f'{macro.package_name}.{macro.name}'
|
||||
possible_macro_calls.append(func_name)
|
||||
else: # this is only for test/unit/test_macro_calls.py
|
||||
if macro_namespace:
|
||||
packages = [macro_namespace]
|
||||
else:
|
||||
packages = []
|
||||
for package_name in packages:
|
||||
possible_macro_calls.append(f'{package_name}.{func_name}')
|
||||
|
||||
return possible_macro_calls
|
||||
@@ -1,10 +1,13 @@
|
||||
from functools import wraps
|
||||
import functools
|
||||
import requests
|
||||
from dbt.exceptions import RegistryException
|
||||
from dbt.utils import memoized
|
||||
from dbt.logger import GLOBAL_LOGGER as logger
|
||||
from dbt.events.functions import fire_event
|
||||
from dbt.events.types import (
|
||||
RegistryProgressMakingGETRequest,
|
||||
RegistryProgressGETResponse
|
||||
)
|
||||
from dbt.utils import memoized, _connection_exception_retry as connection_exception_retry
|
||||
from dbt import deprecations
|
||||
import os
|
||||
import time
|
||||
|
||||
if os.getenv('DBT_PACKAGE_HUB_URL'):
|
||||
DEFAULT_REGISTRY_BASE_URL = os.getenv('DBT_PACKAGE_HUB_URL')
|
||||
@@ -19,54 +22,68 @@ def _get_url(url, registry_base_url=None):
|
||||
return '{}{}'.format(registry_base_url, url)
|
||||
|
||||
|
||||
def _wrap_exceptions(fn):
|
||||
@wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
max_attempts = 5
|
||||
attempt = 0
|
||||
while True:
|
||||
attempt += 1
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
except requests.exceptions.ConnectionError as exc:
|
||||
if attempt < max_attempts:
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
raise RegistryException(
|
||||
'Unable to connect to registry hub'
|
||||
) from exc
|
||||
return wrapper
|
||||
def _get_with_retries(path, registry_base_url=None):
|
||||
get_fn = functools.partial(_get, path, registry_base_url)
|
||||
return connection_exception_retry(get_fn, 5)
|
||||
|
||||
|
||||
@_wrap_exceptions
|
||||
def _get(path, registry_base_url=None):
|
||||
url = _get_url(path, registry_base_url)
|
||||
logger.debug('Making package registry request: GET {}'.format(url))
|
||||
resp = requests.get(url)
|
||||
logger.debug('Response from registry: GET {} {}'.format(url,
|
||||
resp.status_code))
|
||||
fire_event(RegistryProgressMakingGETRequest(url=url))
|
||||
resp = requests.get(url, timeout=30)
|
||||
fire_event(RegistryProgressGETResponse(url=url, resp_code=resp.status_code))
|
||||
resp.raise_for_status()
|
||||
|
||||
# It is unexpected for the content of the response to be None so if it is, raising this error
|
||||
# will cause this function to retry (if called within _get_with_retries) and hopefully get
|
||||
# a response. This seems to happen when there's an issue with the Hub.
|
||||
# See https://github.com/dbt-labs/dbt-core/issues/4577
|
||||
if resp.json() is None:
|
||||
raise requests.exceptions.ContentDecodingError(
|
||||
'Request error: The response is None', response=resp
|
||||
)
|
||||
return resp.json()
|
||||
|
||||
|
||||
def index(registry_base_url=None):
|
||||
return _get('api/v1/index.json', registry_base_url)
|
||||
return _get_with_retries('api/v1/index.json', registry_base_url)
|
||||
|
||||
|
||||
index_cached = memoized(index)
|
||||
|
||||
|
||||
def packages(registry_base_url=None):
|
||||
return _get('api/v1/packages.json', registry_base_url)
|
||||
return _get_with_retries('api/v1/packages.json', registry_base_url)
|
||||
|
||||
|
||||
def package(name, registry_base_url=None):
|
||||
return _get('api/v1/{}.json'.format(name), registry_base_url)
|
||||
response = _get_with_retries('api/v1/{}.json'.format(name), registry_base_url)
|
||||
|
||||
# Either redirectnamespace or redirectname in the JSON response indicate a redirect
|
||||
# redirectnamespace redirects based on package ownership
|
||||
# redirectname redirects based on package name
|
||||
# Both can be present at the same time, or neither. Fails gracefully to old name
|
||||
|
||||
if ('redirectnamespace' in response) or ('redirectname' in response):
|
||||
|
||||
if ('redirectnamespace' in response) and response['redirectnamespace'] is not None:
|
||||
use_namespace = response['redirectnamespace']
|
||||
else:
|
||||
use_namespace = response['namespace']
|
||||
|
||||
if ('redirectname' in response) and response['redirectname'] is not None:
|
||||
use_name = response['redirectname']
|
||||
else:
|
||||
use_name = response['name']
|
||||
|
||||
new_nwo = use_namespace + "/" + use_name
|
||||
deprecations.warn('package-redirect', old_name=name, new_name=new_nwo)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def package_version(name, version, registry_base_url=None):
|
||||
return _get('api/v1/{}/{}.json'.format(name, version), registry_base_url)
|
||||
return _get_with_retries('api/v1/{}/{}.json'.format(name, version), registry_base_url)
|
||||
|
||||
|
||||
def get_available_versions(name):
|
||||
|
||||
@@ -1,24 +1,40 @@
|
||||
import errno
|
||||
import functools
|
||||
import fnmatch
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import requests
|
||||
import stat
|
||||
from typing import (
|
||||
Type, NoReturn, List, Optional, Dict, Any, Tuple, Callable, Union
|
||||
)
|
||||
|
||||
from dbt.events.functions import fire_event
|
||||
from dbt.events.types import (
|
||||
SystemErrorRetrievingModTime, SystemCouldNotWrite, SystemExecutingCmd, SystemStdOutMsg,
|
||||
SystemStdErrMsg, SystemReportReturnCode
|
||||
)
|
||||
import dbt.exceptions
|
||||
import dbt.utils
|
||||
from dbt.utils import _connection_exception_retry as connection_exception_retry
|
||||
|
||||
from dbt.logger import GLOBAL_LOGGER as logger
|
||||
if sys.platform == 'win32':
|
||||
from ctypes import WinDLL, c_bool
|
||||
else:
|
||||
WinDLL = None
|
||||
c_bool = None
|
||||
|
||||
|
||||
def find_matching(root_path,
|
||||
relative_paths_to_search,
|
||||
file_pattern):
|
||||
def find_matching(
|
||||
root_path: str,
|
||||
relative_paths_to_search: List[str],
|
||||
file_pattern: str,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Given an absolute `root_path`, a list of relative paths to that
|
||||
absolute root path (`relative_paths_to_search`), and a `file_pattern`
|
||||
@@ -35,6 +51,8 @@ def find_matching(root_path,
|
||||
"""
|
||||
matching = []
|
||||
root_path = os.path.normpath(root_path)
|
||||
regex = fnmatch.translate(file_pattern)
|
||||
reobj = re.compile(regex, re.IGNORECASE)
|
||||
|
||||
for relative_path_to_search in relative_paths_to_search:
|
||||
absolute_path_to_search = os.path.join(
|
||||
@@ -47,18 +65,24 @@ def find_matching(root_path,
|
||||
relative_path = os.path.relpath(
|
||||
absolute_path, absolute_path_to_search
|
||||
)
|
||||
|
||||
if fnmatch.fnmatch(local_file, file_pattern):
|
||||
modification_time = 0.0
|
||||
try:
|
||||
modification_time = os.path.getmtime(absolute_path)
|
||||
except OSError:
|
||||
fire_event(SystemErrorRetrievingModTime(path=absolute_path))
|
||||
if reobj.match(local_file):
|
||||
matching.append({
|
||||
'searched_path': relative_path_to_search,
|
||||
'absolute_path': absolute_path,
|
||||
'relative_path': relative_path,
|
||||
'modification_time': modification_time,
|
||||
})
|
||||
|
||||
return matching
|
||||
|
||||
|
||||
def load_file_contents(path, strip=True):
|
||||
def load_file_contents(path: str, strip: bool = True) -> str:
|
||||
path = convert_path(path)
|
||||
with open(path, 'rb') as handle:
|
||||
to_return = handle.read().decode('utf-8')
|
||||
|
||||
@@ -68,12 +92,13 @@ def load_file_contents(path, strip=True):
|
||||
return to_return
|
||||
|
||||
|
||||
def make_directory(path):
|
||||
def make_directory(path: str) -> None:
|
||||
"""
|
||||
Make a directory and any intermediate directories that don't already
|
||||
exist. This function handles the case where two threads try to create
|
||||
a directory at once.
|
||||
"""
|
||||
path = convert_path(path)
|
||||
if not os.path.exists(path):
|
||||
# concurrent writes that try to create the same dir can fail
|
||||
try:
|
||||
@@ -86,12 +111,13 @@ def make_directory(path):
|
||||
raise e
|
||||
|
||||
|
||||
def make_file(path, contents='', overwrite=False):
|
||||
def make_file(path: str, contents: str = '', overwrite: bool = False) -> bool:
|
||||
"""
|
||||
Make a file at `path` assuming that the directory it resides in already
|
||||
exists. The file is saved with contents `contents`
|
||||
"""
|
||||
if overwrite or not os.path.exists(path):
|
||||
path = convert_path(path)
|
||||
with open(path, 'w') as fh:
|
||||
fh.write(contents)
|
||||
return True
|
||||
@@ -99,33 +125,61 @@ def make_file(path, contents='', overwrite=False):
|
||||
return False
|
||||
|
||||
|
||||
def make_symlink(source, link_path):
|
||||
def make_symlink(source: str, link_path: str) -> None:
|
||||
"""
|
||||
Create a symlink at `link_path` referring to `source`.
|
||||
"""
|
||||
if not supports_symlinks():
|
||||
dbt.exceptions.system_error('create a symbolic link')
|
||||
|
||||
return os.symlink(source, link_path)
|
||||
os.symlink(source, link_path)
|
||||
|
||||
|
||||
def supports_symlinks():
|
||||
def supports_symlinks() -> bool:
|
||||
return getattr(os, "symlink", None) is not None
|
||||
|
||||
|
||||
def write_file(path, contents=''):
|
||||
make_directory(os.path.dirname(path))
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write(str(contents))
|
||||
|
||||
def write_file(path: str, contents: str = '') -> bool:
|
||||
path = convert_path(path)
|
||||
try:
|
||||
make_directory(os.path.dirname(path))
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write(str(contents))
|
||||
except Exception as exc:
|
||||
# note that you can't just catch FileNotFound, because sometimes
|
||||
# windows apparently raises something else.
|
||||
# It's also not sufficient to look at the path length, because
|
||||
# sometimes windows fails to write paths that are less than the length
|
||||
# limit. So on windows, suppress all errors that happen from writing
|
||||
# to disk.
|
||||
if os.name == 'nt':
|
||||
# sometimes we get a winerror of 3 which means the path was
|
||||
# definitely too long, but other times we don't and it means the
|
||||
# path was just probably too long. This is probably based on the
|
||||
# windows/python version.
|
||||
if getattr(exc, 'winerror', 0) == 3:
|
||||
reason = 'Path was too long'
|
||||
else:
|
||||
reason = 'Path was possibly too long'
|
||||
# all our hard work and the path was still too long. Log and
|
||||
# continue.
|
||||
fire_event(SystemCouldNotWrite(path=path, reason=reason, exc=exc))
|
||||
else:
|
||||
raise
|
||||
return True
|
||||
|
||||
|
||||
def write_json(path, data):
|
||||
def read_json(path: str) -> Dict[str, Any]:
|
||||
return json.loads(load_file_contents(path))
|
||||
|
||||
|
||||
def write_json(path: str, data: Dict[str, Any]) -> bool:
|
||||
return write_file(path, json.dumps(data, cls=dbt.utils.JSONEncoder))
|
||||
|
||||
|
||||
def _windows_rmdir_readonly(func, path, exc):
|
||||
def _windows_rmdir_readonly(
|
||||
func: Callable[[str], Any], path: str, exc: Tuple[Any, OSError, Any]
|
||||
):
|
||||
exception_val = exc[1]
|
||||
if exception_val.errno == errno.EACCES:
|
||||
os.chmod(path, stat.S_IWUSR)
|
||||
@@ -134,7 +188,7 @@ def _windows_rmdir_readonly(func, path, exc):
|
||||
raise
|
||||
|
||||
|
||||
def resolve_path_from_base(path_to_resolve, base_path):
|
||||
def resolve_path_from_base(path_to_resolve: str, base_path: str) -> str:
|
||||
"""
|
||||
If path-to_resolve is a relative path, create an absolute path
|
||||
with base_path as the base.
|
||||
@@ -148,34 +202,109 @@ def resolve_path_from_base(path_to_resolve, base_path):
|
||||
os.path.expanduser(path_to_resolve)))
|
||||
|
||||
|
||||
def rmdir(path):
|
||||
def rmdir(path: str) -> None:
|
||||
"""
|
||||
Recursively deletes a directory. Includes an error handler to retry with
|
||||
different permissions on Windows. Otherwise, removing directories (eg.
|
||||
cloned via git) can cause rmtree to throw a PermissionError exception
|
||||
"""
|
||||
logger.debug("DEBUG** Window rmdir sys.platform: {}".format(sys.platform))
|
||||
path = convert_path(path)
|
||||
if sys.platform == 'win32':
|
||||
onerror = _windows_rmdir_readonly
|
||||
else:
|
||||
onerror = None
|
||||
|
||||
return shutil.rmtree(path, onerror=onerror)
|
||||
shutil.rmtree(path, onerror=onerror)
|
||||
|
||||
|
||||
def remove_file(path):
|
||||
return os.remove(path)
|
||||
def _win_prepare_path(path: str) -> str:
|
||||
"""Given a windows path, prepare it for use by making sure it is absolute
|
||||
and normalized.
|
||||
"""
|
||||
path = os.path.normpath(path)
|
||||
|
||||
# if a path starts with '\', splitdrive() on it will return '' for the
|
||||
# drive, but the prefix requires a drive letter. So let's add the drive
|
||||
# letter back in.
|
||||
# Unless it starts with '\\'. In that case, the path is a UNC mount point
|
||||
# and splitdrive will be fine.
|
||||
if not path.startswith('\\\\') and path.startswith('\\'):
|
||||
curdrive = os.path.splitdrive(os.getcwd())[0]
|
||||
path = curdrive + path
|
||||
|
||||
# now our path is either an absolute UNC path or relative to the current
|
||||
# directory. If it's relative, we need to make it absolute or the prefix
|
||||
# won't work. `ntpath.abspath` allegedly doesn't always play nice with long
|
||||
# paths, so do this instead.
|
||||
if not os.path.splitdrive(path)[0]:
|
||||
path = os.path.join(os.getcwd(), path)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def path_exists(path):
|
||||
def _supports_long_paths() -> bool:
|
||||
if sys.platform != 'win32':
|
||||
return True
|
||||
# Eryk Sun says to use `WinDLL('ntdll')` instead of `windll.ntdll` because
|
||||
# of pointer caching in a comment here:
|
||||
# https://stackoverflow.com/a/35097999/11262881
|
||||
# I don't know exaclty what he means, but I am inclined to believe him as
|
||||
# he's pretty active on Python windows bugs!
|
||||
try:
|
||||
dll = WinDLL('ntdll')
|
||||
except OSError: # I don't think this happens? you need ntdll to run python
|
||||
return False
|
||||
# not all windows versions have it at all
|
||||
if not hasattr(dll, 'RtlAreLongPathsEnabled'):
|
||||
return False
|
||||
# tell windows we want to get back a single unsigned byte (a bool).
|
||||
dll.RtlAreLongPathsEnabled.restype = c_bool
|
||||
return dll.RtlAreLongPathsEnabled()
|
||||
|
||||
|
||||
def convert_path(path: str) -> str:
|
||||
"""Convert a path that dbt has, which might be >260 characters long, to one
|
||||
that will be writable/readable on Windows.
|
||||
|
||||
On other platforms, this is a no-op.
|
||||
"""
|
||||
# some parts of python seem to append '\*.*' to strings, better safe than
|
||||
# sorry.
|
||||
if len(path) < 250:
|
||||
return path
|
||||
if _supports_long_paths():
|
||||
return path
|
||||
|
||||
prefix = '\\\\?\\'
|
||||
# Nothing to do
|
||||
if path.startswith(prefix):
|
||||
return path
|
||||
|
||||
path = _win_prepare_path(path)
|
||||
|
||||
# add the prefix. The check is just in case os.getcwd() does something
|
||||
# unexpected - I believe this if-state should always be True though!
|
||||
if not path.startswith(prefix):
|
||||
path = prefix + path
|
||||
return path
|
||||
|
||||
|
||||
def remove_file(path: str) -> None:
|
||||
path = convert_path(path)
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def path_exists(path: str) -> bool:
|
||||
path = convert_path(path)
|
||||
return os.path.lexists(path)
|
||||
|
||||
|
||||
def path_is_symlink(path):
|
||||
def path_is_symlink(path: str) -> bool:
|
||||
path = convert_path(path)
|
||||
return os.path.islink(path)
|
||||
|
||||
|
||||
def open_dir_cmd():
|
||||
def open_dir_cmd() -> str:
|
||||
# https://docs.python.org/2/library/sys.html#sys.platform
|
||||
if sys.platform == 'win32':
|
||||
return 'start'
|
||||
@@ -187,7 +316,9 @@ def open_dir_cmd():
|
||||
return 'xdg-open'
|
||||
|
||||
|
||||
def _handle_posix_cwd_error(exc, cwd, cmd):
|
||||
def _handle_posix_cwd_error(
|
||||
exc: OSError, cwd: str, cmd: List[str]
|
||||
) -> NoReturn:
|
||||
if exc.errno == errno.ENOENT:
|
||||
message = 'Directory does not exist'
|
||||
elif exc.errno == errno.EACCES:
|
||||
@@ -199,7 +330,9 @@ def _handle_posix_cwd_error(exc, cwd, cmd):
|
||||
raise dbt.exceptions.WorkingDirectoryError(cwd, cmd, message)
|
||||
|
||||
|
||||
def _handle_posix_cmd_error(exc, cwd, cmd):
|
||||
def _handle_posix_cmd_error(
|
||||
exc: OSError, cwd: str, cmd: List[str]
|
||||
) -> NoReturn:
|
||||
if exc.errno == errno.ENOENT:
|
||||
message = "Could not find command, ensure it is in the user's PATH"
|
||||
elif exc.errno == errno.EACCES:
|
||||
@@ -209,7 +342,7 @@ def _handle_posix_cmd_error(exc, cwd, cmd):
|
||||
raise dbt.exceptions.ExecutableError(cwd, cmd, message)
|
||||
|
||||
|
||||
def _handle_posix_error(exc, cwd, cmd):
|
||||
def _handle_posix_error(exc: OSError, cwd: str, cmd: List[str]) -> NoReturn:
|
||||
"""OSError handling for posix systems.
|
||||
|
||||
Some things that could happen to trigger an OSError:
|
||||
@@ -236,8 +369,8 @@ def _handle_posix_error(exc, cwd, cmd):
|
||||
_handle_posix_cmd_error(exc, cwd, cmd)
|
||||
|
||||
|
||||
def _handle_windows_error(exc, cwd, cmd):
|
||||
cls = dbt.exceptions.CommandError
|
||||
def _handle_windows_error(exc: OSError, cwd: str, cmd: List[str]) -> NoReturn:
|
||||
cls: Type[dbt.exceptions.Exception] = dbt.exceptions.CommandError
|
||||
if exc.errno == errno.ENOENT:
|
||||
message = ("Could not find command, ensure it is in the user's PATH "
|
||||
"and that the user has permissions to run it")
|
||||
@@ -256,7 +389,7 @@ def _handle_windows_error(exc, cwd, cmd):
|
||||
raise cls(cwd, cmd, message)
|
||||
|
||||
|
||||
def _interpret_oserror(exc, cwd, cmd):
|
||||
def _interpret_oserror(exc: OSError, cwd: str, cmd: List[str]) -> NoReturn:
|
||||
"""Interpret an OSError exc and raise the appropriate dbt exception.
|
||||
|
||||
"""
|
||||
@@ -275,8 +408,10 @@ def _interpret_oserror(exc, cwd, cmd):
|
||||
)
|
||||
|
||||
|
||||
def run_cmd(cwd, cmd, env=None):
|
||||
logger.debug('Executing "{}"'.format(' '.join(cmd)))
|
||||
def run_cmd(
|
||||
cwd: str, cmd: List[str], env: Optional[Dict[str, Any]] = None
|
||||
) -> Tuple[bytes, bytes]:
|
||||
fire_event(SystemExecutingCmd(cmd=cmd))
|
||||
if len(cmd) == 0:
|
||||
raise dbt.exceptions.CommandError(cwd, cmd)
|
||||
|
||||
@@ -288,6 +423,9 @@ def run_cmd(cwd, cmd, env=None):
|
||||
full_env.update(env)
|
||||
|
||||
try:
|
||||
exe_pth = shutil.which(cmd[0])
|
||||
if exe_pth:
|
||||
cmd = [os.path.abspath(exe_pth)] + list(cmd[1:])
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
@@ -299,25 +437,38 @@ def run_cmd(cwd, cmd, env=None):
|
||||
except OSError as exc:
|
||||
_interpret_oserror(exc, cwd, cmd)
|
||||
|
||||
logger.debug('STDOUT: "{}"'.format(out))
|
||||
logger.debug('STDERR: "{}"'.format(err))
|
||||
fire_event(SystemStdOutMsg(bmsg=out))
|
||||
fire_event(SystemStdErrMsg(bmsg=err))
|
||||
|
||||
if proc.returncode != 0:
|
||||
logger.debug('command return code={}'.format(proc.returncode))
|
||||
fire_event(SystemReportReturnCode(returncode=proc.returncode))
|
||||
raise dbt.exceptions.CommandResultError(cwd, cmd, proc.returncode,
|
||||
out, err)
|
||||
|
||||
return out, err
|
||||
|
||||
|
||||
def download(url, path):
|
||||
response = requests.get(url)
|
||||
def download_with_retries(
|
||||
url: str, path: str, timeout: Optional[Union[float, tuple]] = None
|
||||
) -> None:
|
||||
download_fn = functools.partial(download, url, path, timeout)
|
||||
connection_exception_retry(download_fn, 5)
|
||||
|
||||
|
||||
def download(
|
||||
url: str, path: str, timeout: Optional[Union[float, tuple]] = None
|
||||
) -> None:
|
||||
path = convert_path(path)
|
||||
connection_timeout = timeout or float(os.getenv('DBT_HTTP_TIMEOUT', 10))
|
||||
response = requests.get(url, timeout=connection_timeout)
|
||||
with open(path, 'wb') as handle:
|
||||
for block in response.iter_content(1024 * 64):
|
||||
handle.write(block)
|
||||
|
||||
|
||||
def rename(from_path, to_path, force=False):
|
||||
def rename(from_path: str, to_path: str, force: bool = False) -> None:
|
||||
from_path = convert_path(from_path)
|
||||
to_path = convert_path(to_path)
|
||||
is_symlink = path_is_symlink(to_path)
|
||||
|
||||
if os.path.exists(to_path) and force:
|
||||
@@ -329,9 +480,12 @@ def rename(from_path, to_path, force=False):
|
||||
shutil.move(from_path, to_path)
|
||||
|
||||
|
||||
def untar_package(tar_path, dest_dir, rename_to=None):
|
||||
def untar_package(
|
||||
tar_path: str, dest_dir: str, rename_to: Optional[str] = None
|
||||
) -> None:
|
||||
tar_path = convert_path(tar_path)
|
||||
tar_dir_name = None
|
||||
with tarfile.open(tar_path, 'r') as tarball:
|
||||
with tarfile.open(tar_path, 'r:gz') as tarball:
|
||||
tarball.extractall(dest_dir)
|
||||
tar_dir_name = os.path.commonprefix(tarball.getnames())
|
||||
if rename_to:
|
||||
@@ -366,6 +520,8 @@ def move(src, dst):
|
||||
This is almost identical to the real shutil.move, except it uses our rmtree
|
||||
and skips handling non-windows OSes since the existing one works ok there.
|
||||
"""
|
||||
src = convert_path(src)
|
||||
dst = convert_path(dst)
|
||||
if os.name != 'nt':
|
||||
return shutil.move(src, dst)
|
||||
|
||||
@@ -400,4 +556,5 @@ def rmtree(path):
|
||||
"""Recursively remove path. On permissions errors on windows, try to remove
|
||||
the read-only flag and try again.
|
||||
"""
|
||||
path = convert_path(path)
|
||||
return shutil.rmtree(path, onerror=chmod_and_retry)
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import dbt.exceptions
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
import yaml
|
||||
import yaml.scanner
|
||||
|
||||
# the C version is faster, but it doesn't always exist
|
||||
try:
|
||||
from yaml import (
|
||||
CLoader as Loader,
|
||||
CSafeLoader as SafeLoader,
|
||||
CDumper as Dumper
|
||||
)
|
||||
except ImportError:
|
||||
from yaml import ( # type: ignore # noqa: F401
|
||||
Loader, SafeLoader, Dumper
|
||||
)
|
||||
|
||||
|
||||
YAML_ERROR_MESSAGE = """
|
||||
@@ -44,9 +55,13 @@ def contextualized_yaml_error(raw_contents, error):
|
||||
raw_error=error)
|
||||
|
||||
|
||||
def safe_load(contents) -> Optional[Dict[str, Any]]:
|
||||
return yaml.load(contents, Loader=SafeLoader)
|
||||
|
||||
|
||||
def load_yaml_text(contents):
|
||||
try:
|
||||
return yaml.safe_load(contents)
|
||||
return safe_load(contents)
|
||||
except (yaml.scanner.ScannerError, yaml.YAMLError) as e:
|
||||
if hasattr(e, 'problem_mark'):
|
||||
error = contextualized_yaml_error(contents, e)
|
||||
|
||||
@@ -1,34 +1,47 @@
|
||||
import itertools
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import List, Dict, Any, Tuple, cast, Optional
|
||||
|
||||
import dbt.utils
|
||||
import dbt.include
|
||||
import dbt.tracking
|
||||
import networkx as nx # type: ignore
|
||||
import pickle
|
||||
import sqlparse
|
||||
|
||||
from dbt.node_types import NodeType
|
||||
from dbt.linker import Linker
|
||||
|
||||
import dbt.context.runtime
|
||||
import dbt.contracts.project
|
||||
import dbt.exceptions
|
||||
import dbt.flags
|
||||
import dbt.config
|
||||
from dbt.contracts.graph.compiled import InjectedCTE, COMPILED_TYPES
|
||||
from dbt import flags
|
||||
from dbt.adapters.factory import get_adapter
|
||||
from dbt.clients import jinja
|
||||
from dbt.clients.system import make_directory
|
||||
from dbt.context.providers import generate_runtime_model_context
|
||||
from dbt.contracts.graph.manifest import Manifest, UniqueID
|
||||
from dbt.contracts.graph.compiled import (
|
||||
COMPILED_TYPES,
|
||||
CompiledGenericTestNode,
|
||||
GraphMemberNode,
|
||||
InjectedCTE,
|
||||
ManifestNode,
|
||||
NonSourceCompiledNode,
|
||||
)
|
||||
from dbt.contracts.graph.parsed import ParsedNode
|
||||
|
||||
from dbt.logger import GLOBAL_LOGGER as logger
|
||||
from dbt.exceptions import (
|
||||
dependency_not_found,
|
||||
InternalException,
|
||||
RuntimeException,
|
||||
)
|
||||
from dbt.graph import Graph
|
||||
from dbt.events.functions import fire_event
|
||||
from dbt.events.types import FoundStats, CompilingNode, WritingInjectedSQLForNode
|
||||
from dbt.node_types import NodeType
|
||||
from dbt.events.format import pluralize
|
||||
import dbt.tracking
|
||||
|
||||
graph_file_name = 'graph.gpickle'
|
||||
|
||||
|
||||
def _compiled_type_for(model: ParsedNode):
|
||||
if model.resource_type not in COMPILED_TYPES:
|
||||
raise dbt.exceptions.InternalException(
|
||||
'Asked to compile {} node, but it has no compiled form'
|
||||
.format(model.resource_type)
|
||||
if type(model) not in COMPILED_TYPES:
|
||||
raise InternalException(
|
||||
f'Asked to compile {type(model)} node, but it has no compiled form'
|
||||
)
|
||||
return COMPILED_TYPES[model.resource_type]
|
||||
return COMPILED_TYPES[type(model)]
|
||||
|
||||
|
||||
def print_compile_stats(stats):
|
||||
@@ -41,17 +54,49 @@ def print_compile_stats(stats):
|
||||
NodeType.Operation: 'operation',
|
||||
NodeType.Seed: 'seed file',
|
||||
NodeType.Source: 'source',
|
||||
NodeType.Exposure: 'exposure',
|
||||
NodeType.Metric: 'metric'
|
||||
}
|
||||
|
||||
results = {k: 0 for k in names.keys()}
|
||||
results.update(stats)
|
||||
|
||||
# create tracking event for resource_counts
|
||||
if dbt.tracking.active_user is not None:
|
||||
resource_counts = {k.pluralize(): v for k, v in results.items()}
|
||||
dbt.tracking.track_resource_counts(resource_counts)
|
||||
|
||||
stat_line = ", ".join([
|
||||
dbt.utils.pluralize(ct, names.get(t)) for t, ct in results.items()
|
||||
pluralize(ct, names.get(t)) for t, ct in results.items()
|
||||
if t in names
|
||||
])
|
||||
|
||||
logger.info("Found {}".format(stat_line))
|
||||
fire_event(FoundStats(stat_line=stat_line))
|
||||
|
||||
|
||||
def _node_enabled(node: ManifestNode):
|
||||
# Disabled models are already excluded from the manifest
|
||||
if node.resource_type == NodeType.Test and not node.config.enabled:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def _generate_stats(manifest: Manifest):
|
||||
stats: Dict[NodeType, int] = defaultdict(int)
|
||||
for node in manifest.nodes.values():
|
||||
if _node_enabled(node):
|
||||
stats[node.resource_type] += 1
|
||||
|
||||
for source in manifest.sources.values():
|
||||
stats[source.resource_type] += 1
|
||||
for exposure in manifest.exposures.values():
|
||||
stats[exposure.resource_type] += 1
|
||||
for metric in manifest.metrics.values():
|
||||
stats[metric.resource_type] += 1
|
||||
for macro in manifest.macros.values():
|
||||
stats[macro.resource_type] += 1
|
||||
return stats
|
||||
|
||||
|
||||
def _add_prepended_cte(prepended_ctes, new_cte):
|
||||
@@ -67,39 +112,59 @@ def _extend_prepended_ctes(prepended_ctes, new_prepended_ctes):
|
||||
_add_prepended_cte(prepended_ctes, new_cte)
|
||||
|
||||
|
||||
def prepend_ctes(model, manifest):
|
||||
model, _, manifest = recursively_prepend_ctes(model, manifest)
|
||||
def _get_tests_for_node(manifest: Manifest, unique_id: UniqueID) -> List[UniqueID]:
|
||||
""" Get a list of tests that depend on the node with the
|
||||
provided unique id """
|
||||
|
||||
return (model, manifest)
|
||||
tests = []
|
||||
if unique_id in manifest.child_map:
|
||||
for child_unique_id in manifest.child_map[unique_id]:
|
||||
if child_unique_id.startswith('test.'):
|
||||
tests.append(child_unique_id)
|
||||
|
||||
return tests
|
||||
|
||||
|
||||
def recursively_prepend_ctes(model, manifest):
|
||||
if model.extra_ctes_injected:
|
||||
return (model, model.extra_ctes, manifest)
|
||||
class Linker:
|
||||
def __init__(self, data=None):
|
||||
if data is None:
|
||||
data = {}
|
||||
self.graph = nx.DiGraph(**data)
|
||||
|
||||
if dbt.flags.STRICT_MODE:
|
||||
if not isinstance(model, tuple(COMPILED_TYPES.values())):
|
||||
raise dbt.exceptions.InternalException(
|
||||
'Bad model type: {}'.format(type(model))
|
||||
)
|
||||
def edges(self):
|
||||
return self.graph.edges()
|
||||
|
||||
prepended_ctes = []
|
||||
def nodes(self):
|
||||
return self.graph.nodes()
|
||||
|
||||
for cte in model.extra_ctes:
|
||||
cte_id = cte.id
|
||||
cte_to_add = manifest.nodes.get(cte_id)
|
||||
cte_to_add, new_prepended_ctes, manifest = recursively_prepend_ctes(
|
||||
cte_to_add, manifest)
|
||||
_extend_prepended_ctes(prepended_ctes, new_prepended_ctes)
|
||||
new_cte_name = '__dbt__CTE__{}'.format(cte_to_add.name)
|
||||
sql = ' {} as (\n{}\n)'.format(new_cte_name, cte_to_add.compiled_sql)
|
||||
_add_prepended_cte(prepended_ctes, InjectedCTE(id=cte_id, sql=sql))
|
||||
def find_cycles(self):
|
||||
try:
|
||||
cycle = nx.find_cycle(self.graph)
|
||||
except nx.NetworkXNoCycle:
|
||||
return None
|
||||
else:
|
||||
# cycles is a List[Tuple[str, ...]]
|
||||
return " --> ".join(c[0] for c in cycle)
|
||||
|
||||
model.prepend_ctes(prepended_ctes)
|
||||
def dependency(self, node1, node2):
|
||||
"indicate that node1 depends on node2"
|
||||
self.graph.add_node(node1)
|
||||
self.graph.add_node(node2)
|
||||
self.graph.add_edge(node2, node1)
|
||||
|
||||
manifest.update_node(model)
|
||||
def add_node(self, node):
|
||||
self.graph.add_node(node)
|
||||
|
||||
return (model, prepended_ctes, manifest)
|
||||
def write_graph(self, outfile: str, manifest: Manifest):
|
||||
"""Write the graph to a gpickle file. Before doing so, serialize and
|
||||
include all nodes in their corresponding graph entries.
|
||||
"""
|
||||
out_graph = self.graph.copy()
|
||||
for node_id in self.graph:
|
||||
data = manifest.expect(node_id).to_dict(omit_none=True)
|
||||
out_graph.add_node(node_id, **data)
|
||||
with open(outfile, 'wb') as outfh:
|
||||
pickle.dump(out_graph, outfh, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
|
||||
|
||||
class Compiler:
|
||||
@@ -107,159 +172,388 @@ class Compiler:
|
||||
self.config = config
|
||||
|
||||
def initialize(self):
|
||||
dbt.clients.system.make_directory(self.config.target_path)
|
||||
dbt.clients.system.make_directory(self.config.modules_path)
|
||||
make_directory(self.config.target_path)
|
||||
make_directory(self.config.packages_install_path)
|
||||
|
||||
def compile_node(self, node, manifest, extra_context=None):
|
||||
# creates a ModelContext which is converted to
|
||||
# a dict for jinja rendering of SQL
|
||||
def _create_node_context(
|
||||
self,
|
||||
node: NonSourceCompiledNode,
|
||||
manifest: Manifest,
|
||||
extra_context: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
|
||||
context = generate_runtime_model_context(
|
||||
node, self.config, manifest
|
||||
)
|
||||
context.update(extra_context)
|
||||
if isinstance(node, CompiledGenericTestNode):
|
||||
# for test nodes, add a special keyword args value to the context
|
||||
jinja.add_rendered_test_kwargs(context, node)
|
||||
|
||||
return context
|
||||
|
||||
def add_ephemeral_prefix(self, name: str):
|
||||
adapter = get_adapter(self.config)
|
||||
relation_cls = adapter.Relation
|
||||
return relation_cls.add_ephemeral_prefix(name)
|
||||
|
||||
def _get_relation_name(self, node: ParsedNode):
|
||||
relation_name = None
|
||||
if node.is_relational and not node.is_ephemeral_model:
|
||||
adapter = get_adapter(self.config)
|
||||
relation_cls = adapter.Relation
|
||||
relation_name = str(relation_cls.create_from(self.config, node))
|
||||
return relation_name
|
||||
|
||||
def _inject_ctes_into_sql(self, sql: str, ctes: List[InjectedCTE]) -> str:
|
||||
"""
|
||||
`ctes` is a list of InjectedCTEs like:
|
||||
|
||||
[
|
||||
InjectedCTE(
|
||||
id="cte_id_1",
|
||||
sql="__dbt__cte__ephemeral as (select * from table)",
|
||||
),
|
||||
InjectedCTE(
|
||||
id="cte_id_2",
|
||||
sql="__dbt__cte__events as (select id, type from events)",
|
||||
),
|
||||
]
|
||||
|
||||
Given `sql` like:
|
||||
|
||||
"with internal_cte as (select * from sessions)
|
||||
select * from internal_cte"
|
||||
|
||||
This will spit out:
|
||||
|
||||
"with __dbt__cte__ephemeral as (select * from table),
|
||||
__dbt__cte__events as (select id, type from events),
|
||||
with internal_cte as (select * from sessions)
|
||||
select * from internal_cte"
|
||||
|
||||
(Whitespace enhanced for readability.)
|
||||
"""
|
||||
if len(ctes) == 0:
|
||||
return sql
|
||||
|
||||
parsed_stmts = sqlparse.parse(sql)
|
||||
parsed = parsed_stmts[0]
|
||||
|
||||
with_stmt = None
|
||||
for token in parsed.tokens:
|
||||
if token.is_keyword and token.normalized == 'WITH':
|
||||
with_stmt = token
|
||||
break
|
||||
|
||||
if with_stmt is None:
|
||||
# no with stmt, add one, and inject CTEs right at the beginning
|
||||
first_token = parsed.token_first()
|
||||
with_stmt = sqlparse.sql.Token(sqlparse.tokens.Keyword, 'with')
|
||||
parsed.insert_before(first_token, with_stmt)
|
||||
else:
|
||||
# stmt exists, add a comma (which will come after injected CTEs)
|
||||
trailing_comma = sqlparse.sql.Token(
|
||||
sqlparse.tokens.Punctuation, ','
|
||||
)
|
||||
parsed.insert_after(with_stmt, trailing_comma)
|
||||
|
||||
token = sqlparse.sql.Token(
|
||||
sqlparse.tokens.Keyword,
|
||||
", ".join(c.sql for c in ctes)
|
||||
)
|
||||
parsed.insert_after(with_stmt, token)
|
||||
|
||||
return str(parsed)
|
||||
|
||||
def _recursively_prepend_ctes(
|
||||
self,
|
||||
model: NonSourceCompiledNode,
|
||||
manifest: Manifest,
|
||||
extra_context: Optional[Dict[str, Any]],
|
||||
) -> Tuple[NonSourceCompiledNode, List[InjectedCTE]]:
|
||||
"""This method is called by the 'compile_node' method. Starting
|
||||
from the node that it is passed in, it will recursively call
|
||||
itself using the 'extra_ctes'. The 'ephemeral' models do
|
||||
not produce SQL that is executed directly, instead they
|
||||
are rolled up into the models that refer to them by
|
||||
inserting CTEs into the SQL.
|
||||
"""
|
||||
if model.compiled_sql is None:
|
||||
raise RuntimeException(
|
||||
'Cannot inject ctes into an unparsed node', model
|
||||
)
|
||||
if model.extra_ctes_injected:
|
||||
return (model, model.extra_ctes)
|
||||
|
||||
# Just to make it plain that nothing is actually injected for this case
|
||||
if not model.extra_ctes:
|
||||
model.extra_ctes_injected = True
|
||||
manifest.update_node(model)
|
||||
return (model, model.extra_ctes)
|
||||
|
||||
# This stores the ctes which will all be recursively
|
||||
# gathered and then "injected" into the model.
|
||||
prepended_ctes: List[InjectedCTE] = []
|
||||
|
||||
# extra_ctes are added to the model by
|
||||
# RuntimeRefResolver.create_relation, which adds an
|
||||
# extra_cte for every model relation which is an
|
||||
# ephemeral model.
|
||||
for cte in model.extra_ctes:
|
||||
if cte.id not in manifest.nodes:
|
||||
raise InternalException(
|
||||
f'During compilation, found a cte reference that '
|
||||
f'could not be resolved: {cte.id}'
|
||||
)
|
||||
cte_model = manifest.nodes[cte.id]
|
||||
|
||||
if not cte_model.is_ephemeral_model:
|
||||
raise InternalException(f'{cte.id} is not ephemeral')
|
||||
|
||||
# This model has already been compiled, so it's been
|
||||
# through here before
|
||||
if getattr(cte_model, 'compiled', False):
|
||||
assert isinstance(cte_model, tuple(COMPILED_TYPES.values()))
|
||||
cte_model = cast(NonSourceCompiledNode, cte_model)
|
||||
new_prepended_ctes = cte_model.extra_ctes
|
||||
|
||||
# if the cte_model isn't compiled, i.e. first time here
|
||||
else:
|
||||
# This is an ephemeral parsed model that we can compile.
|
||||
# Compile and update the node
|
||||
cte_model = self._compile_node(
|
||||
cte_model, manifest, extra_context)
|
||||
# recursively call this method
|
||||
cte_model, new_prepended_ctes = \
|
||||
self._recursively_prepend_ctes(
|
||||
cte_model, manifest, extra_context
|
||||
)
|
||||
# Save compiled SQL file and sync manifest
|
||||
self._write_node(cte_model)
|
||||
manifest.sync_update_node(cte_model)
|
||||
|
||||
_extend_prepended_ctes(prepended_ctes, new_prepended_ctes)
|
||||
|
||||
new_cte_name = self.add_ephemeral_prefix(cte_model.name)
|
||||
rendered_sql = (
|
||||
cte_model._pre_injected_sql or cte_model.compiled_sql
|
||||
)
|
||||
sql = f' {new_cte_name} as (\n{rendered_sql}\n)'
|
||||
|
||||
_add_prepended_cte(prepended_ctes, InjectedCTE(id=cte.id, sql=sql))
|
||||
|
||||
injected_sql = self._inject_ctes_into_sql(
|
||||
model.compiled_sql,
|
||||
prepended_ctes,
|
||||
)
|
||||
model._pre_injected_sql = model.compiled_sql
|
||||
model.compiled_sql = injected_sql
|
||||
model.extra_ctes_injected = True
|
||||
model.extra_ctes = prepended_ctes
|
||||
model.validate(model.to_dict(omit_none=True))
|
||||
|
||||
manifest.update_node(model)
|
||||
|
||||
return model, prepended_ctes
|
||||
|
||||
# creates a compiled_node from the ManifestNode passed in,
|
||||
# creates a "context" dictionary for jinja rendering,
|
||||
# and then renders the "compiled_sql" using the node, the
|
||||
# raw_sql and the context.
|
||||
def _compile_node(
|
||||
self,
|
||||
node: ManifestNode,
|
||||
manifest: Manifest,
|
||||
extra_context: Optional[Dict[str, Any]] = None,
|
||||
) -> NonSourceCompiledNode:
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
|
||||
logger.debug("Compiling {}".format(node.unique_id))
|
||||
fire_event(CompilingNode(unique_id=node.unique_id))
|
||||
|
||||
data = node.to_dict()
|
||||
data = node.to_dict(omit_none=True)
|
||||
data.update({
|
||||
'compiled': False,
|
||||
'compiled_sql': None,
|
||||
'extra_ctes_injected': False,
|
||||
'extra_ctes': [],
|
||||
'injected_sql': None,
|
||||
})
|
||||
compiled_node = _compiled_type_for(node).from_dict(data)
|
||||
|
||||
context = dbt.context.runtime.generate(
|
||||
compiled_node, self.config, manifest)
|
||||
context.update(extra_context)
|
||||
context = self._create_node_context(
|
||||
compiled_node, manifest, extra_context
|
||||
)
|
||||
|
||||
compiled_node.compiled_sql = dbt.clients.jinja.get_rendered(
|
||||
compiled_node.compiled_sql = jinja.get_rendered(
|
||||
node.raw_sql,
|
||||
context,
|
||||
node)
|
||||
node,
|
||||
)
|
||||
|
||||
compiled_node.relation_name = self._get_relation_name(node)
|
||||
|
||||
compiled_node.compiled = True
|
||||
|
||||
injected_node, _ = prepend_ctes(compiled_node, manifest)
|
||||
return compiled_node
|
||||
|
||||
should_wrap = {NodeType.Test, NodeType.Operation}
|
||||
if injected_node.resource_type in should_wrap:
|
||||
# data tests get wrapped in count(*)
|
||||
# TODO : move this somewhere more reasonable
|
||||
if 'data' in injected_node.tags and \
|
||||
injected_node.resource_type == NodeType.Test:
|
||||
injected_node.wrapped_sql = (
|
||||
"select count(*) as errors "
|
||||
"from (\n{test_sql}\n) sbq").format(
|
||||
test_sql=injected_node.injected_sql)
|
||||
else:
|
||||
# don't wrap schema tests or analyses.
|
||||
injected_node.wrapped_sql = injected_node.injected_sql
|
||||
|
||||
elif injected_node.resource_type == NodeType.Snapshot:
|
||||
# unfortunately we do everything automagically for
|
||||
# snapshots. in the future it'd be nice to generate
|
||||
# the SQL at the parser level.
|
||||
pass
|
||||
|
||||
elif(injected_node.resource_type == NodeType.Model and
|
||||
injected_node.get_materialization() == 'ephemeral'):
|
||||
pass
|
||||
|
||||
else:
|
||||
injected_node.wrapped_sql = None
|
||||
|
||||
return injected_node
|
||||
|
||||
def write_graph_file(self, linker, manifest):
|
||||
def write_graph_file(self, linker: Linker, manifest: Manifest):
|
||||
filename = graph_file_name
|
||||
graph_path = os.path.join(self.config.target_path, filename)
|
||||
if dbt.flags.WRITE_JSON:
|
||||
if flags.WRITE_JSON:
|
||||
linker.write_graph(graph_path, manifest)
|
||||
|
||||
def link_node(self, linker, node, manifest):
|
||||
def link_node(
|
||||
self, linker: Linker, node: GraphMemberNode, manifest: Manifest
|
||||
):
|
||||
linker.add_node(node.unique_id)
|
||||
|
||||
for dependency in node.depends_on_nodes:
|
||||
if manifest.nodes.get(dependency):
|
||||
if dependency in manifest.nodes:
|
||||
linker.dependency(
|
||||
node.unique_id,
|
||||
(manifest.nodes.get(dependency).unique_id))
|
||||
(manifest.nodes[dependency].unique_id)
|
||||
)
|
||||
elif dependency in manifest.sources:
|
||||
linker.dependency(
|
||||
node.unique_id,
|
||||
(manifest.sources[dependency].unique_id)
|
||||
)
|
||||
else:
|
||||
dbt.exceptions.dependency_not_found(node, dependency)
|
||||
dependency_not_found(node, dependency)
|
||||
|
||||
def link_graph(self, linker, manifest):
|
||||
def link_graph(self, linker: Linker, manifest: Manifest, add_test_edges: bool = False):
|
||||
for source in manifest.sources.values():
|
||||
linker.add_node(source.unique_id)
|
||||
for node in manifest.nodes.values():
|
||||
self.link_node(linker, node, manifest)
|
||||
for exposure in manifest.exposures.values():
|
||||
self.link_node(linker, exposure, manifest)
|
||||
for metric in manifest.metrics.values():
|
||||
self.link_node(linker, metric, manifest)
|
||||
|
||||
cycle = linker.find_cycles()
|
||||
|
||||
if cycle:
|
||||
raise RuntimeError("Found a cycle: {}".format(cycle))
|
||||
|
||||
def compile(self, manifest, write=True):
|
||||
if add_test_edges:
|
||||
manifest.build_parent_and_child_maps()
|
||||
self.add_test_edges(linker, manifest)
|
||||
|
||||
def add_test_edges(self, linker: Linker, manifest: Manifest) -> None:
|
||||
""" This method adds additional edges to the DAG. For a given non-test
|
||||
executable node, add an edge from an upstream test to the given node if
|
||||
the set of nodes the test depends on is a subset of the upstream nodes
|
||||
for the given node. """
|
||||
|
||||
# Given a graph:
|
||||
# model1 --> model2 --> model3
|
||||
# | |
|
||||
# | \/
|
||||
# \/ test 2
|
||||
# test1
|
||||
#
|
||||
# Produce the following graph:
|
||||
# model1 --> model2 --> model3
|
||||
# | /\ | /\ /\
|
||||
# | | \/ | |
|
||||
# \/ | test2 ----| |
|
||||
# test1 ----|---------------|
|
||||
|
||||
for node_id in linker.graph:
|
||||
# If node is executable (in manifest.nodes) and does _not_
|
||||
# represent a test, continue.
|
||||
if (
|
||||
node_id in manifest.nodes and
|
||||
manifest.nodes[node_id].resource_type != NodeType.Test
|
||||
):
|
||||
# Get *everything* upstream of the node
|
||||
all_upstream_nodes = nx.traversal.bfs_tree(
|
||||
linker.graph, node_id, reverse=True
|
||||
)
|
||||
# Get the set of upstream nodes not including the current node.
|
||||
upstream_nodes = set([
|
||||
n for n in all_upstream_nodes if n != node_id
|
||||
])
|
||||
|
||||
# Get all tests that depend on any upstream nodes.
|
||||
upstream_tests = []
|
||||
for upstream_node in upstream_nodes:
|
||||
upstream_tests += _get_tests_for_node(
|
||||
manifest,
|
||||
upstream_node
|
||||
)
|
||||
|
||||
for upstream_test in upstream_tests:
|
||||
# Get the set of all nodes that the test depends on
|
||||
# including the upstream_node itself. This is necessary
|
||||
# because tests can depend on multiple nodes (ex:
|
||||
# relationship tests). Test nodes do not distinguish
|
||||
# between what node the test is "testing" and what
|
||||
# node(s) it depends on.
|
||||
test_depends_on = set(
|
||||
manifest.nodes[upstream_test].depends_on_nodes
|
||||
)
|
||||
|
||||
# If the set of nodes that an upstream test depends on
|
||||
# is a subset of all upstream nodes of the current node,
|
||||
# add an edge from the upstream test to the current node.
|
||||
if (test_depends_on.issubset(upstream_nodes)):
|
||||
linker.graph.add_edge(
|
||||
upstream_test,
|
||||
node_id
|
||||
)
|
||||
|
||||
def compile(self, manifest: Manifest, write=True, add_test_edges=False) -> Graph:
|
||||
self.initialize()
|
||||
linker = Linker()
|
||||
|
||||
self.link_graph(linker, manifest)
|
||||
self.link_graph(linker, manifest, add_test_edges)
|
||||
|
||||
stats = defaultdict(int)
|
||||
|
||||
for node_name, node in itertools.chain(
|
||||
manifest.nodes.items(),
|
||||
manifest.macros.items()):
|
||||
stats[node.resource_type] += 1
|
||||
stats = _generate_stats(manifest)
|
||||
|
||||
if write:
|
||||
self.write_graph_file(linker, manifest)
|
||||
print_compile_stats(stats)
|
||||
|
||||
return linker
|
||||
return Graph(linker.graph)
|
||||
|
||||
# writes the "compiled_sql" into the target/compiled directory
|
||||
def _write_node(self, node: NonSourceCompiledNode) -> ManifestNode:
|
||||
if (not node.extra_ctes_injected or
|
||||
node.resource_type == NodeType.Snapshot):
|
||||
return node
|
||||
fire_event(WritingInjectedSQLForNode(unique_id=node.unique_id))
|
||||
|
||||
def compile_manifest(config, manifest, write=True):
|
||||
compiler = Compiler(config)
|
||||
compiler.initialize()
|
||||
return compiler.compile(manifest, write=write)
|
||||
if node.compiled_sql:
|
||||
node.compiled_path = node.write_node(
|
||||
self.config.target_path,
|
||||
'compiled',
|
||||
node.compiled_sql
|
||||
)
|
||||
return node
|
||||
|
||||
def compile_node(
|
||||
self,
|
||||
node: ManifestNode,
|
||||
manifest: Manifest,
|
||||
extra_context: Optional[Dict[str, Any]] = None,
|
||||
write: bool = True,
|
||||
) -> NonSourceCompiledNode:
|
||||
"""This is the main entry point into this code. It's called by
|
||||
CompileRunner.compile, GenericRPCRunner.compile, and
|
||||
RunTask.get_hook_sql. It calls '_compile_node' to convert
|
||||
the node into a compiled node, and then calls the
|
||||
recursive method to "prepend" the ctes.
|
||||
"""
|
||||
node = self._compile_node(node, manifest, extra_context)
|
||||
|
||||
def _is_writable(node):
|
||||
if not node.injected_sql:
|
||||
return False
|
||||
|
||||
if node.resource_type == NodeType.Snapshot:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def compile_node(adapter, config, node, manifest, extra_context, write=True):
|
||||
compiler = Compiler(config)
|
||||
node = compiler.compile_node(node, manifest, extra_context)
|
||||
node = _inject_runtime_config(adapter, node, extra_context)
|
||||
|
||||
if write and _is_writable(node):
|
||||
logger.debug('Writing injected SQL for node "{}"'.format(
|
||||
node.unique_id))
|
||||
|
||||
written_path = dbt.writer.write_node(
|
||||
node,
|
||||
config.target_path,
|
||||
'compiled',
|
||||
node.injected_sql)
|
||||
|
||||
node.build_path = written_path
|
||||
|
||||
return node
|
||||
|
||||
|
||||
def _inject_runtime_config(adapter, node, extra_context):
|
||||
wrapped_sql = node.wrapped_sql
|
||||
context = _node_context(adapter, node)
|
||||
context.update(extra_context)
|
||||
sql = dbt.clients.jinja.get_rendered(wrapped_sql, context)
|
||||
node.wrapped_sql = sql
|
||||
return node
|
||||
|
||||
|
||||
def _node_context(adapter, node):
|
||||
return {
|
||||
"run_started_at": dbt.tracking.active_user.run_started_at,
|
||||
"invocation_id": dbt.tracking.active_user.invocation_id,
|
||||
}
|
||||
node, _ = self._recursively_prepend_ctes(
|
||||
node, manifest, extra_context
|
||||
)
|
||||
if write:
|
||||
self._write_node(node)
|
||||
return node
|
||||
|
||||
1
core/dbt/config/README.md
Normal file
1
core/dbt/config/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Config README
|
||||
@@ -1,5 +1,4 @@
|
||||
# all these are just exports, they need "noqa" so flake8 will not complain.
|
||||
from .profile import Profile, PROFILES_DIR, read_user_config # noqa
|
||||
from .project import Project # noqa
|
||||
from .runtime import RuntimeConfig # noqa
|
||||
from .renderer import ConfigRenderer # noqa
|
||||
from .profile import Profile, read_user_config # noqa
|
||||
from .project import Project, IsFQNResource # noqa
|
||||
from .runtime import RuntimeConfig, UnsetProfileConfig # noqa
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
import os
|
||||
|
||||
from hologram import ValidationError
|
||||
from dbt.dataclass_schema import ValidationError
|
||||
|
||||
from dbt import flags
|
||||
from dbt.clients.system import load_file_contents
|
||||
from dbt.clients.yaml_helper import load_yaml_text
|
||||
from dbt.contracts.connection import Credentials, HasCredentials
|
||||
from dbt.contracts.project import ProfileConfig, UserConfig
|
||||
from dbt.exceptions import CompilationException
|
||||
from dbt.exceptions import DbtProfileError
|
||||
from dbt.exceptions import DbtProjectError
|
||||
from dbt.exceptions import ValidationException
|
||||
from dbt.exceptions import RuntimeException
|
||||
from dbt.exceptions import validator_error_message
|
||||
from dbt.logger import GLOBAL_LOGGER as logger
|
||||
from dbt.utils import parse_cli_vars
|
||||
from dbt.events.types import MissingProfileTarget
|
||||
from dbt.events.functions import fire_event
|
||||
from dbt.utils import coerce_dict_str
|
||||
|
||||
from .renderer import ConfigRenderer
|
||||
from .renderer import ProfileRenderer
|
||||
|
||||
DEFAULT_THREADS = 1
|
||||
|
||||
DEFAULT_PROFILES_DIR = os.path.join(os.path.expanduser('~'), '.dbt')
|
||||
PROFILES_DIR = os.path.expanduser(
|
||||
os.getenv('DBT_PROFILES_DIR', DEFAULT_PROFILES_DIR)
|
||||
)
|
||||
|
||||
INVALID_PROFILE_MESSAGE = """
|
||||
dbt encountered an error while trying to read your profiles.yml file.
|
||||
@@ -39,17 +43,25 @@ Here, [profile name] should be replaced with a profile name
|
||||
defined in your profiles.yml file. You can find profiles.yml here:
|
||||
|
||||
{profiles_file}/profiles.yml
|
||||
""".format(profiles_file=PROFILES_DIR)
|
||||
""".format(profiles_file=DEFAULT_PROFILES_DIR)
|
||||
|
||||
|
||||
def read_profile(profiles_dir):
|
||||
def read_profile(profiles_dir: str) -> Dict[str, Any]:
|
||||
path = os.path.join(profiles_dir, 'profiles.yml')
|
||||
|
||||
contents = None
|
||||
if os.path.isfile(path):
|
||||
try:
|
||||
contents = load_file_contents(path, strip=False)
|
||||
return load_yaml_text(contents)
|
||||
yaml_content = load_yaml_text(contents)
|
||||
if not yaml_content:
|
||||
msg = f'The profiles.yml file at {path} is empty'
|
||||
raise DbtProfileError(
|
||||
INVALID_PROFILE_MESSAGE.format(
|
||||
error_string=msg
|
||||
)
|
||||
)
|
||||
return yaml_content
|
||||
except ValidationException as e:
|
||||
msg = INVALID_PROFILE_MESSAGE.format(error_string=e)
|
||||
raise ValidationException(msg) from e
|
||||
@@ -57,29 +69,52 @@ def read_profile(profiles_dir):
|
||||
return {}
|
||||
|
||||
|
||||
def read_user_config(directory):
|
||||
def read_user_config(directory: str) -> UserConfig:
|
||||
try:
|
||||
user_cfg = None
|
||||
profile = read_profile(directory)
|
||||
if profile:
|
||||
user_cfg = profile.get('config', {})
|
||||
return UserConfig.from_dict(user_cfg)
|
||||
user_config = coerce_dict_str(profile.get('config', {}))
|
||||
if user_config is not None:
|
||||
UserConfig.validate(user_config)
|
||||
return UserConfig.from_dict(user_config)
|
||||
except (RuntimeException, ValidationError):
|
||||
return UserConfig()
|
||||
pass
|
||||
return UserConfig()
|
||||
|
||||
|
||||
class Profile:
|
||||
def __init__(self, profile_name, target_name, config, threads,
|
||||
credentials):
|
||||
# The Profile class is included in RuntimeConfig, so any attribute
|
||||
# additions must also be set where the RuntimeConfig class is created
|
||||
# `init=False` is a workaround for https://bugs.python.org/issue45081
|
||||
@dataclass(init=False)
|
||||
class Profile(HasCredentials):
|
||||
profile_name: str
|
||||
target_name: str
|
||||
user_config: UserConfig
|
||||
threads: int
|
||||
credentials: Credentials
|
||||
profile_env_vars: Dict[str, Any]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
profile_name: str,
|
||||
target_name: str,
|
||||
user_config: UserConfig,
|
||||
threads: int,
|
||||
credentials: Credentials
|
||||
):
|
||||
"""Explicitly defining `__init__` to work around bug in Python 3.9.7
|
||||
https://bugs.python.org/issue45081
|
||||
"""
|
||||
self.profile_name = profile_name
|
||||
self.target_name = target_name
|
||||
if isinstance(config, dict):
|
||||
config = UserConfig.from_dict(config)
|
||||
self.config = config
|
||||
self.user_config = user_config
|
||||
self.threads = threads
|
||||
self.credentials = credentials
|
||||
self.profile_env_vars = {} # never available on init
|
||||
|
||||
def to_profile_info(self, serialize_credentials=False):
|
||||
def to_profile_info(
|
||||
self, serialize_credentials: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""Unlike to_project_config, this dict is not a mirror of any existing
|
||||
on-disk data structure. It's used when creating a new profile from an
|
||||
existing one.
|
||||
@@ -91,35 +126,49 @@ class Profile:
|
||||
result = {
|
||||
'profile_name': self.profile_name,
|
||||
'target_name': self.target_name,
|
||||
'config': self.config.to_dict(),
|
||||
'user_config': self.user_config,
|
||||
'threads': self.threads,
|
||||
'credentials': self.credentials,
|
||||
}
|
||||
if serialize_credentials:
|
||||
result['credentials'] = result['credentials'].to_dict()
|
||||
result['user_config'] = self.user_config.to_dict(omit_none=True)
|
||||
result['credentials'] = self.credentials.to_dict(omit_none=True)
|
||||
return result
|
||||
|
||||
def __str__(self):
|
||||
return str(self.to_profile_info())
|
||||
def to_target_dict(self) -> Dict[str, Any]:
|
||||
target = dict(
|
||||
self.credentials.connection_info(with_aliases=True)
|
||||
)
|
||||
target.update({
|
||||
'type': self.credentials.type,
|
||||
'threads': self.threads,
|
||||
'name': self.target_name,
|
||||
'target_name': self.target_name,
|
||||
'profile_name': self.profile_name,
|
||||
'config': self.user_config.to_dict(omit_none=True),
|
||||
})
|
||||
return target
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not (isinstance(other, self.__class__) and
|
||||
isinstance(self, other.__class__)):
|
||||
return False
|
||||
return NotImplemented
|
||||
return self.to_profile_info() == other.to_profile_info()
|
||||
|
||||
def validate(self):
|
||||
try:
|
||||
if self.credentials:
|
||||
self.credentials.to_dict(validate=True)
|
||||
ProfileConfig.from_dict(
|
||||
self.to_profile_info(serialize_credentials=True)
|
||||
)
|
||||
dct = self.credentials.to_dict(omit_none=True)
|
||||
self.credentials.validate(dct)
|
||||
dct = self.to_profile_info(serialize_credentials=True)
|
||||
ProfileConfig.validate(dct)
|
||||
except ValidationError as exc:
|
||||
raise DbtProfileError(validator_error_message(exc)) from exc
|
||||
|
||||
@staticmethod
|
||||
def _credentials_from_profile(profile, profile_name, target_name):
|
||||
def _credentials_from_profile(
|
||||
profile: Dict[str, Any], profile_name: str, target_name: str
|
||||
) -> Credentials:
|
||||
# avoid an import cycle
|
||||
from dbt.adapters.factory import load_plugin
|
||||
# credentials carry their 'type' in their actual type, not their
|
||||
@@ -132,7 +181,9 @@ class Profile:
|
||||
typename = profile.pop('type')
|
||||
try:
|
||||
cls = load_plugin(typename)
|
||||
credentials = cls.from_dict(profile)
|
||||
data = cls.translate_aliases(profile)
|
||||
cls.validate(data)
|
||||
credentials = cls.from_dict(data)
|
||||
except (RuntimeException, ValidationError) as e:
|
||||
msg = str(e) if isinstance(e, RuntimeException) else e.message
|
||||
raise DbtProfileError(
|
||||
@@ -143,7 +194,10 @@ class Profile:
|
||||
return credentials
|
||||
|
||||
@staticmethod
|
||||
def pick_profile_name(args_profile_name, project_profile_name=None):
|
||||
def pick_profile_name(
|
||||
args_profile_name: Optional[str],
|
||||
project_profile_name: Optional[str] = None,
|
||||
) -> str:
|
||||
profile_name = project_profile_name
|
||||
if args_profile_name is not None:
|
||||
profile_name = args_profile_name
|
||||
@@ -152,7 +206,9 @@ class Profile:
|
||||
return profile_name
|
||||
|
||||
@staticmethod
|
||||
def _get_profile_data(profile, profile_name, target_name):
|
||||
def _get_profile_data(
|
||||
profile: Dict[str, Any], profile_name: str, target_name: str
|
||||
) -> Dict[str, Any]:
|
||||
if 'outputs' not in profile:
|
||||
raise DbtProfileError(
|
||||
"outputs not specified in profile '{}'".format(profile_name)
|
||||
@@ -167,31 +223,46 @@ class Profile:
|
||||
.format(profile_name, target_name, outputs))
|
||||
raise DbtProfileError(msg, result_type='invalid_target')
|
||||
profile_data = outputs[target_name]
|
||||
|
||||
if not isinstance(profile_data, dict):
|
||||
msg = (
|
||||
f"output '{target_name}' of profile '{profile_name}' is "
|
||||
f"misconfigured in profiles.yml"
|
||||
)
|
||||
raise DbtProfileError(msg, result_type='invalid_target')
|
||||
|
||||
return profile_data
|
||||
|
||||
@classmethod
|
||||
def from_credentials(cls, credentials, threads, profile_name, target_name,
|
||||
user_cfg=None):
|
||||
def from_credentials(
|
||||
cls,
|
||||
credentials: Credentials,
|
||||
threads: int,
|
||||
profile_name: str,
|
||||
target_name: str,
|
||||
user_config: Optional[Dict[str, Any]] = None
|
||||
) -> 'Profile':
|
||||
"""Create a profile from an existing set of Credentials and the
|
||||
remaining information.
|
||||
|
||||
:param credentials dict: The credentials dict for this profile.
|
||||
:param threads int: The number of threads to use for connections.
|
||||
:param profile_name str: The profile name used for this profile.
|
||||
:param target_name str: The target name used for this profile.
|
||||
:param user_cfg Optional[dict]: The user-level config block from the
|
||||
:param credentials: The credentials dict for this profile.
|
||||
:param threads: The number of threads to use for connections.
|
||||
:param profile_name: The profile name used for this profile.
|
||||
:param target_name: The target name used for this profile.
|
||||
:param user_config: The user-level config block from the
|
||||
raw profiles, if specified.
|
||||
:raises DbtProfileError: If the profile is invalid.
|
||||
:returns Profile: The new Profile object.
|
||||
:returns: The new Profile object.
|
||||
"""
|
||||
if user_cfg is None:
|
||||
user_cfg = {}
|
||||
config = UserConfig.from_dict(user_cfg)
|
||||
if user_config is None:
|
||||
user_config = {}
|
||||
UserConfig.validate(user_config)
|
||||
user_config_obj: UserConfig = UserConfig.from_dict(user_config)
|
||||
|
||||
profile = cls(
|
||||
profile_name=profile_name,
|
||||
target_name=target_name,
|
||||
config=config,
|
||||
user_config=user_config_obj,
|
||||
threads=threads,
|
||||
credentials=credentials
|
||||
)
|
||||
@@ -199,13 +270,16 @@ class Profile:
|
||||
return profile
|
||||
|
||||
@classmethod
|
||||
def render_profile(cls, raw_profile, profile_name, target_override,
|
||||
cli_vars):
|
||||
def render_profile(
|
||||
cls,
|
||||
raw_profile: Dict[str, Any],
|
||||
profile_name: str,
|
||||
target_override: Optional[str],
|
||||
renderer: ProfileRenderer,
|
||||
) -> Tuple[str, Dict[str, Any]]:
|
||||
"""This is a containment zone for the hateful way we're rendering
|
||||
profiles.
|
||||
"""
|
||||
renderer = ConfigRenderer(cli_vars=cli_vars)
|
||||
|
||||
# rendering profiles is a bit complex. Two constraints cause trouble:
|
||||
# 1) users should be able to use environment/cli variables to specify
|
||||
# the target in their profile.
|
||||
@@ -220,48 +294,52 @@ class Profile:
|
||||
target_name = renderer.render_value(raw_profile['target'])
|
||||
else:
|
||||
target_name = 'default'
|
||||
logger.debug(
|
||||
"target not specified in profile '{}', using '{}'"
|
||||
.format(profile_name, target_name)
|
||||
)
|
||||
fire_event(MissingProfileTarget(profile_name=profile_name, target_name=target_name))
|
||||
|
||||
raw_profile_data = cls._get_profile_data(
|
||||
raw_profile, profile_name, target_name
|
||||
)
|
||||
|
||||
profile_data = renderer.render_profile_data(raw_profile_data)
|
||||
try:
|
||||
profile_data = renderer.render_data(raw_profile_data)
|
||||
except CompilationException as exc:
|
||||
raise DbtProfileError(str(exc)) from exc
|
||||
return target_name, profile_data
|
||||
|
||||
@classmethod
|
||||
def from_raw_profile_info(cls, raw_profile, profile_name, cli_vars,
|
||||
user_cfg=None, target_override=None,
|
||||
threads_override=None):
|
||||
def from_raw_profile_info(
|
||||
cls,
|
||||
raw_profile: Dict[str, Any],
|
||||
profile_name: str,
|
||||
renderer: ProfileRenderer,
|
||||
user_config: Optional[Dict[str, Any]] = None,
|
||||
target_override: Optional[str] = None,
|
||||
threads_override: Optional[int] = None,
|
||||
) -> 'Profile':
|
||||
"""Create a profile from its raw profile information.
|
||||
|
||||
(this is an intermediate step, mostly useful for unit testing)
|
||||
|
||||
:param raw_profile dict: The profile data for a single profile, from
|
||||
:param raw_profile: The profile data for a single profile, from
|
||||
disk as yaml and its values rendered with jinja.
|
||||
:param profile_name str: The profile name used.
|
||||
:param cli_vars dict: The command-line variables passed as arguments,
|
||||
as a dict.
|
||||
:param user_cfg Optional[dict]: The global config for the user, if it
|
||||
:param profile_name: The profile name used.
|
||||
:param renderer: The config renderer.
|
||||
:param user_config: The global config for the user, if it
|
||||
was present.
|
||||
:param target_override Optional[str]: The target to use, if provided on
|
||||
:param target_override: The target to use, if provided on
|
||||
the command line.
|
||||
:param threads_override Optional[str]: The thread count to use, if
|
||||
:param threads_override: The thread count to use, if
|
||||
provided on the command line.
|
||||
:raises DbtProfileError: If the profile is invalid or missing, or the
|
||||
target could not be found
|
||||
:returns Profile: The new Profile object.
|
||||
:returns: The new Profile object.
|
||||
"""
|
||||
# user_cfg is not rendered.
|
||||
if user_cfg is None:
|
||||
user_cfg = raw_profile.get('config')
|
||||
|
||||
# user_config is not rendered.
|
||||
if user_config is None:
|
||||
user_config = raw_profile.get('config')
|
||||
# TODO: should it be, and the values coerced to bool?
|
||||
target_name, profile_data = cls.render_profile(
|
||||
raw_profile, profile_name, target_override, cli_vars
|
||||
raw_profile, profile_name, target_override, renderer
|
||||
)
|
||||
|
||||
# valid connections never include the number of threads, but it's
|
||||
@@ -270,7 +348,7 @@ class Profile:
|
||||
if threads_override is not None:
|
||||
threads = threads_override
|
||||
|
||||
credentials = cls._credentials_from_profile(
|
||||
credentials: Credentials = cls._credentials_from_profile(
|
||||
profile_data, profile_name, target_name
|
||||
)
|
||||
|
||||
@@ -279,26 +357,31 @@ class Profile:
|
||||
profile_name=profile_name,
|
||||
target_name=target_name,
|
||||
threads=threads,
|
||||
user_cfg=user_cfg
|
||||
user_config=user_config
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_raw_profiles(cls, raw_profiles, profile_name, cli_vars,
|
||||
target_override=None, threads_override=None):
|
||||
def from_raw_profiles(
|
||||
cls,
|
||||
raw_profiles: Dict[str, Any],
|
||||
profile_name: str,
|
||||
renderer: ProfileRenderer,
|
||||
target_override: Optional[str] = None,
|
||||
threads_override: Optional[int] = None,
|
||||
) -> 'Profile':
|
||||
"""
|
||||
:param raw_profiles dict: The profile data, from disk as yaml.
|
||||
:param profile_name str: The profile name to use.
|
||||
:param cli_vars dict: The command-line variables passed as arguments,
|
||||
as a dict.
|
||||
:param target_override Optional[str]: The target to use, if provided on
|
||||
the command line.
|
||||
:param threads_override Optional[str]: The thread count to use, if
|
||||
provided on the command line.
|
||||
:param raw_profiles: The profile data, from disk as yaml.
|
||||
:param profile_name: The profile name to use.
|
||||
:param renderer: The config renderer.
|
||||
:param target_override: The target to use, if provided on the command
|
||||
line.
|
||||
:param threads_override: The thread count to use, if provided on the
|
||||
command line.
|
||||
:raises DbtProjectError: If there is no profile name specified in the
|
||||
project or the command line arguments
|
||||
:raises DbtProfileError: If the profile is invalid or missing, or the
|
||||
target could not be found
|
||||
:returns Profile: The new Profile object.
|
||||
:returns: The new Profile object.
|
||||
"""
|
||||
if profile_name not in raw_profiles:
|
||||
raise DbtProjectError(
|
||||
@@ -308,20 +391,33 @@ class Profile:
|
||||
# First, we've already got our final decision on profile name, and we
|
||||
# don't render keys, so we can pluck that out
|
||||
raw_profile = raw_profiles[profile_name]
|
||||
|
||||
user_cfg = raw_profiles.get('config')
|
||||
if not raw_profile:
|
||||
msg = (
|
||||
f'Profile {profile_name} in profiles.yml is empty'
|
||||
)
|
||||
raise DbtProfileError(
|
||||
INVALID_PROFILE_MESSAGE.format(
|
||||
error_string=msg
|
||||
)
|
||||
)
|
||||
user_config = raw_profiles.get('config')
|
||||
|
||||
return cls.from_raw_profile_info(
|
||||
raw_profile=raw_profile,
|
||||
profile_name=profile_name,
|
||||
cli_vars=cli_vars,
|
||||
user_cfg=user_cfg,
|
||||
renderer=renderer,
|
||||
user_config=user_config,
|
||||
target_override=target_override,
|
||||
threads_override=threads_override,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_args(cls, args, project_profile_name=None):
|
||||
def render_from_args(
|
||||
cls,
|
||||
args: Any,
|
||||
renderer: ProfileRenderer,
|
||||
project_profile_name: Optional[str],
|
||||
) -> 'Profile':
|
||||
"""Given the raw profiles as read from disk and the name of the desired
|
||||
profile if specified, return the profile component of the runtime
|
||||
config.
|
||||
@@ -336,17 +432,15 @@ class Profile:
|
||||
target could not be found.
|
||||
:returns Profile: The new Profile object.
|
||||
"""
|
||||
cli_vars = parse_cli_vars(getattr(args, 'vars', '{}'))
|
||||
threads_override = getattr(args, 'threads', None)
|
||||
target_override = getattr(args, 'target', None)
|
||||
raw_profiles = read_profile(args.profiles_dir)
|
||||
profile_name = cls.pick_profile_name(args.profile,
|
||||
raw_profiles = read_profile(flags.PROFILES_DIR)
|
||||
profile_name = cls.pick_profile_name(getattr(args, 'profile', None),
|
||||
project_profile_name)
|
||||
|
||||
return cls.from_raw_profiles(
|
||||
raw_profiles=raw_profiles,
|
||||
profile_name=profile_name,
|
||||
cli_vars=cli_vars,
|
||||
renderer=renderer,
|
||||
target_override=target_override,
|
||||
threads_override=threads_override
|
||||
)
|
||||
|
||||
@@ -1,46 +1,51 @@
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass, field
|
||||
from itertools import chain
|
||||
from typing import (
|
||||
List, Dict, Any, Optional, TypeVar, Union, Mapping,
|
||||
)
|
||||
from typing_extensions import Protocol, runtime_checkable
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
from dbt import deprecations
|
||||
from dbt.clients.system import resolve_path_from_base
|
||||
from dbt.clients.system import path_exists
|
||||
from dbt.clients.system import load_file_contents
|
||||
from dbt.clients.yaml_helper import load_yaml_text
|
||||
from dbt.contracts.connection import QueryComment
|
||||
from dbt.exceptions import DbtProjectError
|
||||
from dbt.exceptions import RecursionException
|
||||
from dbt.exceptions import SemverException
|
||||
from dbt.exceptions import validator_error_message
|
||||
from dbt.exceptions import warn_or_error
|
||||
from dbt.exceptions import RuntimeException
|
||||
from dbt.graph import SelectionSpec
|
||||
from dbt.helper_types import NoValue
|
||||
from dbt.semver import VersionSpecifier
|
||||
from dbt.semver import versions_compatible
|
||||
from dbt.version import get_installed_version
|
||||
from dbt.ui import printer
|
||||
from dbt.utils import deep_map
|
||||
from dbt.utils import parse_cli_vars
|
||||
from dbt.source_config import SourceConfig
|
||||
|
||||
from dbt.contracts.graph.manifest import ManifestMetadata
|
||||
from dbt.contracts.project import Project as ProjectContract
|
||||
from dbt.utils import MultiDict
|
||||
from dbt.node_types import NodeType
|
||||
from dbt.config.selectors import SelectorDict
|
||||
from dbt.contracts.project import (
|
||||
Project as ProjectContract,
|
||||
SemverString,
|
||||
)
|
||||
from dbt.contracts.project import PackageConfig
|
||||
|
||||
from hologram import ValidationError
|
||||
|
||||
from .renderer import ConfigRenderer
|
||||
|
||||
|
||||
UNUSED_RESOURCE_CONFIGURATION_PATH_MESSAGE = """\
|
||||
WARNING: Configuration paths exist in your dbt_project.yml file which do not \
|
||||
apply to any resources.
|
||||
There are {} unused configuration paths:\n{}
|
||||
"""
|
||||
from dbt.dataclass_schema import ValidationError
|
||||
from .renderer import DbtProjectYamlRenderer
|
||||
from .selectors import (
|
||||
selector_config_from_data,
|
||||
selector_data_from_root,
|
||||
SelectorConfig,
|
||||
)
|
||||
|
||||
|
||||
INVALID_VERSION_ERROR = """\
|
||||
This version of dbt is not supported with the '{package}' package.
|
||||
Installed version of dbt: {installed}
|
||||
Required version of dbt for '{package}': {version_spec}
|
||||
Check the requirements for the '{package}' package, or run dbt again with \
|
||||
Check for a different version of the '{package}' package, or run dbt again with \
|
||||
--no-version-check
|
||||
"""
|
||||
|
||||
@@ -49,28 +54,27 @@ IMPOSSIBLE_VERSION_ERROR = """\
|
||||
The package version requirement can never be satisfied for the '{package}
|
||||
package.
|
||||
Required versions of dbt for '{package}': {version_spec}
|
||||
Check the requirements for the '{package}' package, or run dbt again with \
|
||||
Check for a different version of the '{package}' package, or run dbt again with \
|
||||
--no-version-check
|
||||
"""
|
||||
|
||||
MALFORMED_PACKAGE_ERROR = """\
|
||||
The packages.yml file in this project is malformed. Please double check
|
||||
the contents of this file and fix any errors before retrying.
|
||||
|
||||
def _list_if_none(value):
|
||||
if value is None:
|
||||
value = []
|
||||
return value
|
||||
You can find more information on the syntax for this file here:
|
||||
https://docs.getdbt.com/docs/package-management
|
||||
|
||||
Validator Error:
|
||||
{error}
|
||||
"""
|
||||
|
||||
|
||||
def _dict_if_none(value):
|
||||
if value is None:
|
||||
value = {}
|
||||
return value
|
||||
|
||||
|
||||
def _list_if_none_or_string(value):
|
||||
value = _list_if_none(value)
|
||||
if isinstance(value, str):
|
||||
return [value]
|
||||
return value
|
||||
@runtime_checkable
|
||||
class IsFQNResource(Protocol):
|
||||
fqn: List[str]
|
||||
resource_type: NodeType
|
||||
package_name: str
|
||||
|
||||
|
||||
def _load_yaml(path):
|
||||
@@ -78,32 +82,6 @@ def _load_yaml(path):
|
||||
return load_yaml_text(contents)
|
||||
|
||||
|
||||
def _get_config_paths(config, path=(), paths=None):
|
||||
if paths is None:
|
||||
paths = set()
|
||||
|
||||
for key, value in config.items():
|
||||
if isinstance(value, dict):
|
||||
if key in SourceConfig.ConfigKeys:
|
||||
if path not in paths:
|
||||
paths.add(path)
|
||||
else:
|
||||
_get_config_paths(value, path + (key,), paths)
|
||||
else:
|
||||
if path not in paths:
|
||||
paths.add(path)
|
||||
|
||||
return frozenset(paths)
|
||||
|
||||
|
||||
def _is_config_used(path, fqns):
|
||||
if fqns:
|
||||
for fqn in fqns:
|
||||
if len(path) <= len(fqn) and fqn[:len(path)] == path:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def package_data_from_root(project_root):
|
||||
package_filepath = resolve_path_from_base(
|
||||
'packages.yml', project_root
|
||||
@@ -116,20 +94,21 @@ def package_data_from_root(project_root):
|
||||
return packages_dict
|
||||
|
||||
|
||||
def package_config_from_data(packages_data):
|
||||
if packages_data is None:
|
||||
def package_config_from_data(packages_data: Dict[str, Any]):
|
||||
if not packages_data:
|
||||
packages_data = {'packages': []}
|
||||
|
||||
try:
|
||||
PackageConfig.validate(packages_data)
|
||||
packages = PackageConfig.from_dict(packages_data)
|
||||
except ValidationError as e:
|
||||
raise DbtProjectError(
|
||||
'Invalid package config: {}'.format(validator_error_message(e))
|
||||
MALFORMED_PACKAGE_ERROR.format(error=str(e.message))
|
||||
) from e
|
||||
return packages
|
||||
|
||||
|
||||
def _parse_versions(versions):
|
||||
def _parse_versions(versions: Union[List[str], str]) -> List[VersionSpecifier]:
|
||||
"""Parse multiple versions as read from disk. The versions value may be any
|
||||
one of:
|
||||
- a single version string ('>0.12.1')
|
||||
@@ -144,163 +123,459 @@ def _parse_versions(versions):
|
||||
return [VersionSpecifier.from_version_string(v) for v in versions]
|
||||
|
||||
|
||||
class Project:
|
||||
def __init__(self, project_name, version, project_root, profile_name,
|
||||
source_paths, macro_paths, data_paths, test_paths,
|
||||
analysis_paths, docs_paths, target_path, snapshot_paths,
|
||||
clean_targets, log_path, modules_path, quoting, models,
|
||||
on_run_start, on_run_end, seeds, snapshots, dbt_version,
|
||||
packages, query_comment):
|
||||
self.project_name = project_name
|
||||
self.version = version
|
||||
self.project_root = project_root
|
||||
self.profile_name = profile_name
|
||||
self.source_paths = source_paths
|
||||
self.macro_paths = macro_paths
|
||||
self.data_paths = data_paths
|
||||
self.test_paths = test_paths
|
||||
self.analysis_paths = analysis_paths
|
||||
self.docs_paths = docs_paths
|
||||
self.target_path = target_path
|
||||
self.snapshot_paths = snapshot_paths
|
||||
self.clean_targets = clean_targets
|
||||
self.log_path = log_path
|
||||
self.modules_path = modules_path
|
||||
self.quoting = quoting
|
||||
self.models = models
|
||||
self.on_run_start = on_run_start
|
||||
self.on_run_end = on_run_end
|
||||
self.seeds = seeds
|
||||
self.snapshots = snapshots
|
||||
self.dbt_version = dbt_version
|
||||
self.packages = packages
|
||||
self.query_comment = query_comment
|
||||
def _all_source_paths(
|
||||
model_paths: List[str],
|
||||
seed_paths: List[str],
|
||||
snapshot_paths: List[str],
|
||||
analysis_paths: List[str],
|
||||
macro_paths: List[str],
|
||||
) -> List[str]:
|
||||
return list(chain(model_paths, seed_paths, snapshot_paths, analysis_paths,
|
||||
macro_paths))
|
||||
|
||||
@staticmethod
|
||||
def _preprocess(project_dict):
|
||||
"""Pre-process certain special keys to convert them from None values
|
||||
into empty containers, and to turn strings into arrays of strings.
|
||||
"""
|
||||
handlers = {
|
||||
('on-run-start',): _list_if_none_or_string,
|
||||
('on-run-end',): _list_if_none_or_string,
|
||||
}
|
||||
|
||||
for k in ('models', 'seeds', 'snapshots'):
|
||||
handlers[(k,)] = _dict_if_none
|
||||
handlers[(k, 'vars')] = _dict_if_none
|
||||
handlers[(k, 'pre-hook')] = _list_if_none_or_string
|
||||
handlers[(k, 'post-hook')] = _list_if_none_or_string
|
||||
handlers[('seeds', 'column_types')] = _dict_if_none
|
||||
T = TypeVar('T')
|
||||
|
||||
def converter(value, keypath):
|
||||
if keypath in handlers:
|
||||
handler = handlers[keypath]
|
||||
return handler(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
return deep_map(converter, project_dict)
|
||||
def value_or(value: Optional[T], default: T) -> T:
|
||||
if value is None:
|
||||
return default
|
||||
else:
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def from_project_config(cls, project_dict, packages_dict=None):
|
||||
"""Create a project from its project and package configuration, as read
|
||||
by yaml.safe_load().
|
||||
|
||||
:param project_dict dict: The dictionary as read from disk
|
||||
:param packages_dict Optional[dict]: If it exists, the packages file as
|
||||
read from disk.
|
||||
:raises DbtProjectError: If the project is missing or invalid, or if
|
||||
the packages file exists and is invalid.
|
||||
:returns Project: The project, with defaults populated.
|
||||
"""
|
||||
try:
|
||||
project_dict = cls._preprocess(project_dict)
|
||||
except RecursionException:
|
||||
def _raw_project_from(project_root: str) -> Dict[str, Any]:
|
||||
|
||||
project_root = os.path.normpath(project_root)
|
||||
project_yaml_filepath = os.path.join(project_root, 'dbt_project.yml')
|
||||
|
||||
# get the project.yml contents
|
||||
if not path_exists(project_yaml_filepath):
|
||||
raise DbtProjectError(
|
||||
'no dbt_project.yml found at expected path {}'
|
||||
.format(project_yaml_filepath)
|
||||
)
|
||||
|
||||
project_dict = _load_yaml(project_yaml_filepath)
|
||||
|
||||
if not isinstance(project_dict, dict):
|
||||
raise DbtProjectError(
|
||||
'dbt_project.yml does not parse to a dictionary'
|
||||
)
|
||||
|
||||
return project_dict
|
||||
|
||||
|
||||
def _query_comment_from_cfg(
|
||||
cfg_query_comment: Union[QueryComment, NoValue, str, None]
|
||||
) -> QueryComment:
|
||||
if not cfg_query_comment:
|
||||
return QueryComment(comment='')
|
||||
|
||||
if isinstance(cfg_query_comment, str):
|
||||
return QueryComment(comment=cfg_query_comment)
|
||||
|
||||
if isinstance(cfg_query_comment, NoValue):
|
||||
return QueryComment()
|
||||
|
||||
return cfg_query_comment
|
||||
|
||||
|
||||
def validate_version(dbt_version: List[VersionSpecifier], project_name: str):
|
||||
"""Ensure this package works with the installed version of dbt."""
|
||||
installed = get_installed_version()
|
||||
if not versions_compatible(*dbt_version):
|
||||
msg = IMPOSSIBLE_VERSION_ERROR.format(
|
||||
package=project_name,
|
||||
version_spec=[
|
||||
x.to_version_string() for x in dbt_version
|
||||
]
|
||||
)
|
||||
raise DbtProjectError(msg)
|
||||
|
||||
if not versions_compatible(installed, *dbt_version):
|
||||
msg = INVALID_VERSION_ERROR.format(
|
||||
package=project_name,
|
||||
installed=installed.to_version_string(),
|
||||
version_spec=[
|
||||
x.to_version_string() for x in dbt_version
|
||||
]
|
||||
)
|
||||
raise DbtProjectError(msg)
|
||||
|
||||
|
||||
def _get_required_version(
|
||||
project_dict: Dict[str, Any],
|
||||
verify_version: bool,
|
||||
) -> List[VersionSpecifier]:
|
||||
dbt_raw_version: Union[List[str], str] = '>=0.0.0'
|
||||
required = project_dict.get('require-dbt-version')
|
||||
if required is not None:
|
||||
dbt_raw_version = required
|
||||
|
||||
try:
|
||||
dbt_version = _parse_versions(dbt_raw_version)
|
||||
except SemverException as e:
|
||||
raise DbtProjectError(str(e)) from e
|
||||
|
||||
if verify_version:
|
||||
# no name is also an error that we want to raise
|
||||
if 'name' not in project_dict:
|
||||
raise DbtProjectError(
|
||||
'Cycle detected: Project input has a reference to itself',
|
||||
project=project_dict
|
||||
'Required "name" field not present in project',
|
||||
)
|
||||
# just for validation.
|
||||
validate_version(dbt_version, project_dict['name'])
|
||||
|
||||
return dbt_version
|
||||
|
||||
|
||||
@dataclass
|
||||
class RenderComponents:
|
||||
project_dict: Dict[str, Any] = field(
|
||||
metadata=dict(description='The project dictionary')
|
||||
)
|
||||
packages_dict: Dict[str, Any] = field(
|
||||
metadata=dict(description='The packages dictionary')
|
||||
)
|
||||
selectors_dict: Dict[str, Any] = field(
|
||||
metadata=dict(description='The selectors dictionary')
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PartialProject(RenderComponents):
|
||||
profile_name: Optional[str] = field(metadata=dict(
|
||||
description='The unrendered profile name in the project, if set'
|
||||
))
|
||||
project_name: Optional[str] = field(metadata=dict(
|
||||
description=(
|
||||
'The name of the project. This should always be set and will not '
|
||||
'be rendered'
|
||||
)
|
||||
))
|
||||
project_root: str = field(
|
||||
metadata=dict(description='The root directory of the project'),
|
||||
)
|
||||
verify_version: bool = field(
|
||||
metadata=dict(description=(
|
||||
'If True, verify the dbt version matches the required version'
|
||||
))
|
||||
)
|
||||
|
||||
def render_profile_name(self, renderer) -> Optional[str]:
|
||||
if self.profile_name is None:
|
||||
return None
|
||||
return renderer.render_value(self.profile_name)
|
||||
|
||||
def get_rendered(
|
||||
self,
|
||||
renderer: DbtProjectYamlRenderer,
|
||||
) -> RenderComponents:
|
||||
|
||||
rendered_project = renderer.render_project(
|
||||
self.project_dict, self.project_root
|
||||
)
|
||||
rendered_packages = renderer.render_packages(self.packages_dict)
|
||||
rendered_selectors = renderer.render_selectors(self.selectors_dict)
|
||||
|
||||
return RenderComponents(
|
||||
project_dict=rendered_project,
|
||||
packages_dict=rendered_packages,
|
||||
selectors_dict=rendered_selectors,
|
||||
)
|
||||
|
||||
# Called by 'collect_parts' in RuntimeConfig
|
||||
def render(self, renderer: DbtProjectYamlRenderer) -> 'Project':
|
||||
try:
|
||||
ProjectContract.from_dict(project_dict)
|
||||
rendered = self.get_rendered(renderer)
|
||||
return self.create_project(rendered)
|
||||
except DbtProjectError as exc:
|
||||
if exc.path is None:
|
||||
exc.path = os.path.join(self.project_root, 'dbt_project.yml')
|
||||
raise
|
||||
|
||||
def check_config_path(self, project_dict, deprecated_path, exp_path):
|
||||
if deprecated_path in project_dict:
|
||||
if exp_path in project_dict:
|
||||
msg = (
|
||||
'{deprecated_path} and {exp_path} cannot both be defined. The '
|
||||
'`{deprecated_path}` config has been deprecated in favor of `{exp_path}`. '
|
||||
'Please update your `dbt_project.yml` configuration to reflect this '
|
||||
'change.'
|
||||
)
|
||||
raise DbtProjectError(msg.format(deprecated_path=deprecated_path,
|
||||
exp_path=exp_path))
|
||||
deprecations.warn(f'project-config-{deprecated_path}',
|
||||
deprecated_path=deprecated_path,
|
||||
exp_path=exp_path)
|
||||
|
||||
def create_project(self, rendered: RenderComponents) -> 'Project':
|
||||
unrendered = RenderComponents(
|
||||
project_dict=self.project_dict,
|
||||
packages_dict=self.packages_dict,
|
||||
selectors_dict=self.selectors_dict,
|
||||
)
|
||||
dbt_version = _get_required_version(
|
||||
rendered.project_dict,
|
||||
verify_version=self.verify_version,
|
||||
)
|
||||
|
||||
self.check_config_path(rendered.project_dict, 'source-paths', 'model-paths')
|
||||
self.check_config_path(rendered.project_dict, 'data-paths', 'seed-paths')
|
||||
|
||||
try:
|
||||
ProjectContract.validate(rendered.project_dict)
|
||||
cfg = ProjectContract.from_dict(
|
||||
rendered.project_dict
|
||||
)
|
||||
except ValidationError as e:
|
||||
raise DbtProjectError(validator_error_message(e)) from e
|
||||
|
||||
# name/version are required in the Project definition, so we can assume
|
||||
# they are present
|
||||
name = project_dict['name']
|
||||
version = project_dict['version']
|
||||
name = cfg.name
|
||||
version = cfg.version
|
||||
# this is added at project_dict parse time and should always be here
|
||||
# once we see it.
|
||||
project_root = project_dict['project-root']
|
||||
if cfg.project_root is None:
|
||||
raise DbtProjectError('cfg must have a project root!')
|
||||
else:
|
||||
project_root = cfg.project_root
|
||||
# this is only optional in the sense that if it's not present, it needs
|
||||
# to have been a cli argument.
|
||||
profile_name = project_dict.get('profile')
|
||||
# these are optional
|
||||
source_paths = project_dict.get('source-paths', ['models'])
|
||||
macro_paths = project_dict.get('macro-paths', ['macros'])
|
||||
data_paths = project_dict.get('data-paths', ['data'])
|
||||
test_paths = project_dict.get('test-paths', ['test'])
|
||||
analysis_paths = project_dict.get('analysis-paths', [])
|
||||
docs_paths = project_dict.get('docs-paths', source_paths[:])
|
||||
target_path = project_dict.get('target-path', 'target')
|
||||
snapshot_paths = project_dict.get('snapshot-paths', ['snapshots'])
|
||||
# should this also include the modules path by default?
|
||||
clean_targets = project_dict.get('clean-targets', [target_path])
|
||||
log_path = project_dict.get('log-path', 'logs')
|
||||
modules_path = project_dict.get('modules-path', 'dbt_modules')
|
||||
profile_name = cfg.profile
|
||||
# these are all the defaults
|
||||
|
||||
# `source_paths` is deprecated but still allowed. Copy it into
|
||||
# `model_paths` to simlify logic throughout the rest of the system.
|
||||
model_paths: List[str] = value_or(cfg.model_paths
|
||||
if 'model-paths' in rendered.project_dict
|
||||
else cfg.source_paths, ['models'])
|
||||
macro_paths: List[str] = value_or(cfg.macro_paths, ['macros'])
|
||||
# `data_paths` is deprecated but still allowed. Copy it into
|
||||
# `seed_paths` to simlify logic throughout the rest of the system.
|
||||
seed_paths: List[str] = value_or(cfg.seed_paths
|
||||
if 'seed-paths' in rendered.project_dict
|
||||
else cfg.data_paths, ['seeds'])
|
||||
test_paths: List[str] = value_or(cfg.test_paths, ['tests'])
|
||||
analysis_paths: List[str] = value_or(cfg.analysis_paths, ['analyses'])
|
||||
snapshot_paths: List[str] = value_or(cfg.snapshot_paths, ['snapshots'])
|
||||
|
||||
all_source_paths: List[str] = _all_source_paths(
|
||||
model_paths, seed_paths, snapshot_paths, analysis_paths,
|
||||
macro_paths
|
||||
)
|
||||
|
||||
docs_paths: List[str] = value_or(cfg.docs_paths, all_source_paths)
|
||||
asset_paths: List[str] = value_or(cfg.asset_paths, [])
|
||||
target_path: str = value_or(cfg.target_path, 'target')
|
||||
clean_targets: List[str] = value_or(cfg.clean_targets, [target_path])
|
||||
log_path: str = value_or(cfg.log_path, 'logs')
|
||||
packages_install_path: str = value_or(cfg.packages_install_path, 'dbt_packages')
|
||||
# in the default case we'll populate this once we know the adapter type
|
||||
quoting = project_dict.get('quoting', {})
|
||||
# It would be nice to just pass along a Quoting here, but that would
|
||||
# break many things
|
||||
quoting: Dict[str, Any] = {}
|
||||
if cfg.quoting is not None:
|
||||
quoting = cfg.quoting.to_dict(omit_none=True)
|
||||
|
||||
models = project_dict.get('models', {})
|
||||
on_run_start = project_dict.get('on-run-start', [])
|
||||
on_run_end = project_dict.get('on-run-end', [])
|
||||
seeds = project_dict.get('seeds', {})
|
||||
snapshots = project_dict.get('snapshots', {})
|
||||
dbt_raw_version = project_dict.get('require-dbt-version', '>=0.0.0')
|
||||
query_comment = project_dict.get('query-comment', NoValue())
|
||||
dispatch: List[Dict[str, Any]]
|
||||
models: Dict[str, Any]
|
||||
seeds: Dict[str, Any]
|
||||
snapshots: Dict[str, Any]
|
||||
sources: Dict[str, Any]
|
||||
tests: Dict[str, Any]
|
||||
vars_value: VarProvider
|
||||
|
||||
try:
|
||||
dbt_version = _parse_versions(dbt_raw_version)
|
||||
except SemverException as e:
|
||||
raise DbtProjectError(str(e)) from e
|
||||
dispatch = cfg.dispatch
|
||||
models = cfg.models
|
||||
seeds = cfg.seeds
|
||||
snapshots = cfg.snapshots
|
||||
sources = cfg.sources
|
||||
tests = cfg.tests
|
||||
if cfg.vars is None:
|
||||
vars_dict: Dict[str, Any] = {}
|
||||
else:
|
||||
vars_dict = cfg.vars
|
||||
|
||||
try:
|
||||
packages = package_config_from_data(packages_dict)
|
||||
except ValidationError as e:
|
||||
raise DbtProjectError(validator_error_message(e)) from e
|
||||
vars_value = VarProvider(vars_dict)
|
||||
# There will never be any project_env_vars when it's first created
|
||||
project_env_vars: Dict[str, Any] = {}
|
||||
on_run_start: List[str] = value_or(cfg.on_run_start, [])
|
||||
on_run_end: List[str] = value_or(cfg.on_run_end, [])
|
||||
|
||||
project = cls(
|
||||
query_comment = _query_comment_from_cfg(cfg.query_comment)
|
||||
|
||||
packages = package_config_from_data(rendered.packages_dict)
|
||||
selectors = selector_config_from_data(rendered.selectors_dict)
|
||||
manifest_selectors: Dict[str, Any] = {}
|
||||
if rendered.selectors_dict and rendered.selectors_dict['selectors']:
|
||||
# this is a dict with a single key 'selectors' pointing to a list
|
||||
# of dicts.
|
||||
manifest_selectors = SelectorDict.parse_from_selectors_list(
|
||||
rendered.selectors_dict['selectors'])
|
||||
project = Project(
|
||||
project_name=name,
|
||||
version=version,
|
||||
project_root=project_root,
|
||||
profile_name=profile_name,
|
||||
source_paths=source_paths,
|
||||
model_paths=model_paths,
|
||||
macro_paths=macro_paths,
|
||||
data_paths=data_paths,
|
||||
seed_paths=seed_paths,
|
||||
test_paths=test_paths,
|
||||
analysis_paths=analysis_paths,
|
||||
docs_paths=docs_paths,
|
||||
asset_paths=asset_paths,
|
||||
target_path=target_path,
|
||||
snapshot_paths=snapshot_paths,
|
||||
clean_targets=clean_targets,
|
||||
log_path=log_path,
|
||||
modules_path=modules_path,
|
||||
packages_install_path=packages_install_path,
|
||||
quoting=quoting,
|
||||
models=models,
|
||||
on_run_start=on_run_start,
|
||||
on_run_end=on_run_end,
|
||||
dispatch=dispatch,
|
||||
seeds=seeds,
|
||||
snapshots=snapshots,
|
||||
dbt_version=dbt_version,
|
||||
packages=packages,
|
||||
manifest_selectors=manifest_selectors,
|
||||
selectors=selectors,
|
||||
query_comment=query_comment,
|
||||
sources=sources,
|
||||
tests=tests,
|
||||
vars=vars_value,
|
||||
config_version=cfg.config_version,
|
||||
unrendered=unrendered,
|
||||
project_env_vars=project_env_vars,
|
||||
)
|
||||
# sanity check - this means an internal issue
|
||||
project.validate()
|
||||
return project
|
||||
|
||||
@classmethod
|
||||
def from_dicts(
|
||||
cls,
|
||||
project_root: str,
|
||||
project_dict: Dict[str, Any],
|
||||
packages_dict: Dict[str, Any],
|
||||
selectors_dict: Dict[str, Any],
|
||||
*,
|
||||
verify_version: bool = False,
|
||||
):
|
||||
"""Construct a partial project from its constituent dicts.
|
||||
"""
|
||||
project_name = project_dict.get('name')
|
||||
profile_name = project_dict.get('profile')
|
||||
|
||||
return cls(
|
||||
profile_name=profile_name,
|
||||
project_name=project_name,
|
||||
project_root=project_root,
|
||||
project_dict=project_dict,
|
||||
packages_dict=packages_dict,
|
||||
selectors_dict=selectors_dict,
|
||||
verify_version=verify_version,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_project_root(
|
||||
cls, project_root: str, *, verify_version: bool = False
|
||||
) -> 'PartialProject':
|
||||
project_root = os.path.normpath(project_root)
|
||||
project_dict = _raw_project_from(project_root)
|
||||
config_version = project_dict.get('config-version', 1)
|
||||
if config_version != 2:
|
||||
raise DbtProjectError(
|
||||
f'Invalid config version: {config_version}, expected 2',
|
||||
path=os.path.join(project_root, 'dbt_project.yml')
|
||||
)
|
||||
|
||||
packages_dict = package_data_from_root(project_root)
|
||||
selectors_dict = selector_data_from_root(project_root)
|
||||
return cls.from_dicts(
|
||||
project_root=project_root,
|
||||
project_dict=project_dict,
|
||||
selectors_dict=selectors_dict,
|
||||
packages_dict=packages_dict,
|
||||
verify_version=verify_version,
|
||||
)
|
||||
|
||||
|
||||
class VarProvider:
|
||||
"""Var providers are tied to a particular Project."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vars: Dict[str, Dict[str, Any]]
|
||||
) -> None:
|
||||
self.vars = vars
|
||||
|
||||
def vars_for(
|
||||
self, node: IsFQNResource, adapter_type: str
|
||||
) -> Mapping[str, Any]:
|
||||
# in v2, vars are only either project or globally scoped
|
||||
merged = MultiDict([self.vars])
|
||||
merged.add(self.vars.get(node.package_name, {}))
|
||||
return merged
|
||||
|
||||
def to_dict(self):
|
||||
return self.vars
|
||||
|
||||
|
||||
# The Project class is included in RuntimeConfig, so any attribute
|
||||
# additions must also be set where the RuntimeConfig class is created
|
||||
@dataclass
|
||||
class Project:
|
||||
project_name: str
|
||||
version: Union[SemverString, float]
|
||||
project_root: str
|
||||
profile_name: Optional[str]
|
||||
model_paths: List[str]
|
||||
macro_paths: List[str]
|
||||
seed_paths: List[str]
|
||||
test_paths: List[str]
|
||||
analysis_paths: List[str]
|
||||
docs_paths: List[str]
|
||||
asset_paths: List[str]
|
||||
target_path: str
|
||||
snapshot_paths: List[str]
|
||||
clean_targets: List[str]
|
||||
log_path: str
|
||||
packages_install_path: str
|
||||
quoting: Dict[str, Any]
|
||||
models: Dict[str, Any]
|
||||
on_run_start: List[str]
|
||||
on_run_end: List[str]
|
||||
dispatch: List[Dict[str, Any]]
|
||||
seeds: Dict[str, Any]
|
||||
snapshots: Dict[str, Any]
|
||||
sources: Dict[str, Any]
|
||||
tests: Dict[str, Any]
|
||||
vars: VarProvider
|
||||
dbt_version: List[VersionSpecifier]
|
||||
packages: Dict[str, Any]
|
||||
manifest_selectors: Dict[str, Any]
|
||||
selectors: SelectorConfig
|
||||
query_comment: QueryComment
|
||||
config_version: int
|
||||
unrendered: RenderComponents
|
||||
project_env_vars: Dict[str, Any]
|
||||
|
||||
@property
|
||||
def all_source_paths(self) -> List[str]:
|
||||
return _all_source_paths(
|
||||
self.model_paths, self.seed_paths, self.snapshot_paths,
|
||||
self.analysis_paths, self.macro_paths
|
||||
)
|
||||
|
||||
@property
|
||||
def generic_test_paths(self):
|
||||
generic_test_paths = []
|
||||
for test_path in self.test_paths:
|
||||
generic_test_paths.append(os.path.join(test_path, 'generic'))
|
||||
return generic_test_paths
|
||||
|
||||
def __str__(self):
|
||||
cfg = self.to_project_config(with_packages=True)
|
||||
return str(cfg)
|
||||
@@ -325,12 +600,13 @@ class Project:
|
||||
'version': self.version,
|
||||
'project-root': self.project_root,
|
||||
'profile': self.profile_name,
|
||||
'source-paths': self.source_paths,
|
||||
'model-paths': self.model_paths,
|
||||
'macro-paths': self.macro_paths,
|
||||
'data-paths': self.data_paths,
|
||||
'seed-paths': self.seed_paths,
|
||||
'test-paths': self.test_paths,
|
||||
'analysis-paths': self.analysis_paths,
|
||||
'docs-paths': self.docs_paths,
|
||||
'asset-paths': self.asset_paths,
|
||||
'target-path': self.target_path,
|
||||
'snapshot-paths': self.snapshot_paths,
|
||||
'clean-targets': self.clean_targets,
|
||||
@@ -339,128 +615,76 @@ class Project:
|
||||
'models': self.models,
|
||||
'on-run-start': self.on_run_start,
|
||||
'on-run-end': self.on_run_end,
|
||||
'dispatch': self.dispatch,
|
||||
'seeds': self.seeds,
|
||||
'snapshots': self.snapshots,
|
||||
'sources': self.sources,
|
||||
'tests': self.tests,
|
||||
'vars': self.vars.to_dict(),
|
||||
'require-dbt-version': [
|
||||
v.to_version_string() for v in self.dbt_version
|
||||
],
|
||||
'config-version': self.config_version,
|
||||
})
|
||||
if self.query_comment:
|
||||
result['query-comment'] = \
|
||||
self.query_comment.to_dict(omit_none=True)
|
||||
|
||||
if with_packages:
|
||||
result.update(self.packages.to_dict())
|
||||
if self.query_comment != NoValue():
|
||||
result['query-comment'] = self.query_comment
|
||||
result.update(self.packages.to_dict(omit_none=True))
|
||||
|
||||
return result
|
||||
|
||||
def validate(self):
|
||||
try:
|
||||
ProjectContract.from_dict(self.to_project_config())
|
||||
ProjectContract.validate(self.to_project_config())
|
||||
except ValidationError as e:
|
||||
raise DbtProjectError(validator_error_message(e)) from e
|
||||
|
||||
@classmethod
|
||||
def from_project_root(cls, project_root, cli_vars):
|
||||
"""Create a project from a root directory. Reads in dbt_project.yml and
|
||||
packages.yml, if it exists.
|
||||
|
||||
:param project_root str: The path to the project root to load.
|
||||
:raises DbtProjectError: If the project is missing or invalid, or if
|
||||
the packages file exists and is invalid.
|
||||
:returns Project: The project, with defaults populated.
|
||||
"""
|
||||
project_root = os.path.normpath(project_root)
|
||||
project_yaml_filepath = os.path.join(project_root, 'dbt_project.yml')
|
||||
|
||||
# get the project.yml contents
|
||||
if not path_exists(project_yaml_filepath):
|
||||
raise DbtProjectError(
|
||||
'no dbt_project.yml found at expected path {}'
|
||||
.format(project_yaml_filepath)
|
||||
)
|
||||
|
||||
if isinstance(cli_vars, str):
|
||||
cli_vars = parse_cli_vars(cli_vars)
|
||||
renderer = ConfigRenderer(cli_vars)
|
||||
|
||||
project_dict = _load_yaml(project_yaml_filepath)
|
||||
rendered_project = renderer.render_project(project_dict)
|
||||
rendered_project['project-root'] = project_root
|
||||
packages_dict = package_data_from_root(project_root)
|
||||
return cls.from_project_config(rendered_project, packages_dict)
|
||||
def partial_load(
|
||||
cls, project_root: str, *, verify_version: bool = False
|
||||
) -> PartialProject:
|
||||
return PartialProject.from_project_root(
|
||||
project_root,
|
||||
verify_version=verify_version,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_current_directory(cls, cli_vars):
|
||||
return cls.from_project_root(os.getcwd(), cli_vars)
|
||||
|
||||
@classmethod
|
||||
def from_args(cls, args):
|
||||
return cls.from_current_directory(getattr(args, 'vars', '{}'))
|
||||
def from_project_root(
|
||||
cls,
|
||||
project_root: str,
|
||||
renderer: DbtProjectYamlRenderer,
|
||||
*,
|
||||
verify_version: bool = False,
|
||||
) -> 'Project':
|
||||
partial = cls.partial_load(project_root, verify_version=verify_version)
|
||||
return partial.render(renderer)
|
||||
|
||||
def hashed_name(self):
|
||||
return hashlib.md5(self.project_name.encode('utf-8')).hexdigest()
|
||||
|
||||
def get_resource_config_paths(self):
|
||||
"""Return a dictionary with 'seeds' and 'models' keys whose values are
|
||||
lists of lists of strings, where each inner list of strings represents
|
||||
a configured path in the resource.
|
||||
"""
|
||||
return {
|
||||
'models': _get_config_paths(self.models),
|
||||
'seeds': _get_config_paths(self.seeds),
|
||||
'snapshots': _get_config_paths(self.snapshots),
|
||||
}
|
||||
|
||||
def get_unused_resource_config_paths(self, resource_fqns, disabled):
|
||||
"""Return a list of lists of strings, where each inner list of strings
|
||||
represents a type + FQN path of a resource configuration that is not
|
||||
used.
|
||||
"""
|
||||
disabled_fqns = frozenset(tuple(fqn) for fqn in disabled)
|
||||
resource_config_paths = self.get_resource_config_paths()
|
||||
unused_resource_config_paths = []
|
||||
for resource_type, config_paths in resource_config_paths.items():
|
||||
used_fqns = resource_fqns.get(resource_type, frozenset())
|
||||
fqns = used_fqns | disabled_fqns
|
||||
|
||||
for config_path in config_paths:
|
||||
if not _is_config_used(config_path, fqns):
|
||||
unused_resource_config_paths.append(
|
||||
(resource_type,) + config_path
|
||||
)
|
||||
return unused_resource_config_paths
|
||||
|
||||
def warn_for_unused_resource_config_paths(self, resource_fqns, disabled):
|
||||
unused = self.get_unused_resource_config_paths(resource_fqns, disabled)
|
||||
if len(unused) == 0:
|
||||
return
|
||||
|
||||
msg = UNUSED_RESOURCE_CONFIGURATION_PATH_MESSAGE.format(
|
||||
len(unused),
|
||||
'\n'.join('- {}'.format('.'.join(u)) for u in unused)
|
||||
)
|
||||
warn_or_error(msg, log_fmt=printer.yellow('{}'))
|
||||
|
||||
def validate_version(self):
|
||||
"""Ensure this package works with the installed version of dbt."""
|
||||
installed = get_installed_version()
|
||||
if not versions_compatible(*self.dbt_version):
|
||||
msg = IMPOSSIBLE_VERSION_ERROR.format(
|
||||
package=self.project_name,
|
||||
version_spec=[
|
||||
x.to_version_string() for x in self.dbt_version
|
||||
]
|
||||
def get_selector(self, name: str) -> Union[SelectionSpec, bool]:
|
||||
if name not in self.selectors:
|
||||
raise RuntimeException(
|
||||
f'Could not find selector named {name}, expected one of '
|
||||
f'{list(self.selectors)}'
|
||||
)
|
||||
raise DbtProjectError(msg)
|
||||
return self.selectors[name]["definition"]
|
||||
|
||||
if not versions_compatible(installed, *self.dbt_version):
|
||||
msg = INVALID_VERSION_ERROR.format(
|
||||
package=self.project_name,
|
||||
installed=installed.to_version_string(),
|
||||
version_spec=[
|
||||
x.to_version_string() for x in self.dbt_version
|
||||
]
|
||||
)
|
||||
raise DbtProjectError(msg)
|
||||
def get_default_selector_name(self) -> Union[str, None]:
|
||||
"""This function fetch the default selector to use on `dbt run` (if any)
|
||||
:return: either a selector if default is set or None
|
||||
:rtype: Union[SelectionSpec, None]
|
||||
"""
|
||||
for selector_name, selector in self.selectors.items():
|
||||
if selector["default"] is True:
|
||||
return selector_name
|
||||
|
||||
def get_metadata(self) -> ManifestMetadata:
|
||||
return ManifestMetadata(self.hashed_name())
|
||||
return None
|
||||
|
||||
def get_macro_search_order(self, macro_namespace: str):
|
||||
for dispatch_entry in self.dispatch:
|
||||
if dispatch_entry['macro_namespace'] == macro_namespace:
|
||||
return dispatch_entry['search_order']
|
||||
return None
|
||||
|
||||
@@ -1,109 +1,211 @@
|
||||
from dbt.clients.jinja import get_rendered
|
||||
from dbt.context.base import ConfigRenderContext
|
||||
from dbt.exceptions import DbtProfileError
|
||||
from dbt.exceptions import DbtProjectError
|
||||
from dbt.exceptions import RecursionException
|
||||
from dbt.utils import deep_map
|
||||
from typing import Dict, Any, Tuple, Optional, Union, Callable
|
||||
|
||||
from dbt.clients.jinja import get_rendered, catch_jinja
|
||||
from dbt.context.target import TargetContext
|
||||
from dbt.context.secret import SecretContext
|
||||
from dbt.context.base import BaseContext
|
||||
from dbt.contracts.connection import HasCredentials
|
||||
from dbt.exceptions import (
|
||||
DbtProjectError, CompilationException, RecursionException
|
||||
)
|
||||
from dbt.utils import deep_map_render
|
||||
|
||||
|
||||
class ConfigRenderer:
|
||||
"""A renderer provides configuration rendering for a given set of cli
|
||||
variables and a render type.
|
||||
"""
|
||||
def __init__(self, cli_vars):
|
||||
self.context = ConfigRenderContext(cli_vars).to_dict()
|
||||
Keypath = Tuple[Union[str, int], ...]
|
||||
|
||||
@staticmethod
|
||||
def _is_deferred_render(keypath):
|
||||
if not keypath:
|
||||
return False
|
||||
|
||||
first = keypath[0]
|
||||
# run hooks
|
||||
if first in {'on-run-start', 'on-run-end', 'query-comment'}:
|
||||
return True
|
||||
# models have two things to avoid
|
||||
if first in {'seeds', 'models', 'snapshots'}:
|
||||
# model-level hooks
|
||||
if 'pre-hook' in keypath or 'post-hook' in keypath:
|
||||
return True
|
||||
# model-level 'vars' declarations
|
||||
if 'vars' in keypath:
|
||||
return True
|
||||
class BaseRenderer:
|
||||
def __init__(self, context: Dict[str, Any]) -> None:
|
||||
self.context = context
|
||||
|
||||
return False
|
||||
@property
|
||||
def name(self):
|
||||
return 'Rendering'
|
||||
|
||||
def _render_project_entry(self, value, keypath):
|
||||
"""Render an entry, in case it's jinja. This is meant to be passed to
|
||||
deep_map.
|
||||
def should_render_keypath(self, keypath: Keypath) -> bool:
|
||||
return True
|
||||
|
||||
If the parsed entry is a string and has the name 'port', this will
|
||||
attempt to cast it to an int, and on failure will return the parsed
|
||||
string.
|
||||
|
||||
:param value Any: The value to potentially render
|
||||
:param key str: The key to convert on.
|
||||
:return Any: The rendered entry.
|
||||
"""
|
||||
# query comments and hooks should be treated as raw sql, they'll get
|
||||
# rendered later.
|
||||
# Same goes for 'vars' declarations inside 'models'/'seeds'
|
||||
if self._is_deferred_render(keypath):
|
||||
def render_entry(self, value: Any, keypath: Keypath) -> Any:
|
||||
if not self.should_render_keypath(keypath):
|
||||
return value
|
||||
|
||||
return self.render_value(value)
|
||||
return self.render_value(value, keypath)
|
||||
|
||||
def render_value(self, value, keypath=None):
|
||||
def render_value(
|
||||
self, value: Any, keypath: Optional[Keypath] = None
|
||||
) -> Any:
|
||||
# keypath is ignored.
|
||||
# if it wasn't read as a string, ignore it
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
return str(get_rendered(value, self.context))
|
||||
|
||||
def _render_profile_data(self, value, keypath):
|
||||
result = self.render_value(value)
|
||||
if len(keypath) == 1 and keypath[-1] == 'port':
|
||||
try:
|
||||
result = int(result)
|
||||
except ValueError:
|
||||
# let the validator or connection handle this
|
||||
pass
|
||||
return result
|
||||
|
||||
def _render_schema_source_data(self, value, keypath):
|
||||
# things to not render:
|
||||
# - descriptions
|
||||
if len(keypath) > 0 and keypath[-1] == 'description':
|
||||
return value
|
||||
|
||||
return self.render_value(value)
|
||||
|
||||
def render_project(self, as_parsed):
|
||||
"""Render the parsed data, returning a new dict (or whatever was read).
|
||||
"""
|
||||
try:
|
||||
return deep_map(self._render_project_entry, as_parsed)
|
||||
with catch_jinja():
|
||||
return get_rendered(value, self.context, native=True)
|
||||
except CompilationException as exc:
|
||||
msg = f'Could not render {value}: {exc.msg}'
|
||||
raise CompilationException(msg) from exc
|
||||
|
||||
def render_data(
|
||||
self, data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
return deep_map_render(self.render_entry, data)
|
||||
except RecursionException:
|
||||
raise DbtProjectError(
|
||||
'Cycle detected: Project input has a reference to itself',
|
||||
project=as_parsed
|
||||
f'Cycle detected: {self.name} input has a reference to itself',
|
||||
project=data
|
||||
)
|
||||
|
||||
def render_profile_data(self, as_parsed):
|
||||
"""Render the chosen profile entry, as it was parsed."""
|
||||
try:
|
||||
return deep_map(self._render_profile_data, as_parsed)
|
||||
except RecursionException:
|
||||
raise DbtProfileError(
|
||||
'Cycle detected: Profile input has a reference to itself',
|
||||
project=as_parsed
|
||||
)
|
||||
|
||||
def render_schema_source(self, as_parsed):
|
||||
try:
|
||||
return deep_map(self._render_schema_source_data, as_parsed)
|
||||
except RecursionException:
|
||||
raise DbtProfileError(
|
||||
'Cycle detected: schema.yml input has a reference to itself',
|
||||
project=as_parsed
|
||||
)
|
||||
def _list_if_none(value):
|
||||
if value is None:
|
||||
value = []
|
||||
return value
|
||||
|
||||
|
||||
def _dict_if_none(value):
|
||||
if value is None:
|
||||
value = {}
|
||||
return value
|
||||
|
||||
|
||||
def _list_if_none_or_string(value):
|
||||
value = _list_if_none(value)
|
||||
if isinstance(value, str):
|
||||
return [value]
|
||||
return value
|
||||
|
||||
|
||||
class ProjectPostprocessor(Dict[Keypath, Callable[[Any], Any]]):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self[('on-run-start',)] = _list_if_none_or_string
|
||||
self[('on-run-end',)] = _list_if_none_or_string
|
||||
|
||||
for k in ('models', 'seeds', 'snapshots'):
|
||||
self[(k,)] = _dict_if_none
|
||||
self[(k, 'vars')] = _dict_if_none
|
||||
self[(k, 'pre-hook')] = _list_if_none_or_string
|
||||
self[(k, 'post-hook')] = _list_if_none_or_string
|
||||
self[('seeds', 'column_types')] = _dict_if_none
|
||||
|
||||
def postprocess(self, value: Any, key: Keypath) -> Any:
|
||||
if key in self:
|
||||
handler = self[key]
|
||||
return handler(value)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class DbtProjectYamlRenderer(BaseRenderer):
|
||||
_KEYPATH_HANDLERS = ProjectPostprocessor()
|
||||
|
||||
def __init__(
|
||||
self, profile: Optional[HasCredentials] = None,
|
||||
cli_vars: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
# Generate contexts here because we want to save the context
|
||||
# object in order to retrieve the env_vars. This is almost always
|
||||
# a TargetContext, but in the debug task we want a project
|
||||
# even when we don't have a profile.
|
||||
if cli_vars is None:
|
||||
cli_vars = {}
|
||||
if profile:
|
||||
self.ctx_obj = TargetContext(profile, cli_vars)
|
||||
else:
|
||||
self.ctx_obj = BaseContext(cli_vars) # type:ignore
|
||||
context = self.ctx_obj.to_dict()
|
||||
super().__init__(context)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
'Project config'
|
||||
|
||||
def get_package_renderer(self) -> BaseRenderer:
|
||||
return PackageRenderer(self.context)
|
||||
|
||||
def get_selector_renderer(self) -> BaseRenderer:
|
||||
return SelectorRenderer(self.context)
|
||||
|
||||
def render_project(
|
||||
self,
|
||||
project: Dict[str, Any],
|
||||
project_root: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Render the project and insert the project root after rendering."""
|
||||
rendered_project = self.render_data(project)
|
||||
rendered_project['project-root'] = project_root
|
||||
return rendered_project
|
||||
|
||||
def render_packages(self, packages: Dict[str, Any]):
|
||||
"""Render the given packages dict"""
|
||||
package_renderer = self.get_package_renderer()
|
||||
return package_renderer.render_data(packages)
|
||||
|
||||
def render_selectors(self, selectors: Dict[str, Any]):
|
||||
selector_renderer = self.get_selector_renderer()
|
||||
return selector_renderer.render_data(selectors)
|
||||
|
||||
def render_entry(self, value: Any, keypath: Keypath) -> Any:
|
||||
result = super().render_entry(value, keypath)
|
||||
return self._KEYPATH_HANDLERS.postprocess(result, keypath)
|
||||
|
||||
def should_render_keypath(self, keypath: Keypath) -> bool:
|
||||
if not keypath:
|
||||
return True
|
||||
|
||||
first = keypath[0]
|
||||
# run hooks are not rendered
|
||||
if first in {'on-run-start', 'on-run-end', 'query-comment'}:
|
||||
return False
|
||||
|
||||
# don't render vars blocks until runtime
|
||||
if first == 'vars':
|
||||
return False
|
||||
|
||||
if first in {'seeds', 'models', 'snapshots', 'tests'}:
|
||||
keypath_parts = {
|
||||
(k.lstrip('+ ') if isinstance(k, str) else k)
|
||||
for k in keypath
|
||||
}
|
||||
# model-level hooks
|
||||
if 'pre-hook' in keypath_parts or 'post-hook' in keypath_parts:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class SelectorRenderer(BaseRenderer):
|
||||
@property
|
||||
def name(self):
|
||||
return 'Selector config'
|
||||
|
||||
|
||||
class SecretRenderer(BaseRenderer):
|
||||
def __init__(
|
||||
self, cli_vars: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
# Generate contexts here because we want to save the context
|
||||
# object in order to retrieve the env_vars.
|
||||
if cli_vars is None:
|
||||
cli_vars = {}
|
||||
self.ctx_obj = SecretContext(cli_vars)
|
||||
context = self.ctx_obj.to_dict()
|
||||
super().__init__(context)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return 'Secret'
|
||||
|
||||
|
||||
class ProfileRenderer(SecretRenderer):
|
||||
@property
|
||||
def name(self):
|
||||
return 'Profile'
|
||||
|
||||
|
||||
class PackageRenderer(SecretRenderer):
|
||||
@property
|
||||
def name(self):
|
||||
return 'Packages config'
|
||||
|
||||
@@ -1,142 +1,163 @@
|
||||
import itertools
|
||||
import os
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Dict, Any, Optional, Mapping, Iterator, Iterable, Tuple, List, MutableSet,
|
||||
Type
|
||||
)
|
||||
|
||||
from .profile import Profile
|
||||
from .project import Project
|
||||
from dbt.utils import parse_cli_vars
|
||||
from dbt.contracts.project import Configuration
|
||||
from dbt.exceptions import DbtProjectError
|
||||
from dbt.exceptions import validator_error_message
|
||||
from dbt.adapters.factory import get_relation_class_by_name
|
||||
from .renderer import DbtProjectYamlRenderer, ProfileRenderer
|
||||
from .utils import parse_cli_vars
|
||||
from dbt import flags
|
||||
from dbt.adapters.factory import get_relation_class_by_name, get_include_paths
|
||||
from dbt.helper_types import FQNPath, PathSet
|
||||
from dbt.config.profile import read_user_config
|
||||
from dbt.contracts.connection import AdapterRequiredConfig, Credentials
|
||||
from dbt.contracts.graph.manifest import ManifestMetadata
|
||||
from dbt.contracts.relation import ComponentName
|
||||
from dbt.ui import warning_tag
|
||||
|
||||
from dbt.contracts.project import Configuration, UserConfig
|
||||
from dbt.exceptions import (
|
||||
RuntimeException,
|
||||
DbtProjectError,
|
||||
validator_error_message,
|
||||
warn_or_error,
|
||||
raise_compiler_error
|
||||
)
|
||||
|
||||
from dbt.dataclass_schema import ValidationError
|
||||
|
||||
|
||||
from hologram import ValidationError
|
||||
def _project_quoting_dict(
|
||||
proj: Project, profile: Profile
|
||||
) -> Dict[ComponentName, bool]:
|
||||
src: Dict[str, Any] = profile.credentials.translate_aliases(proj.quoting)
|
||||
result: Dict[ComponentName, bool] = {}
|
||||
for key in ComponentName:
|
||||
if key in src:
|
||||
value = src[key]
|
||||
if isinstance(value, bool):
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
class RuntimeConfig(Project, Profile):
|
||||
"""The runtime configuration, as constructed from its components. There's a
|
||||
lot because there is a lot of stuff!
|
||||
"""
|
||||
def __init__(self, project_name, version, project_root, source_paths,
|
||||
macro_paths, data_paths, test_paths, analysis_paths,
|
||||
docs_paths, target_path, snapshot_paths, clean_targets,
|
||||
log_path, modules_path, quoting, models, on_run_start,
|
||||
on_run_end, seeds, snapshots, dbt_version, profile_name,
|
||||
target_name, config, threads, credentials, packages,
|
||||
query_comment, args):
|
||||
# 'vars'
|
||||
self.args = args
|
||||
self.cli_vars = parse_cli_vars(getattr(args, 'vars', '{}'))
|
||||
# 'project'
|
||||
Project.__init__(
|
||||
self,
|
||||
project_name=project_name,
|
||||
version=version,
|
||||
project_root=project_root,
|
||||
profile_name=profile_name,
|
||||
source_paths=source_paths,
|
||||
macro_paths=macro_paths,
|
||||
data_paths=data_paths,
|
||||
test_paths=test_paths,
|
||||
analysis_paths=analysis_paths,
|
||||
docs_paths=docs_paths,
|
||||
target_path=target_path,
|
||||
snapshot_paths=snapshot_paths,
|
||||
clean_targets=clean_targets,
|
||||
log_path=log_path,
|
||||
modules_path=modules_path,
|
||||
quoting=quoting,
|
||||
models=models,
|
||||
on_run_start=on_run_start,
|
||||
on_run_end=on_run_end,
|
||||
seeds=seeds,
|
||||
snapshots=snapshots,
|
||||
dbt_version=dbt_version,
|
||||
packages=packages,
|
||||
query_comment=query_comment,
|
||||
)
|
||||
# 'profile'
|
||||
Profile.__init__(
|
||||
self,
|
||||
profile_name=profile_name,
|
||||
target_name=target_name,
|
||||
config=config,
|
||||
threads=threads,
|
||||
credentials=credentials
|
||||
)
|
||||
@dataclass
|
||||
class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
|
||||
args: Any
|
||||
profile_name: str
|
||||
cli_vars: Dict[str, Any]
|
||||
dependencies: Optional[Mapping[str, 'RuntimeConfig']] = None
|
||||
|
||||
def __post_init__(self):
|
||||
self.validate()
|
||||
|
||||
# Called by 'new_project' and 'from_args'
|
||||
@classmethod
|
||||
def from_parts(cls, project, profile, args):
|
||||
def from_parts(
|
||||
cls,
|
||||
project: Project,
|
||||
profile: Profile,
|
||||
args: Any,
|
||||
dependencies: Optional[Mapping[str, 'RuntimeConfig']] = None,
|
||||
) -> 'RuntimeConfig':
|
||||
"""Instantiate a RuntimeConfig from its components.
|
||||
|
||||
:param profile Profile: A parsed dbt Profile.
|
||||
:param project Project: A parsed dbt Project.
|
||||
:param args argparse.Namespace: The parsed command-line arguments.
|
||||
:param profile: A parsed dbt Profile.
|
||||
:param project: A parsed dbt Project.
|
||||
:param args: The parsed command-line arguments.
|
||||
:returns RuntimeConfig: The new configuration.
|
||||
"""
|
||||
quoting = (
|
||||
quoting: Dict[str, Any] = (
|
||||
get_relation_class_by_name(profile.credentials.type)
|
||||
.get_default_quote_policy()
|
||||
.replace_dict(project.quoting)
|
||||
).to_dict()
|
||||
.replace_dict(_project_quoting_dict(project, profile))
|
||||
).to_dict(omit_none=True)
|
||||
|
||||
cli_vars: Dict[str, Any] = parse_cli_vars(getattr(args, 'vars', '{}'))
|
||||
|
||||
return cls(
|
||||
project_name=project.project_name,
|
||||
version=project.version,
|
||||
project_root=project.project_root,
|
||||
source_paths=project.source_paths,
|
||||
model_paths=project.model_paths,
|
||||
macro_paths=project.macro_paths,
|
||||
data_paths=project.data_paths,
|
||||
seed_paths=project.seed_paths,
|
||||
test_paths=project.test_paths,
|
||||
analysis_paths=project.analysis_paths,
|
||||
docs_paths=project.docs_paths,
|
||||
asset_paths=project.asset_paths,
|
||||
target_path=project.target_path,
|
||||
snapshot_paths=project.snapshot_paths,
|
||||
clean_targets=project.clean_targets,
|
||||
log_path=project.log_path,
|
||||
modules_path=project.modules_path,
|
||||
packages_install_path=project.packages_install_path,
|
||||
quoting=quoting,
|
||||
models=project.models,
|
||||
on_run_start=project.on_run_start,
|
||||
on_run_end=project.on_run_end,
|
||||
dispatch=project.dispatch,
|
||||
seeds=project.seeds,
|
||||
snapshots=project.snapshots,
|
||||
dbt_version=project.dbt_version,
|
||||
packages=project.packages,
|
||||
manifest_selectors=project.manifest_selectors,
|
||||
selectors=project.selectors,
|
||||
query_comment=project.query_comment,
|
||||
sources=project.sources,
|
||||
tests=project.tests,
|
||||
vars=project.vars,
|
||||
config_version=project.config_version,
|
||||
unrendered=project.unrendered,
|
||||
project_env_vars=project.project_env_vars,
|
||||
profile_env_vars=profile.profile_env_vars,
|
||||
profile_name=profile.profile_name,
|
||||
target_name=profile.target_name,
|
||||
config=profile.config,
|
||||
user_config=profile.user_config,
|
||||
threads=profile.threads,
|
||||
credentials=profile.credentials,
|
||||
args=args
|
||||
args=args,
|
||||
cli_vars=cli_vars,
|
||||
dependencies=dependencies,
|
||||
)
|
||||
|
||||
def new_project(self, project_root):
|
||||
# Called by 'load_projects' in this class
|
||||
def new_project(self, project_root: str) -> 'RuntimeConfig':
|
||||
"""Given a new project root, read in its project dictionary, supply the
|
||||
existing project's profile info, and create a new project file.
|
||||
|
||||
:param project_root str: A filepath to a dbt project.
|
||||
:param project_root: A filepath to a dbt project.
|
||||
:raises DbtProfileError: If the profile is invalid.
|
||||
:raises DbtProjectError: If project is missing or invalid.
|
||||
:returns RuntimeConfig: The new configuration.
|
||||
:returns: The new configuration.
|
||||
"""
|
||||
# copy profile
|
||||
profile = Profile(**self.to_profile_info())
|
||||
profile.validate()
|
||||
# load the new project and its packages. Don't pass cli variables.
|
||||
project = Project.from_project_root(project_root, {})
|
||||
|
||||
cfg = self.from_parts(
|
||||
# load the new project and its packages. Don't pass cli variables.
|
||||
renderer = DbtProjectYamlRenderer(profile)
|
||||
|
||||
project = Project.from_project_root(
|
||||
project_root,
|
||||
renderer,
|
||||
verify_version=bool(flags.VERSION_CHECK),
|
||||
)
|
||||
|
||||
runtime_config = self.from_parts(
|
||||
project=project,
|
||||
profile=profile,
|
||||
args=deepcopy(self.args),
|
||||
)
|
||||
# force our quoting back onto the new project.
|
||||
cfg.quoting = deepcopy(self.quoting)
|
||||
return cfg
|
||||
runtime_config.quoting = deepcopy(self.quoting)
|
||||
return runtime_config
|
||||
|
||||
def serialize(self):
|
||||
def serialize(self) -> Dict[str, Any]:
|
||||
"""Serialize the full configuration to a single dictionary. For any
|
||||
instance that has passed validate() (which happens in __init__), it
|
||||
matches the Configuration contract.
|
||||
@@ -150,44 +171,405 @@ class RuntimeConfig(Project, Profile):
|
||||
result['cli_vars'] = deepcopy(self.cli_vars)
|
||||
return result
|
||||
|
||||
def __str__(self):
|
||||
return str(self.serialize())
|
||||
|
||||
def validate(self):
|
||||
"""Validate the configuration against its contract.
|
||||
|
||||
:raises DbtProjectError: If the configuration fails validation.
|
||||
"""
|
||||
try:
|
||||
Configuration.from_dict(self.serialize())
|
||||
Configuration.validate(self.serialize())
|
||||
except ValidationError as e:
|
||||
raise DbtProjectError(validator_error_message(e)) from e
|
||||
|
||||
if getattr(self.args, 'version_check', False):
|
||||
self.validate_version()
|
||||
@classmethod
|
||||
def _get_rendered_profile(
|
||||
cls,
|
||||
args: Any,
|
||||
profile_renderer: ProfileRenderer,
|
||||
profile_name: Optional[str],
|
||||
) -> Profile:
|
||||
|
||||
return Profile.render_from_args(
|
||||
args, profile_renderer, profile_name
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_args(cls, args):
|
||||
def collect_parts(
|
||||
cls: Type['RuntimeConfig'], args: Any
|
||||
) -> Tuple[Project, Profile]:
|
||||
# profile_name from the project
|
||||
project_root = args.project_dir if args.project_dir else os.getcwd()
|
||||
version_check = bool(flags.VERSION_CHECK)
|
||||
partial = Project.partial_load(
|
||||
project_root,
|
||||
verify_version=version_check
|
||||
)
|
||||
|
||||
# build the profile using the base renderer and the one fact we know
|
||||
# Note: only the named profile section is rendered. The rest of the
|
||||
# profile is ignored.
|
||||
cli_vars: Dict[str, Any] = parse_cli_vars(getattr(args, 'vars', '{}'))
|
||||
profile_renderer = ProfileRenderer(cli_vars)
|
||||
profile_name = partial.render_profile_name(profile_renderer)
|
||||
profile = cls._get_rendered_profile(
|
||||
args, profile_renderer, profile_name
|
||||
)
|
||||
# Save env_vars encountered in rendering for partial parsing
|
||||
profile.profile_env_vars = profile_renderer.ctx_obj.env_vars
|
||||
|
||||
# get a new renderer using our target information and render the
|
||||
# project
|
||||
project_renderer = DbtProjectYamlRenderer(profile, cli_vars)
|
||||
project = partial.render(project_renderer)
|
||||
# Save env_vars encountered in rendering for partial parsing
|
||||
project.project_env_vars = project_renderer.ctx_obj.env_vars
|
||||
return (project, profile)
|
||||
|
||||
# Called in main.py, lib.py, task/base.py
|
||||
@classmethod
|
||||
def from_args(cls, args: Any) -> 'RuntimeConfig':
|
||||
"""Given arguments, read in dbt_project.yml from the current directory,
|
||||
read in packages.yml if it exists, and use them to find the profile to
|
||||
load.
|
||||
|
||||
:param args argparse.Namespace: The arguments as parsed from the cli.
|
||||
:param args: The arguments as parsed from the cli.
|
||||
:raises DbtProjectError: If the project is invalid or missing.
|
||||
:raises DbtProfileError: If the profile is invalid or missing.
|
||||
:raises ValidationException: If the cli variables are invalid.
|
||||
"""
|
||||
# build the project and read in packages.yml
|
||||
project = Project.from_args(args)
|
||||
|
||||
# build the profile
|
||||
profile = Profile.from_args(
|
||||
args=args,
|
||||
project_profile_name=project.profile_name
|
||||
)
|
||||
project, profile = cls.collect_parts(args)
|
||||
|
||||
return cls.from_parts(
|
||||
project=project,
|
||||
profile=profile,
|
||||
args=args,
|
||||
)
|
||||
|
||||
def get_metadata(self) -> ManifestMetadata:
|
||||
return ManifestMetadata(
|
||||
project_id=self.hashed_name(),
|
||||
adapter_type=self.credentials.type
|
||||
)
|
||||
|
||||
def _get_v2_config_paths(
|
||||
self,
|
||||
config,
|
||||
path: FQNPath,
|
||||
paths: MutableSet[FQNPath],
|
||||
) -> PathSet:
|
||||
for key, value in config.items():
|
||||
if isinstance(value, dict) and not key.startswith('+'):
|
||||
self._get_config_paths(value, path + (key,), paths)
|
||||
else:
|
||||
paths.add(path)
|
||||
return frozenset(paths)
|
||||
|
||||
def _get_config_paths(
|
||||
self,
|
||||
config: Dict[str, Any],
|
||||
path: FQNPath = (),
|
||||
paths: Optional[MutableSet[FQNPath]] = None,
|
||||
) -> PathSet:
|
||||
if paths is None:
|
||||
paths = set()
|
||||
|
||||
for key, value in config.items():
|
||||
if isinstance(value, dict) and not key.startswith('+'):
|
||||
self._get_v2_config_paths(value, path + (key,), paths)
|
||||
else:
|
||||
paths.add(path)
|
||||
return frozenset(paths)
|
||||
|
||||
def get_resource_config_paths(self) -> Dict[str, PathSet]:
|
||||
"""Return a dictionary with resource type keys whose values are
|
||||
lists of lists of strings, where each inner list of strings represents
|
||||
a configured path in the resource.
|
||||
"""
|
||||
return {
|
||||
'models': self._get_config_paths(self.models),
|
||||
'seeds': self._get_config_paths(self.seeds),
|
||||
'snapshots': self._get_config_paths(self.snapshots),
|
||||
'sources': self._get_config_paths(self.sources),
|
||||
'tests': self._get_config_paths(self.tests),
|
||||
}
|
||||
|
||||
def get_unused_resource_config_paths(
|
||||
self,
|
||||
resource_fqns: Mapping[str, PathSet],
|
||||
disabled: PathSet,
|
||||
) -> List[FQNPath]:
|
||||
"""Return a list of lists of strings, where each inner list of strings
|
||||
represents a type + FQN path of a resource configuration that is not
|
||||
used.
|
||||
"""
|
||||
disabled_fqns = frozenset(tuple(fqn) for fqn in disabled)
|
||||
resource_config_paths = self.get_resource_config_paths()
|
||||
unused_resource_config_paths = []
|
||||
for resource_type, config_paths in resource_config_paths.items():
|
||||
used_fqns = resource_fqns.get(resource_type, frozenset())
|
||||
fqns = used_fqns | disabled_fqns
|
||||
|
||||
for config_path in config_paths:
|
||||
if not _is_config_used(config_path, fqns):
|
||||
unused_resource_config_paths.append(
|
||||
(resource_type,) + config_path
|
||||
)
|
||||
return unused_resource_config_paths
|
||||
|
||||
def warn_for_unused_resource_config_paths(
|
||||
self,
|
||||
resource_fqns: Mapping[str, PathSet],
|
||||
disabled: PathSet,
|
||||
) -> None:
|
||||
unused = self.get_unused_resource_config_paths(resource_fqns, disabled)
|
||||
if len(unused) == 0:
|
||||
return
|
||||
|
||||
msg = UNUSED_RESOURCE_CONFIGURATION_PATH_MESSAGE.format(
|
||||
len(unused),
|
||||
'\n'.join('- {}'.format('.'.join(u)) for u in unused)
|
||||
)
|
||||
|
||||
warn_or_error(msg, log_fmt=warning_tag('{}'))
|
||||
|
||||
def load_dependencies(self) -> Mapping[str, 'RuntimeConfig']:
|
||||
if self.dependencies is None:
|
||||
all_projects = {self.project_name: self}
|
||||
internal_packages = get_include_paths(self.credentials.type)
|
||||
# raise exception if fewer installed packages than in packages.yml
|
||||
count_packages_specified = len(self.packages.packages) # type: ignore
|
||||
count_packages_installed = len(tuple(self._get_project_directories()))
|
||||
if count_packages_specified > count_packages_installed:
|
||||
raise_compiler_error(
|
||||
f'dbt found {count_packages_specified} package(s) '
|
||||
f'specified in packages.yml, but only '
|
||||
f'{count_packages_installed} package(s) installed '
|
||||
f'in {self.packages_install_path}. Run "dbt deps" to '
|
||||
f'install package dependencies.'
|
||||
)
|
||||
project_paths = itertools.chain(
|
||||
internal_packages,
|
||||
self._get_project_directories()
|
||||
)
|
||||
for project_name, project in self.load_projects(project_paths):
|
||||
if project_name in all_projects:
|
||||
raise_compiler_error(
|
||||
f'dbt found more than one package with the name '
|
||||
f'"{project_name}" included in this project. Package '
|
||||
f'names must be unique in a project. Please rename '
|
||||
f'one of these packages.'
|
||||
)
|
||||
all_projects[project_name] = project
|
||||
self.dependencies = all_projects
|
||||
return self.dependencies
|
||||
|
||||
def clear_dependencies(self):
|
||||
self.dependencies = None
|
||||
|
||||
# Called by 'load_dependencies' in this class
|
||||
def load_projects(
|
||||
self, paths: Iterable[Path]
|
||||
) -> Iterator[Tuple[str, 'RuntimeConfig']]:
|
||||
for path in paths:
|
||||
try:
|
||||
project = self.new_project(str(path))
|
||||
except DbtProjectError as e:
|
||||
raise DbtProjectError(
|
||||
f'Failed to read package: {e}',
|
||||
result_type='invalid_project',
|
||||
path=path,
|
||||
) from e
|
||||
else:
|
||||
yield project.project_name, project
|
||||
|
||||
def _get_project_directories(self) -> Iterator[Path]:
|
||||
root = Path(self.project_root) / self.packages_install_path
|
||||
|
||||
if root.exists():
|
||||
for path in root.iterdir():
|
||||
if path.is_dir() and not path.name.startswith('__'):
|
||||
yield path
|
||||
|
||||
|
||||
class UnsetCredentials(Credentials):
|
||||
def __init__(self):
|
||||
super().__init__('', '')
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return None
|
||||
|
||||
@property
|
||||
def unique_field(self):
|
||||
return None
|
||||
|
||||
def connection_info(self, *args, **kwargs):
|
||||
return {}
|
||||
|
||||
def _connection_keys(self):
|
||||
return ()
|
||||
|
||||
|
||||
# This is used by UnsetProfileConfig, for commands which do
|
||||
# not require a profile, i.e. dbt deps and clean
|
||||
class UnsetProfile(Profile):
|
||||
def __init__(self):
|
||||
self.credentials = UnsetCredentials()
|
||||
self.user_config = UserConfig() # This will be read in _get_rendered_profile
|
||||
self.profile_name = ''
|
||||
self.target_name = ''
|
||||
self.threads = -1
|
||||
|
||||
def to_target_dict(self):
|
||||
return {}
|
||||
|
||||
def __getattribute__(self, name):
|
||||
if name in {'profile_name', 'target_name', 'threads'}:
|
||||
raise RuntimeException(
|
||||
f'Error: disallowed attribute "{name}" - no profile!'
|
||||
)
|
||||
|
||||
return Profile.__getattribute__(self, name)
|
||||
|
||||
|
||||
# This class is used by the dbt deps and clean commands, because they don't
|
||||
# require a functioning profile.
|
||||
@dataclass
|
||||
class UnsetProfileConfig(RuntimeConfig):
|
||||
"""This class acts a lot _like_ a RuntimeConfig, except if your profile is
|
||||
missing, any access to profile members results in an exception.
|
||||
"""
|
||||
|
||||
def __post_init__(self):
|
||||
# instead of futzing with InitVar overrides or rewriting __init__, just
|
||||
# `del` the attrs we don't want users touching.
|
||||
del self.profile_name
|
||||
del self.target_name
|
||||
# don't call super().__post_init__(), as that calls validate(), and
|
||||
# this object isn't very valid
|
||||
|
||||
def __getattribute__(self, name):
|
||||
# Override __getattribute__ to check that the attribute isn't 'banned'.
|
||||
if name in {'profile_name', 'target_name'}:
|
||||
raise RuntimeException(
|
||||
f'Error: disallowed attribute "{name}" - no profile!'
|
||||
)
|
||||
|
||||
# avoid every attribute access triggering infinite recursion
|
||||
return RuntimeConfig.__getattribute__(self, name)
|
||||
|
||||
def to_target_dict(self):
|
||||
# re-override the poisoned profile behavior
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def from_parts(
|
||||
cls,
|
||||
project: Project,
|
||||
profile: Profile,
|
||||
args: Any,
|
||||
dependencies: Optional[Mapping[str, 'RuntimeConfig']] = None,
|
||||
) -> 'RuntimeConfig':
|
||||
"""Instantiate a RuntimeConfig from its components.
|
||||
|
||||
:param profile: Ignored.
|
||||
:param project: A parsed dbt Project.
|
||||
:param args: The parsed command-line arguments.
|
||||
:returns RuntimeConfig: The new configuration.
|
||||
"""
|
||||
cli_vars: Dict[str, Any] = parse_cli_vars(getattr(args, 'vars', '{}'))
|
||||
|
||||
return cls(
|
||||
project_name=project.project_name,
|
||||
version=project.version,
|
||||
project_root=project.project_root,
|
||||
model_paths=project.model_paths,
|
||||
macro_paths=project.macro_paths,
|
||||
seed_paths=project.seed_paths,
|
||||
test_paths=project.test_paths,
|
||||
analysis_paths=project.analysis_paths,
|
||||
docs_paths=project.docs_paths,
|
||||
asset_paths=project.asset_paths,
|
||||
target_path=project.target_path,
|
||||
snapshot_paths=project.snapshot_paths,
|
||||
clean_targets=project.clean_targets,
|
||||
log_path=project.log_path,
|
||||
packages_install_path=project.packages_install_path,
|
||||
quoting=project.quoting, # we never use this anyway.
|
||||
models=project.models,
|
||||
on_run_start=project.on_run_start,
|
||||
on_run_end=project.on_run_end,
|
||||
dispatch=project.dispatch,
|
||||
seeds=project.seeds,
|
||||
snapshots=project.snapshots,
|
||||
dbt_version=project.dbt_version,
|
||||
packages=project.packages,
|
||||
manifest_selectors=project.manifest_selectors,
|
||||
selectors=project.selectors,
|
||||
query_comment=project.query_comment,
|
||||
sources=project.sources,
|
||||
tests=project.tests,
|
||||
vars=project.vars,
|
||||
config_version=project.config_version,
|
||||
unrendered=project.unrendered,
|
||||
project_env_vars=project.project_env_vars,
|
||||
profile_env_vars=profile.profile_env_vars,
|
||||
profile_name='',
|
||||
target_name='',
|
||||
user_config=UserConfig(),
|
||||
threads=getattr(args, 'threads', 1),
|
||||
credentials=UnsetCredentials(),
|
||||
args=args,
|
||||
cli_vars=cli_vars,
|
||||
dependencies=dependencies,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_rendered_profile(
|
||||
cls,
|
||||
args: Any,
|
||||
profile_renderer: ProfileRenderer,
|
||||
profile_name: Optional[str],
|
||||
) -> Profile:
|
||||
|
||||
profile = UnsetProfile()
|
||||
# The profile (for warehouse connection) is not needed, but we want
|
||||
# to get the UserConfig, which is also in profiles.yml
|
||||
user_config = read_user_config(flags.PROFILES_DIR)
|
||||
profile.user_config = user_config
|
||||
return profile
|
||||
|
||||
@classmethod
|
||||
def from_args(cls: Type[RuntimeConfig], args: Any) -> 'RuntimeConfig':
|
||||
"""Given arguments, read in dbt_project.yml from the current directory,
|
||||
read in packages.yml if it exists, and use them to find the profile to
|
||||
load.
|
||||
|
||||
:param args: The arguments as parsed from the cli.
|
||||
:raises DbtProjectError: If the project is invalid or missing.
|
||||
:raises DbtProfileError: If the profile is invalid or missing.
|
||||
:raises ValidationException: If the cli variables are invalid.
|
||||
"""
|
||||
project, profile = cls.collect_parts(args)
|
||||
|
||||
return cls.from_parts(
|
||||
project=project,
|
||||
profile=profile,
|
||||
args=args
|
||||
)
|
||||
|
||||
|
||||
UNUSED_RESOURCE_CONFIGURATION_PATH_MESSAGE = """\
|
||||
Configuration paths exist in your dbt_project.yml file which do not \
|
||||
apply to any resources.
|
||||
There are {} unused configuration paths:
|
||||
{}
|
||||
"""
|
||||
|
||||
|
||||
def _is_config_used(path, fqns):
|
||||
if fqns:
|
||||
for fqn in fqns:
|
||||
if len(path) <= len(fqn) and fqn[:len(path)] == path:
|
||||
return True
|
||||
return False
|
||||
|
||||
201
core/dbt/config/selectors.py
Normal file
201
core/dbt/config/selectors.py
Normal file
@@ -0,0 +1,201 @@
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Union
|
||||
from dbt.clients.yaml_helper import ( # noqa: F401
|
||||
yaml, Loader, Dumper, load_yaml_text
|
||||
)
|
||||
from dbt.dataclass_schema import ValidationError
|
||||
|
||||
from .renderer import SelectorRenderer
|
||||
|
||||
from dbt.clients.system import (
|
||||
load_file_contents,
|
||||
path_exists,
|
||||
resolve_path_from_base,
|
||||
)
|
||||
from dbt.contracts.selection import SelectorFile
|
||||
from dbt.exceptions import DbtSelectorsError, RuntimeException
|
||||
from dbt.graph import parse_from_selectors_definition, SelectionSpec
|
||||
from dbt.graph.selector_spec import SelectionCriteria
|
||||
|
||||
MALFORMED_SELECTOR_ERROR = """\
|
||||
The selectors.yml file in this project is malformed. Please double check
|
||||
the contents of this file and fix any errors before retrying.
|
||||
|
||||
You can find more information on the syntax for this file here:
|
||||
https://docs.getdbt.com/docs/package-management
|
||||
|
||||
Validator Error:
|
||||
{error}
|
||||
"""
|
||||
|
||||
|
||||
class SelectorConfig(Dict[str, Dict[str, Union[SelectionSpec, bool]]]):
|
||||
|
||||
@classmethod
|
||||
def selectors_from_dict(cls, data: Dict[str, Any]) -> 'SelectorConfig':
|
||||
try:
|
||||
SelectorFile.validate(data)
|
||||
selector_file = SelectorFile.from_dict(data)
|
||||
validate_selector_default(selector_file)
|
||||
selectors = parse_from_selectors_definition(selector_file)
|
||||
except ValidationError as exc:
|
||||
yaml_sel_cfg = yaml.dump(exc.instance)
|
||||
raise DbtSelectorsError(
|
||||
f"Could not parse selector file data: \n{yaml_sel_cfg}\n"
|
||||
f"Valid root-level selector definitions: "
|
||||
f"union, intersection, string, dictionary. No lists. "
|
||||
f"\nhttps://docs.getdbt.com/reference/node-selection/"
|
||||
f"yaml-selectors",
|
||||
result_type='invalid_selector'
|
||||
) from exc
|
||||
except RuntimeException as exc:
|
||||
raise DbtSelectorsError(
|
||||
f'Could not read selector file data: {exc}',
|
||||
result_type='invalid_selector',
|
||||
) from exc
|
||||
|
||||
return cls(selectors)
|
||||
|
||||
@classmethod
|
||||
def render_from_dict(
|
||||
cls,
|
||||
data: Dict[str, Any],
|
||||
renderer: SelectorRenderer,
|
||||
) -> 'SelectorConfig':
|
||||
try:
|
||||
rendered = renderer.render_data(data)
|
||||
except (ValidationError, RuntimeException) as exc:
|
||||
raise DbtSelectorsError(
|
||||
f'Could not render selector data: {exc}',
|
||||
result_type='invalid_selector',
|
||||
) from exc
|
||||
return cls.selectors_from_dict(rendered)
|
||||
|
||||
@classmethod
|
||||
def from_path(
|
||||
cls, path: Path, renderer: SelectorRenderer,
|
||||
) -> 'SelectorConfig':
|
||||
try:
|
||||
data = load_yaml_text(load_file_contents(str(path)))
|
||||
except (ValidationError, RuntimeException) as exc:
|
||||
raise DbtSelectorsError(
|
||||
f'Could not read selector file: {exc}',
|
||||
result_type='invalid_selector',
|
||||
path=path,
|
||||
) from exc
|
||||
|
||||
try:
|
||||
return cls.render_from_dict(data, renderer)
|
||||
except DbtSelectorsError as exc:
|
||||
exc.path = path
|
||||
raise
|
||||
|
||||
|
||||
def selector_data_from_root(project_root: str) -> Dict[str, Any]:
|
||||
selector_filepath = resolve_path_from_base(
|
||||
'selectors.yml', project_root
|
||||
)
|
||||
|
||||
if path_exists(selector_filepath):
|
||||
selectors_dict = load_yaml_text(load_file_contents(selector_filepath))
|
||||
else:
|
||||
selectors_dict = None
|
||||
return selectors_dict
|
||||
|
||||
|
||||
def selector_config_from_data(
|
||||
selectors_data: Dict[str, Any]
|
||||
) -> SelectorConfig:
|
||||
if not selectors_data:
|
||||
selectors_data = {'selectors': []}
|
||||
|
||||
try:
|
||||
selectors = SelectorConfig.selectors_from_dict(selectors_data)
|
||||
except ValidationError as e:
|
||||
raise DbtSelectorsError(
|
||||
MALFORMED_SELECTOR_ERROR.format(error=str(e.message)),
|
||||
result_type='invalid_selector',
|
||||
) from e
|
||||
return selectors
|
||||
|
||||
|
||||
def validate_selector_default(selector_file: SelectorFile) -> None:
|
||||
"""Check if a selector.yml file has more than 1 default key set to true"""
|
||||
default_set: bool = False
|
||||
default_selector_name: Union[str, None] = None
|
||||
|
||||
for selector in selector_file.selectors:
|
||||
if selector.default is True and default_set is False:
|
||||
default_set = True
|
||||
default_selector_name = selector.name
|
||||
continue
|
||||
if selector.default is True and default_set is True:
|
||||
raise DbtSelectorsError(
|
||||
"Error when parsing the selector file. "
|
||||
"Found multiple selectors with `default: true`:"
|
||||
f"{default_selector_name} and {selector.name}"
|
||||
)
|
||||
|
||||
|
||||
# These are utilities to clean up the dictionary created from
|
||||
# selectors.yml by turning the cli-string format entries into
|
||||
# normalized dictionary entries. It parallels the flow in
|
||||
# dbt/graph/cli.py. If changes are made there, it might
|
||||
# be necessary to make changes here. Ideally it would be
|
||||
# good to combine the two flows into one at some point.
|
||||
class SelectorDict:
|
||||
|
||||
@classmethod
|
||||
def parse_dict_definition(cls, definition):
|
||||
key = list(definition)[0]
|
||||
value = definition[key]
|
||||
if isinstance(value, list):
|
||||
new_values = []
|
||||
for sel_def in value:
|
||||
new_value = cls.parse_from_definition(sel_def)
|
||||
new_values.append(new_value)
|
||||
value = new_values
|
||||
if key == 'exclude':
|
||||
definition = {key: value}
|
||||
elif len(definition) == 1:
|
||||
definition = {'method': key, 'value': value}
|
||||
return definition
|
||||
|
||||
@classmethod
|
||||
def parse_a_definition(cls, def_type, definition):
|
||||
# this definition must be a list
|
||||
new_dict = {def_type: []}
|
||||
for sel_def in definition[def_type]:
|
||||
if isinstance(sel_def, dict):
|
||||
sel_def = cls.parse_from_definition(sel_def)
|
||||
new_dict[def_type].append(sel_def)
|
||||
elif isinstance(sel_def, str):
|
||||
sel_def = SelectionCriteria.dict_from_single_spec(sel_def)
|
||||
new_dict[def_type].append(sel_def)
|
||||
else:
|
||||
new_dict[def_type].append(sel_def)
|
||||
return new_dict
|
||||
|
||||
@classmethod
|
||||
def parse_from_definition(cls, definition):
|
||||
if isinstance(definition, str):
|
||||
definition = SelectionCriteria.dict_from_single_spec(definition)
|
||||
elif 'union' in definition:
|
||||
definition = cls.parse_a_definition('union', definition)
|
||||
elif 'intersection' in definition:
|
||||
definition = cls.parse_a_definition('intersection', definition)
|
||||
elif isinstance(definition, dict):
|
||||
definition = cls.parse_dict_definition(definition)
|
||||
return definition
|
||||
|
||||
# This is the normal entrypoint of this code. Give it the
|
||||
# list of selectors generated from the selectors.yml file.
|
||||
@classmethod
|
||||
def parse_from_selectors_list(cls, selectors):
|
||||
selector_dict = {}
|
||||
for selector in selectors:
|
||||
sel_name = selector['name']
|
||||
selector_dict[sel_name] = selector
|
||||
definition = cls.parse_from_definition(selector['definition'])
|
||||
selector_dict[sel_name]['definition'] = definition
|
||||
return selector_dict
|
||||
22
core/dbt/config/utils.py
Normal file
22
core/dbt/config/utils.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
from dbt.clients import yaml_helper
|
||||
from dbt.events.functions import fire_event
|
||||
from dbt.exceptions import raise_compiler_error, ValidationException
|
||||
from dbt.events.types import InvalidVarsYAML
|
||||
|
||||
|
||||
def parse_cli_vars(var_string: str) -> Dict[str, Any]:
|
||||
try:
|
||||
cli_vars = yaml_helper.load_yaml_text(var_string)
|
||||
var_type = type(cli_vars)
|
||||
if var_type is dict:
|
||||
return cli_vars
|
||||
else:
|
||||
type_name = var_type.__name__
|
||||
raise_compiler_error(
|
||||
"The --vars argument must be a YAML dictionary, but was "
|
||||
"of type '{}'".format(type_name))
|
||||
except ValidationException:
|
||||
fire_event(InvalidVarsYAML())
|
||||
raise
|
||||
1
core/dbt/context/README.md
Normal file
1
core/dbt/context/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Contexts and Jinja rendering
|
||||
@@ -1,95 +1,62 @@
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
from typing import Callable, Any, Dict, List, Optional
|
||||
from typing import (
|
||||
Any, Dict, NoReturn, Optional, Mapping
|
||||
)
|
||||
|
||||
import dbt.tracking
|
||||
from dbt.clients.jinja import undefined_error
|
||||
from dbt.contracts.graph.parsed import ParsedMacro
|
||||
from dbt.exceptions import MacroReturn, raise_compiler_error
|
||||
from dbt.include.global_project import PACKAGES
|
||||
from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME
|
||||
from dbt.logger import GLOBAL_LOGGER as logger
|
||||
from dbt import flags
|
||||
from dbt import tracking
|
||||
from dbt.clients.jinja import get_rendered
|
||||
from dbt.clients.yaml_helper import ( # noqa: F401
|
||||
yaml, safe_load, SafeLoader, Loader, Dumper
|
||||
)
|
||||
from dbt.contracts.graph.compiled import CompiledResource
|
||||
from dbt.exceptions import (
|
||||
raise_compiler_error, MacroReturn, raise_parsing_error, disallow_secret_env_var
|
||||
)
|
||||
from dbt.logger import SECRET_ENV_PREFIX
|
||||
from dbt.events.functions import fire_event, get_invocation_id
|
||||
from dbt.events.types import MacroEventInfo, MacroEventDebug
|
||||
from dbt.version import __version__ as dbt_version
|
||||
|
||||
from dbt.node_types import NodeType
|
||||
|
||||
|
||||
# These modules are added to the context. Consider alternative
|
||||
# approaches which will extend well to potentially many modules
|
||||
import pytz
|
||||
import datetime
|
||||
import re
|
||||
|
||||
|
||||
def env_var(var, default=None):
|
||||
if var in os.environ:
|
||||
return os.environ[var]
|
||||
elif default is not None:
|
||||
return default
|
||||
else:
|
||||
msg = "Env var required but not provided: '{}'".format(var)
|
||||
undefined_error(msg)
|
||||
|
||||
|
||||
def debug_here():
|
||||
import sys
|
||||
import ipdb # type: ignore
|
||||
frame = sys._getframe(3)
|
||||
ipdb.set_trace(frame)
|
||||
|
||||
|
||||
class Var:
|
||||
UndefinedVarError = "Required var '{}' not found in config:\nVars "\
|
||||
"supplied to {} = {}"
|
||||
_VAR_NOTSET = object()
|
||||
|
||||
def __init__(self, model, context, overrides):
|
||||
self.model = model
|
||||
self.context = context
|
||||
|
||||
# These are hard-overrides (eg. CLI vars) that should take
|
||||
# precedence over context-based var definitions
|
||||
self.overrides = overrides
|
||||
|
||||
if model is None:
|
||||
# during config parsing we have no model and no local vars
|
||||
self.model_name = '<Configuration>'
|
||||
local_vars = {}
|
||||
else:
|
||||
self.model_name = model.name
|
||||
local_vars = model.local_vars()
|
||||
|
||||
self.local_vars = dbt.utils.merge(local_vars, overrides)
|
||||
|
||||
def pretty_dict(self, data):
|
||||
return json.dumps(data, sort_keys=True, indent=4)
|
||||
|
||||
def get_missing_var(self, var_name):
|
||||
pretty_vars = self.pretty_dict(self.local_vars)
|
||||
msg = self.UndefinedVarError.format(
|
||||
var_name, self.model_name, pretty_vars
|
||||
)
|
||||
raise_compiler_error(msg, self.model)
|
||||
|
||||
def assert_var_defined(self, var_name, default):
|
||||
if var_name not in self.local_vars and default is self._VAR_NOTSET:
|
||||
return self.get_missing_var(var_name)
|
||||
|
||||
def get_rendered_var(self, var_name):
|
||||
raw = self.local_vars[var_name]
|
||||
# if bool/int/float/etc are passed in, don't compile anything
|
||||
if not isinstance(raw, str):
|
||||
return raw
|
||||
|
||||
return dbt.clients.jinja.get_rendered(raw, self.context)
|
||||
|
||||
def __call__(self, var_name, default=_VAR_NOTSET):
|
||||
if var_name in self.local_vars:
|
||||
return self.get_rendered_var(var_name)
|
||||
elif default is not self._VAR_NOTSET:
|
||||
return default
|
||||
else:
|
||||
return self.get_missing_var(var_name)
|
||||
# Contexts in dbt Core
|
||||
# Contexts are used for Jinja rendering. They include context methods,
|
||||
# executable macros, and various settings that are available in Jinja.
|
||||
#
|
||||
# Different contexts are used in different places because we allow access
|
||||
# to different methods and data in different places. Executable SQL, for
|
||||
# example, includes the available macros and the model, while Jinja in
|
||||
# yaml files is more limited.
|
||||
#
|
||||
# The context that is passed to Jinja is always in a dictionary format,
|
||||
# not an actual class, so a 'to_dict()' is executed on a context class
|
||||
# before it is used for rendering.
|
||||
#
|
||||
# Each context has a generate_<name>_context function to create the context.
|
||||
# ProviderContext subclasses have different generate functions for
|
||||
# parsing and for execution.
|
||||
#
|
||||
# Context class hierarchy
|
||||
#
|
||||
# BaseContext -- core/dbt/context/base.py
|
||||
# SecretContext -- core/dbt/context/secret.py
|
||||
# TargetContext -- core/dbt/context/target.py
|
||||
# ConfiguredContext -- core/dbt/context/configured.py
|
||||
# SchemaYamlContext -- core/dbt/context/configured.py
|
||||
# DocsRuntimeContext -- core/dbt/context/configured.py
|
||||
# MacroResolvingContext -- core/dbt/context/configured.py
|
||||
# ManifestContext -- core/dbt/context/manifest.py
|
||||
# QueryHeaderContext -- core/dbt/context/manifest.py
|
||||
# ProviderContext -- core/dbt/context/provider.py
|
||||
# MacroContext -- core/dbt/context/provider.py
|
||||
# ModelContext -- core/dbt/context/provider.py
|
||||
# TestContext -- core/dbt/context/provider.py
|
||||
|
||||
|
||||
def get_pytz_module_context() -> Dict[str, Any]:
|
||||
@@ -114,158 +81,498 @@ def get_datetime_module_context() -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def get_re_module_context() -> Dict[str, Any]:
|
||||
context_exports = re.__all__
|
||||
|
||||
return {
|
||||
name: getattr(re, name) for name in context_exports
|
||||
}
|
||||
|
||||
|
||||
def get_context_modules() -> Dict[str, Dict[str, Any]]:
|
||||
return {
|
||||
'pytz': get_pytz_module_context(),
|
||||
'datetime': get_datetime_module_context(),
|
||||
're': get_re_module_context(),
|
||||
}
|
||||
|
||||
|
||||
def _return(value):
|
||||
raise MacroReturn(value)
|
||||
class ContextMember:
|
||||
def __init__(self, value, name=None):
|
||||
self.name = name
|
||||
self.inner = value
|
||||
|
||||
def key(self, default):
|
||||
if self.name is None:
|
||||
return default
|
||||
return self.name
|
||||
|
||||
|
||||
def fromjson(string, default=None):
|
||||
try:
|
||||
return json.loads(string)
|
||||
except ValueError:
|
||||
return default
|
||||
def contextmember(value):
|
||||
if isinstance(value, str):
|
||||
return lambda v: ContextMember(v, name=value)
|
||||
return ContextMember(value)
|
||||
|
||||
|
||||
def tojson(value, default=None, sort_keys=False):
|
||||
try:
|
||||
return json.dumps(value, sort_keys=sort_keys)
|
||||
except ValueError:
|
||||
return default
|
||||
def contextproperty(value):
|
||||
if isinstance(value, str):
|
||||
return lambda v: ContextMember(property(v), name=value)
|
||||
return ContextMember(property(value))
|
||||
|
||||
|
||||
def log(msg, info=False):
|
||||
if info:
|
||||
logger.info(msg)
|
||||
else:
|
||||
logger.debug(msg)
|
||||
return ''
|
||||
class ContextMeta(type):
|
||||
def __new__(mcls, name, bases, dct):
|
||||
context_members = {}
|
||||
context_attrs = {}
|
||||
new_dct = {}
|
||||
|
||||
for base in bases:
|
||||
context_members.update(getattr(base, '_context_members_', {}))
|
||||
context_attrs.update(getattr(base, '_context_attrs_', {}))
|
||||
|
||||
for key, value in dct.items():
|
||||
if isinstance(value, ContextMember):
|
||||
context_key = value.key(key)
|
||||
context_members[context_key] = value.inner
|
||||
context_attrs[context_key] = key
|
||||
value = value.inner
|
||||
new_dct[key] = value
|
||||
new_dct['_context_members_'] = context_members
|
||||
new_dct['_context_attrs_'] = context_attrs
|
||||
return type.__new__(mcls, name, bases, new_dct)
|
||||
|
||||
|
||||
class BaseContext:
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
run_started_at = None
|
||||
invocation_id = None
|
||||
class Var:
|
||||
UndefinedVarError = "Required var '{}' not found in config:\nVars "\
|
||||
"supplied to {} = {}"
|
||||
_VAR_NOTSET = object()
|
||||
|
||||
if dbt.tracking.active_user is not None:
|
||||
run_started_at = dbt.tracking.active_user.run_started_at
|
||||
invocation_id = dbt.tracking.active_user.invocation_id
|
||||
def __init__(
|
||||
self,
|
||||
context: Mapping[str, Any],
|
||||
cli_vars: Mapping[str, Any],
|
||||
node: Optional[CompiledResource] = None
|
||||
) -> None:
|
||||
self._context: Mapping[str, Any] = context
|
||||
self._cli_vars: Mapping[str, Any] = cli_vars
|
||||
self._node: Optional[CompiledResource] = node
|
||||
self._merged: Mapping[str, Any] = self._generate_merged()
|
||||
|
||||
context: Dict[str, Any] = {
|
||||
'env_var': env_var,
|
||||
'modules': get_context_modules(),
|
||||
'run_started_at': run_started_at,
|
||||
'invocation_id': invocation_id,
|
||||
'return': _return,
|
||||
'fromjson': fromjson,
|
||||
'tojson': tojson,
|
||||
'log': log,
|
||||
}
|
||||
if os.environ.get('DBT_MACRO_DEBUGGING'):
|
||||
context['debug'] = debug_here
|
||||
return context
|
||||
|
||||
|
||||
class ConfigRenderContext(BaseContext):
|
||||
def __init__(self, cli_vars):
|
||||
self.cli_vars = cli_vars
|
||||
|
||||
def make_var(self, context) -> Var:
|
||||
return Var(None, context, self.cli_vars)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
context = super().to_dict()
|
||||
context['var'] = self.make_var(context)
|
||||
return context
|
||||
|
||||
|
||||
def _add_macro_map(
|
||||
context: Dict[str, Any], package_name: str, macro_map: Dict[str, Callable]
|
||||
):
|
||||
"""Update an existing context in-place, adding the given macro map to the
|
||||
appropriate package namespace. Adapter packages get inserted into the
|
||||
global namespace.
|
||||
"""
|
||||
key = package_name
|
||||
if package_name in PACKAGES:
|
||||
key = GLOBAL_PROJECT_NAME
|
||||
if key not in context:
|
||||
value: Dict[str, Callable] = {}
|
||||
context[key] = value
|
||||
|
||||
context[key].update(macro_map)
|
||||
|
||||
|
||||
class HasCredentialsContext(ConfigRenderContext):
|
||||
def __init__(self, config):
|
||||
# sometimes we only have a profile object and end up here. In those
|
||||
# cases, we never want the actual cli vars passed, so we can do this.
|
||||
cli_vars = getattr(config, 'cli_vars', {})
|
||||
super().__init__(cli_vars=cli_vars)
|
||||
self.config = config
|
||||
|
||||
def get_target(self) -> Dict[str, Any]:
|
||||
target = dict(
|
||||
self.config.credentials.connection_info(with_aliases=True)
|
||||
)
|
||||
target.update({
|
||||
'type': self.config.credentials.type,
|
||||
'threads': self.config.threads,
|
||||
'name': self.config.target_name,
|
||||
# not specified, but present for compatibility
|
||||
'target_name': self.config.target_name,
|
||||
'profile_name': self.config.profile_name,
|
||||
'config': self.config.config.to_dict(),
|
||||
})
|
||||
return target
|
||||
def _generate_merged(self) -> Mapping[str, Any]:
|
||||
return self._cli_vars
|
||||
|
||||
@property
|
||||
def search_package_name(self):
|
||||
return self.config.project_name
|
||||
def node_name(self):
|
||||
if self._node is not None:
|
||||
return self._node.name
|
||||
else:
|
||||
return '<Configuration>'
|
||||
|
||||
def add_macros_from(
|
||||
self,
|
||||
context: Dict[str, Any],
|
||||
macros: Dict[str, ParsedMacro],
|
||||
):
|
||||
global_macros: List[Dict[str, Callable]] = []
|
||||
local_macros: List[Dict[str, Callable]] = []
|
||||
def get_missing_var(self, var_name):
|
||||
dct = {k: self._merged[k] for k in self._merged}
|
||||
pretty_vars = json.dumps(dct, sort_keys=True, indent=4)
|
||||
msg = self.UndefinedVarError.format(
|
||||
var_name, self.node_name, pretty_vars
|
||||
)
|
||||
raise_compiler_error(msg, self._node)
|
||||
|
||||
for unique_id, macro in macros.items():
|
||||
if macro.resource_type != NodeType.Macro:
|
||||
continue
|
||||
package_name = macro.package_name
|
||||
def has_var(self, var_name: str):
|
||||
return var_name in self._merged
|
||||
|
||||
macro_map: Dict[str, Callable] = {
|
||||
macro.name: macro.generator(context)
|
||||
def get_rendered_var(self, var_name):
|
||||
raw = self._merged[var_name]
|
||||
# if bool/int/float/etc are passed in, don't compile anything
|
||||
if not isinstance(raw, str):
|
||||
return raw
|
||||
|
||||
return get_rendered(raw, self._context)
|
||||
|
||||
def __call__(self, var_name, default=_VAR_NOTSET):
|
||||
if self.has_var(var_name):
|
||||
return self.get_rendered_var(var_name)
|
||||
elif default is not self._VAR_NOTSET:
|
||||
return default
|
||||
else:
|
||||
return self.get_missing_var(var_name)
|
||||
|
||||
|
||||
class BaseContext(metaclass=ContextMeta):
|
||||
# subclass is TargetContext
|
||||
def __init__(self, cli_vars):
|
||||
self._ctx = {}
|
||||
self.cli_vars = cli_vars
|
||||
self.env_vars = {}
|
||||
|
||||
def generate_builtins(self):
|
||||
builtins: Dict[str, Any] = {}
|
||||
for key, value in self._context_members_.items():
|
||||
if hasattr(value, '__get__'):
|
||||
# handle properties, bound methods, etc
|
||||
value = value.__get__(self)
|
||||
builtins[key] = value
|
||||
return builtins
|
||||
|
||||
# no dbtClassMixin so this is not an actual override
|
||||
def to_dict(self):
|
||||
self._ctx['context'] = self._ctx
|
||||
builtins = self.generate_builtins()
|
||||
self._ctx['builtins'] = builtins
|
||||
self._ctx.update(builtins)
|
||||
return self._ctx
|
||||
|
||||
@contextproperty
|
||||
def dbt_version(self) -> str:
|
||||
"""The `dbt_version` variable returns the installed version of dbt that
|
||||
is currently running. It can be used for debugging or auditing
|
||||
purposes.
|
||||
|
||||
> macros/get_version.sql
|
||||
|
||||
{% macro get_version() %}
|
||||
{% set msg = "The installed version of dbt is: " ~ dbt_version %}
|
||||
{% do log(msg, info=true) %}
|
||||
{% endmacro %}
|
||||
|
||||
Example output:
|
||||
|
||||
$ dbt run-operation get_version
|
||||
The installed version of dbt is 0.16.0
|
||||
"""
|
||||
return dbt_version
|
||||
|
||||
@contextproperty
|
||||
def var(self) -> Var:
|
||||
"""Variables can be passed from your `dbt_project.yml` file into models
|
||||
during compilation. These variables are useful for configuring packages
|
||||
for deployment in multiple environments, or defining values that should
|
||||
be used across multiple models within a package.
|
||||
|
||||
To add a variable to a model, use the `var()` function:
|
||||
|
||||
> my_model.sql:
|
||||
|
||||
select * from events where event_type = '{{ var("event_type") }}'
|
||||
|
||||
If you try to run this model without supplying an `event_type`
|
||||
variable, you'll receive a compilation error that looks like this:
|
||||
|
||||
Encountered an error:
|
||||
! Compilation error while compiling model package_name.my_model:
|
||||
! Required var 'event_type' not found in config:
|
||||
Vars supplied to package_name.my_model = {
|
||||
}
|
||||
|
||||
# adapter packages are part of the global project space
|
||||
_add_macro_map(context, package_name, macro_map)
|
||||
To supply a variable to a given model, add one or more `vars`
|
||||
dictionaries to the `models` config in your `dbt_project.yml` file.
|
||||
These `vars` are in-scope for all models at or below where they are
|
||||
defined, so place them where they make the most sense. Below are three
|
||||
different placements of the `vars` dict, all of which will make the
|
||||
`my_model` model compile.
|
||||
|
||||
if package_name == self.search_package_name:
|
||||
local_macros.append(macro_map)
|
||||
elif package_name in PACKAGES:
|
||||
global_macros.append(macro_map)
|
||||
> dbt_project.yml:
|
||||
|
||||
# Load global macros before local macros -- local takes precedence
|
||||
for macro_map in itertools.chain(global_macros, local_macros):
|
||||
context.update(macro_map)
|
||||
# 1) scoped at the model level
|
||||
models:
|
||||
package_name:
|
||||
my_model:
|
||||
materialized: view
|
||||
vars:
|
||||
event_type: activation
|
||||
# 2) scoped at the package level
|
||||
models:
|
||||
package_name:
|
||||
vars:
|
||||
event_type: activation
|
||||
my_model:
|
||||
materialized: view
|
||||
# 3) scoped globally
|
||||
models:
|
||||
vars:
|
||||
event_type: activation
|
||||
package_name:
|
||||
my_model:
|
||||
materialized: view
|
||||
|
||||
## Variable default values
|
||||
|
||||
The `var()` function takes an optional second argument, `default`. If
|
||||
this argument is provided, then it will be the default value for the
|
||||
variable if one is not explicitly defined.
|
||||
|
||||
> my_model.sql:
|
||||
|
||||
-- Use 'activation' as the event_type if the variable is not
|
||||
-- defined.
|
||||
select *
|
||||
from events
|
||||
where event_type = '{{ var("event_type", "activation") }}'
|
||||
"""
|
||||
return Var(self._ctx, self.cli_vars)
|
||||
|
||||
@contextmember
|
||||
def env_var(self, var: str, default: Optional[str] = None) -> str:
|
||||
"""The env_var() function. Return the environment variable named 'var'.
|
||||
If there is no such environment variable set, return the default.
|
||||
|
||||
If the default is None, raise an exception for an undefined variable.
|
||||
"""
|
||||
return_value = None
|
||||
if var.startswith(SECRET_ENV_PREFIX):
|
||||
disallow_secret_env_var(var)
|
||||
if var in os.environ:
|
||||
return_value = os.environ[var]
|
||||
elif default is not None:
|
||||
return_value = default
|
||||
|
||||
if return_value is not None:
|
||||
self.env_vars[var] = return_value
|
||||
return return_value
|
||||
else:
|
||||
msg = f"Env var required but not provided: '{var}'"
|
||||
raise_parsing_error(msg)
|
||||
|
||||
if os.environ.get('DBT_MACRO_DEBUGGING'):
|
||||
@contextmember
|
||||
@staticmethod
|
||||
def debug():
|
||||
"""Enter a debugger at this line in the compiled jinja code."""
|
||||
import sys
|
||||
import ipdb # type: ignore
|
||||
frame = sys._getframe(3)
|
||||
ipdb.set_trace(frame)
|
||||
return ''
|
||||
|
||||
@contextmember('return')
|
||||
@staticmethod
|
||||
def _return(data: Any) -> NoReturn:
|
||||
"""The `return` function can be used in macros to return data to the
|
||||
caller. The type of the data (`dict`, `list`, `int`, etc) will be
|
||||
preserved through the return call.
|
||||
|
||||
:param data: The data to return to the caller
|
||||
|
||||
|
||||
class QueryHeaderContext(HasCredentialsContext):
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
> macros/example.sql:
|
||||
|
||||
def to_dict(self, macros: Optional[Dict[str, ParsedMacro]] = None):
|
||||
context = super().to_dict()
|
||||
context['target'] = self.get_target()
|
||||
context['dbt_version'] = dbt_version
|
||||
if macros is not None:
|
||||
self.add_macros_from(context, macros)
|
||||
return context
|
||||
{% macro get_data() %}
|
||||
{{ return([1,2,3]) }}
|
||||
{% endmacro %}
|
||||
|
||||
> models/my_model.sql:
|
||||
|
||||
select
|
||||
-- getdata() returns a list!
|
||||
{% for i in getdata() %}
|
||||
{{ i }}
|
||||
{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
"""
|
||||
raise MacroReturn(data)
|
||||
|
||||
@contextmember
|
||||
@staticmethod
|
||||
def fromjson(string: str, default: Any = None) -> Any:
|
||||
"""The `fromjson` context method can be used to deserialize a json
|
||||
string into a Python object primitive, eg. a `dict` or `list`.
|
||||
|
||||
:param value: The json string to deserialize
|
||||
:param default: A default value to return if the `string` argument
|
||||
cannot be deserialized (optional)
|
||||
|
||||
Usage:
|
||||
|
||||
{% set my_json_str = '{"abc": 123}' %}
|
||||
{% set my_dict = fromjson(my_json_str) %}
|
||||
{% do log(my_dict['abc']) %}
|
||||
"""
|
||||
try:
|
||||
return json.loads(string)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
@contextmember
|
||||
@staticmethod
|
||||
def tojson(
|
||||
value: Any, default: Any = None, sort_keys: bool = False
|
||||
) -> Any:
|
||||
"""The `tojson` context method can be used to serialize a Python
|
||||
object primitive, eg. a `dict` or `list` to a json string.
|
||||
|
||||
:param value: The value serialize to json
|
||||
:param default: A default value to return if the `value` argument
|
||||
cannot be serialized
|
||||
:param sort_keys: If True, sort the keys.
|
||||
|
||||
|
||||
Usage:
|
||||
|
||||
{% set my_dict = {"abc": 123} %}
|
||||
{% set my_json_string = tojson(my_dict) %}
|
||||
{% do log(my_json_string) %}
|
||||
"""
|
||||
try:
|
||||
return json.dumps(value, sort_keys=sort_keys)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
@contextmember
|
||||
@staticmethod
|
||||
def fromyaml(value: str, default: Any = None) -> Any:
|
||||
"""The fromyaml context method can be used to deserialize a yaml string
|
||||
into a Python object primitive, eg. a `dict` or `list`.
|
||||
|
||||
:param value: The yaml string to deserialize
|
||||
:param default: A default value to return if the `string` argument
|
||||
cannot be deserialized (optional)
|
||||
|
||||
Usage:
|
||||
|
||||
{% set my_yml_str -%}
|
||||
dogs:
|
||||
- good
|
||||
- bad
|
||||
{%- endset %}
|
||||
{% set my_dict = fromyaml(my_yml_str) %}
|
||||
{% do log(my_dict['dogs'], info=true) %}
|
||||
-- ["good", "bad"]
|
||||
{% do my_dict['dogs'].pop() }
|
||||
{% do log(my_dict['dogs'], info=true) %}
|
||||
-- ["good"]
|
||||
"""
|
||||
try:
|
||||
return safe_load(value)
|
||||
except (AttributeError, ValueError, yaml.YAMLError):
|
||||
return default
|
||||
|
||||
# safe_dump defaults to sort_keys=True, but we act like json.dumps (the
|
||||
# opposite)
|
||||
@contextmember
|
||||
@staticmethod
|
||||
def toyaml(
|
||||
value: Any, default: Optional[str] = None, sort_keys: bool = False
|
||||
) -> Optional[str]:
|
||||
"""The `tojson` context method can be used to serialize a Python
|
||||
object primitive, eg. a `dict` or `list` to a yaml string.
|
||||
|
||||
:param value: The value serialize to yaml
|
||||
:param default: A default value to return if the `value` argument
|
||||
cannot be serialized
|
||||
:param sort_keys: If True, sort the keys.
|
||||
|
||||
|
||||
Usage:
|
||||
|
||||
{% set my_dict = {"abc": 123} %}
|
||||
{% set my_yaml_string = toyaml(my_dict) %}
|
||||
{% do log(my_yaml_string) %}
|
||||
"""
|
||||
try:
|
||||
return yaml.safe_dump(data=value, sort_keys=sort_keys)
|
||||
except (ValueError, yaml.YAMLError):
|
||||
return default
|
||||
|
||||
@contextmember
|
||||
@staticmethod
|
||||
def log(msg: str, info: bool = False) -> str:
|
||||
"""Logs a line to either the log file or stdout.
|
||||
|
||||
:param msg: The message to log
|
||||
:param info: If `False`, write to the log file. If `True`, write to
|
||||
both the log file and stdout.
|
||||
|
||||
> macros/my_log_macro.sql
|
||||
|
||||
{% macro some_macro(arg1, arg2) %}
|
||||
{{ log("Running some_macro: " ~ arg1 ~ ", " ~ arg2) }}
|
||||
{% endmacro %}"
|
||||
"""
|
||||
if info:
|
||||
fire_event(MacroEventInfo(msg=msg))
|
||||
else:
|
||||
fire_event(MacroEventDebug(msg=msg))
|
||||
return ''
|
||||
|
||||
@contextproperty
|
||||
def run_started_at(self) -> Optional[datetime.datetime]:
|
||||
"""`run_started_at` outputs the timestamp that this run started, e.g.
|
||||
`2017-04-21 01:23:45.678`. The `run_started_at` variable is a Python
|
||||
`datetime` object. As of 0.9.1, the timezone of this variable defaults
|
||||
to UTC.
|
||||
|
||||
> run_started_at_example.sql
|
||||
|
||||
select
|
||||
'{{ run_started_at.strftime("%Y-%m-%d") }}' as date_day
|
||||
from ...
|
||||
|
||||
|
||||
To modify the timezone of this variable, use the the `pytz` module:
|
||||
|
||||
> run_started_at_utc.sql
|
||||
|
||||
{% set est = modules.pytz.timezone("America/New_York") %}
|
||||
select
|
||||
'{{ run_started_at.astimezone(est) }}' as run_started_est
|
||||
from ...
|
||||
"""
|
||||
if tracking.active_user is not None:
|
||||
return tracking.active_user.run_started_at
|
||||
else:
|
||||
return None
|
||||
|
||||
@contextproperty
|
||||
def invocation_id(self) -> Optional[str]:
|
||||
"""invocation_id outputs a UUID generated for this dbt run (useful for
|
||||
auditing)
|
||||
"""
|
||||
return get_invocation_id()
|
||||
|
||||
@contextproperty
|
||||
def modules(self) -> Dict[str, Any]:
|
||||
"""The `modules` variable in the Jinja context contains useful Python
|
||||
modules for operating on data.
|
||||
|
||||
# datetime
|
||||
|
||||
This variable is a pointer to the Python datetime module.
|
||||
|
||||
Usage:
|
||||
|
||||
{% set dt = modules.datetime.datetime.now() %}
|
||||
|
||||
# pytz
|
||||
|
||||
This variable is a pointer to the Python pytz module.
|
||||
|
||||
Usage:
|
||||
|
||||
{% set dt = modules.datetime.datetime(2002, 10, 27, 6, 0, 0) %}
|
||||
{% set dt_local = modules.pytz.timezone('US/Eastern').localize(dt) %}
|
||||
{{ dt_local }}
|
||||
""" # noqa
|
||||
return get_context_modules()
|
||||
|
||||
@contextproperty
|
||||
def flags(self) -> Any:
|
||||
"""The `flags` variable contains true/false values for flags provided
|
||||
on the command line.
|
||||
|
||||
> flags.sql:
|
||||
|
||||
{% if flags.FULL_REFRESH %}
|
||||
drop table ...
|
||||
{% else %}
|
||||
-- no-op
|
||||
{% endif %}
|
||||
|
||||
This supports all flags defined in flags submodule (core/dbt/flags.py)
|
||||
TODO: Replace with object that provides read-only access to flag values
|
||||
"""
|
||||
return flags
|
||||
|
||||
|
||||
def generate_base_context(cli_vars: Dict[str, Any]) -> Dict[str, Any]:
|
||||
ctx = BaseContext(cli_vars)
|
||||
# This is not a Mashumaro to_dict call
|
||||
return ctx.to_dict()
|
||||
|
||||
@@ -1,350 +0,0 @@
|
||||
import agate
|
||||
import os
|
||||
from typing_extensions import Protocol
|
||||
from typing import Union, Callable, Any, Dict, TypeVar, Type
|
||||
|
||||
import dbt.clients.agate_helper
|
||||
from dbt.contracts.graph.compiled import CompiledSeedNode
|
||||
from dbt.contracts.graph.parsed import ParsedSeedNode
|
||||
import dbt.exceptions
|
||||
import dbt.flags
|
||||
import dbt.tracking
|
||||
import dbt.utils
|
||||
import dbt.writer
|
||||
from dbt.adapters.factory import get_adapter
|
||||
from dbt.node_types import NodeType
|
||||
from dbt.clients.jinja import get_rendered
|
||||
from dbt.context.base import Var, HasCredentialsContext
|
||||
from dbt.contracts.graph.manifest import Manifest
|
||||
|
||||
|
||||
class RelationProxy:
|
||||
def __init__(self, adapter):
|
||||
self.quoting_config = adapter.config.quoting
|
||||
self.relation_type = adapter.Relation
|
||||
|
||||
def __getattr__(self, key):
|
||||
return getattr(self.relation_type, key)
|
||||
|
||||
def create_from_source(self, *args, **kwargs):
|
||||
# bypass our create when creating from source so as not to mess up
|
||||
# the source quoting
|
||||
return self.relation_type.create_from_source(*args, **kwargs)
|
||||
|
||||
def create(self, *args, **kwargs):
|
||||
kwargs['quote_policy'] = dbt.utils.merge(
|
||||
self.quoting_config,
|
||||
kwargs.pop('quote_policy', {})
|
||||
)
|
||||
return self.relation_type.create(*args, **kwargs)
|
||||
|
||||
|
||||
class BaseDatabaseWrapper:
|
||||
"""
|
||||
Wrapper for runtime database interaction. Applies the runtime quote policy
|
||||
via a relation proxy.
|
||||
"""
|
||||
def __init__(self, adapter):
|
||||
self.adapter = adapter
|
||||
self.Relation = RelationProxy(adapter)
|
||||
|
||||
def __getattr__(self, name):
|
||||
raise NotImplementedError('subclasses need to implement this')
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return self.adapter.config
|
||||
|
||||
def type(self):
|
||||
return self.adapter.type()
|
||||
|
||||
def commit(self):
|
||||
return self.adapter.commit_if_has_connection()
|
||||
|
||||
|
||||
class BaseResolver:
|
||||
def __init__(self, db_wrapper, model, config, manifest):
|
||||
self.db_wrapper = db_wrapper
|
||||
self.model = model
|
||||
self.config = config
|
||||
self.manifest = manifest
|
||||
|
||||
@property
|
||||
def current_project(self):
|
||||
return self.config.project_name
|
||||
|
||||
@property
|
||||
def Relation(self):
|
||||
return self.db_wrapper.Relation
|
||||
|
||||
|
||||
class Config(Protocol):
|
||||
def __init__(self, model, source_config):
|
||||
...
|
||||
|
||||
|
||||
class Provider(Protocol):
|
||||
execute: bool
|
||||
Config: Type[Config]
|
||||
DatabaseWrapper: Type[BaseDatabaseWrapper]
|
||||
Var: Type[Var]
|
||||
ref: Type[BaseResolver]
|
||||
source: Type[BaseResolver]
|
||||
|
||||
|
||||
class ManifestParsedContext(HasCredentialsContext):
|
||||
"""A context available after the manifest has been parsed."""
|
||||
def __init__(self, config, manifest):
|
||||
super().__init__(config)
|
||||
self.manifest = manifest
|
||||
|
||||
def add_macros(self, context):
|
||||
self.add_macros_from(context, self.manifest.macros)
|
||||
|
||||
|
||||
def _store_result(sql_results):
|
||||
def call(name, status, agate_table=None):
|
||||
if agate_table is None:
|
||||
agate_table = dbt.clients.agate_helper.empty_table()
|
||||
|
||||
sql_results[name] = dbt.utils.AttrDict({
|
||||
'status': status,
|
||||
'data': dbt.clients.agate_helper.as_matrix(agate_table),
|
||||
'table': agate_table
|
||||
})
|
||||
return ''
|
||||
|
||||
return call
|
||||
|
||||
|
||||
def _load_result(sql_results):
|
||||
def call(name):
|
||||
return sql_results.get(name)
|
||||
|
||||
return call
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def get_validation() -> dbt.utils.AttrDict:
|
||||
def validate_any(*args) -> Callable[[T], None]:
|
||||
def inner(value: T) -> None:
|
||||
for arg in args:
|
||||
if isinstance(arg, type) and isinstance(value, arg):
|
||||
return
|
||||
elif value == arg:
|
||||
return
|
||||
raise dbt.exceptions.ValidationException(
|
||||
'Expected value "{}" to be one of {}'
|
||||
.format(value, ','.join(map(str, args))))
|
||||
return inner
|
||||
|
||||
return dbt.utils.AttrDict({
|
||||
'any': validate_any,
|
||||
})
|
||||
|
||||
|
||||
def add_sql_handlers(context):
|
||||
sql_results = {}
|
||||
context['_sql_results'] = sql_results
|
||||
context['store_result'] = _store_result(sql_results)
|
||||
context['load_result'] = _load_result(sql_results)
|
||||
|
||||
|
||||
def write(node, target_path, subdirectory):
|
||||
def fn(payload):
|
||||
node.build_path = dbt.writer.write_node(
|
||||
node, target_path, subdirectory, payload)
|
||||
return ''
|
||||
|
||||
return fn
|
||||
|
||||
|
||||
def render(context, node):
|
||||
def fn(string):
|
||||
return get_rendered(string, context, node)
|
||||
|
||||
return fn
|
||||
|
||||
|
||||
def try_or_compiler_error(model):
|
||||
def impl(message_if_exception, func, *args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception:
|
||||
dbt.exceptions.raise_compiler_error(message_if_exception, model)
|
||||
return impl
|
||||
|
||||
|
||||
# Base context collection, used for parsing configs.
|
||||
|
||||
|
||||
def _build_load_agate_table(
|
||||
model: Union[ParsedSeedNode, CompiledSeedNode]
|
||||
) -> Callable[[], agate.Table]:
|
||||
def load_agate_table():
|
||||
path = model.seed_file_path
|
||||
try:
|
||||
table = dbt.clients.agate_helper.from_csv(path)
|
||||
except ValueError as e:
|
||||
dbt.exceptions.raise_compiler_error(str(e))
|
||||
table.original_abspath = os.path.abspath(path)
|
||||
return table
|
||||
return load_agate_table
|
||||
|
||||
|
||||
class ProviderContext(ManifestParsedContext):
|
||||
def __init__(self, model, config, manifest, provider, source_config):
|
||||
if provider is None:
|
||||
raise dbt.exceptions.InternalException(
|
||||
"Invalid provider given to context: {}".format(provider))
|
||||
self.model = model
|
||||
super().__init__(config, manifest)
|
||||
self.source_config = source_config
|
||||
self.provider = provider
|
||||
self.adapter = get_adapter(self.config)
|
||||
self.db_wrapper = self.provider.DatabaseWrapper(self.adapter)
|
||||
|
||||
@property
|
||||
def search_package_name(self):
|
||||
return self.model.package_name
|
||||
|
||||
def add_provider_functions(self, context):
|
||||
context['ref'] = self.provider.ref(
|
||||
self.db_wrapper, self.model, self.config, self.manifest
|
||||
)
|
||||
context['source'] = self.provider.source(
|
||||
self.db_wrapper, self.model, self.config, self.manifest
|
||||
)
|
||||
context['config'] = self.provider.Config(
|
||||
self.model, self.source_config
|
||||
)
|
||||
context['execute'] = self.provider.execute
|
||||
|
||||
def add_exceptions(self, context):
|
||||
context['exceptions'] = dbt.exceptions.wrapped_exports(self.model)
|
||||
|
||||
def add_default_schema_info(self, context):
|
||||
context['database'] = getattr(
|
||||
self.model, 'database', self.config.credentials.database
|
||||
)
|
||||
context['schema'] = getattr(
|
||||
self.model, 'schema', self.config.credentials.schema
|
||||
)
|
||||
|
||||
def make_var(self, context) -> Var:
|
||||
return self.provider.Var(
|
||||
self.model, context=context, overrides=self.config.cli_vars
|
||||
)
|
||||
|
||||
def insert_model_information(self, context: Dict[str, Any]) -> None:
|
||||
"""By default, the model information is not added to the context"""
|
||||
pass
|
||||
|
||||
def modify_generated_context(self, context: Dict[str, Any]) -> None:
|
||||
context['validation'] = get_validation()
|
||||
add_sql_handlers(context)
|
||||
self.add_macros(context)
|
||||
|
||||
context["write"] = write(self.model, self.config.target_path, 'run')
|
||||
context["render"] = render(context, self.model)
|
||||
context['context'] = context
|
||||
|
||||
def to_dict(self):
|
||||
target = self.get_target()
|
||||
|
||||
context = super().to_dict()
|
||||
|
||||
self.add_provider_functions(context)
|
||||
self.add_exceptions(context)
|
||||
self.add_default_schema_info(context)
|
||||
|
||||
context.update({
|
||||
"adapter": self.db_wrapper,
|
||||
"api": {
|
||||
"Relation": self.db_wrapper.Relation,
|
||||
"Column": self.adapter.Column,
|
||||
},
|
||||
"column": self.adapter.Column,
|
||||
'env': target,
|
||||
'target': target,
|
||||
"flags": dbt.flags,
|
||||
"load_agate_table": _build_load_agate_table(self.model),
|
||||
"graph": self.manifest.flat_graph,
|
||||
"model": self.model.to_dict(),
|
||||
"post_hooks": None,
|
||||
"pre_hooks": None,
|
||||
"sql": None,
|
||||
"sql_now": self.adapter.date_function(),
|
||||
"try_or_compiler_error": try_or_compiler_error(self.model)
|
||||
})
|
||||
|
||||
self.insert_model_information(context)
|
||||
|
||||
self.modify_generated_context(context)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class ExecuteMacroContext(ProviderContext):
|
||||
"""Internally, macros can be executed like nodes, with some restrictions:
|
||||
|
||||
- they don't have have all values available that nodes do:
|
||||
- 'this', 'pre_hooks', 'post_hooks', and 'sql' are missing
|
||||
- 'schema' does not use any 'model' information
|
||||
- they can't be configured with config() directives
|
||||
"""
|
||||
def __init__(self, model, config, manifest: Manifest, provider) -> None:
|
||||
super().__init__(model, config, manifest, provider, None)
|
||||
|
||||
|
||||
class ModelContext(ProviderContext):
|
||||
def get_this(self):
|
||||
return self.db_wrapper.Relation.create_from(self.config, self.model)
|
||||
|
||||
def add_hooks(self, context):
|
||||
context['pre_hooks'] = [
|
||||
h.to_dict() for h in self.model.config.pre_hook
|
||||
]
|
||||
context['post_hooks'] = [
|
||||
h.to_dict() for h in self.model.config.post_hook
|
||||
]
|
||||
|
||||
def insert_model_information(self, context):
|
||||
# operations (hooks) don't get a 'this'
|
||||
if self.model.resource_type != NodeType.Operation:
|
||||
context['this'] = self.get_this()
|
||||
# overwrite schema/database if we have them, and hooks + sql
|
||||
# the hooks should come in as dicts, at least for the `run_hooks` macro
|
||||
# TODO: do we have to preserve this as backwards a compatibility thing?
|
||||
self.add_default_schema_info(context)
|
||||
self.add_hooks(context)
|
||||
context['sql'] = getattr(self.model, 'injected_sql', None)
|
||||
|
||||
|
||||
def generate_execute_macro(
|
||||
model, config, manifest: Manifest, provider
|
||||
) -> Dict[str, Any]:
|
||||
"""Internally, macros can be executed like nodes, with some restrictions:
|
||||
|
||||
- they don't have have all values available that nodes do:
|
||||
- 'this', 'pre_hooks', 'post_hooks', and 'sql' are missing
|
||||
- 'schema' does not use any 'model' information
|
||||
- they can't be configured with config() directives
|
||||
"""
|
||||
ctx = ExecuteMacroContext(model, config, manifest, provider)
|
||||
return ctx.to_dict()
|
||||
|
||||
|
||||
def generate(
|
||||
model, config, manifest: Manifest, provider, source_config=None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Not meant to be called directly. Call with either:
|
||||
dbt.context.parser.generate
|
||||
or
|
||||
dbt.context.runtime.generate
|
||||
"""
|
||||
ctx = ModelContext(model, config, manifest, provider, source_config)
|
||||
return ctx.to_dict()
|
||||
130
core/dbt/context/configured.py
Normal file
130
core/dbt/context/configured.py
Normal file
@@ -0,0 +1,130 @@
|
||||
import os
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from dbt.contracts.connection import AdapterRequiredConfig
|
||||
from dbt.logger import SECRET_ENV_PREFIX
|
||||
from dbt.node_types import NodeType
|
||||
from dbt.utils import MultiDict
|
||||
|
||||
from dbt.context.base import contextproperty, contextmember, Var
|
||||
from dbt.context.target import TargetContext
|
||||
from dbt.exceptions import raise_parsing_error, disallow_secret_env_var
|
||||
|
||||
|
||||
class ConfiguredContext(TargetContext):
|
||||
# subclasses are SchemaYamlContext, MacroResolvingContext, ManifestContext
|
||||
config: AdapterRequiredConfig
|
||||
|
||||
def __init__(
|
||||
self, config: AdapterRequiredConfig
|
||||
) -> None:
|
||||
super().__init__(config, config.cli_vars)
|
||||
|
||||
@contextproperty
|
||||
def project_name(self) -> str:
|
||||
return self.config.project_name
|
||||
|
||||
|
||||
class FQNLookup:
|
||||
def __init__(self, package_name: str):
|
||||
self.package_name = package_name
|
||||
self.fqn = [package_name]
|
||||
self.resource_type = NodeType.Model
|
||||
|
||||
|
||||
class ConfiguredVar(Var):
|
||||
def __init__(
|
||||
self,
|
||||
context: Dict[str, Any],
|
||||
config: AdapterRequiredConfig,
|
||||
project_name: str,
|
||||
):
|
||||
super().__init__(context, config.cli_vars)
|
||||
self._config = config
|
||||
self._project_name = project_name
|
||||
|
||||
def __call__(self, var_name, default=Var._VAR_NOTSET):
|
||||
my_config = self._config.load_dependencies()[self._project_name]
|
||||
|
||||
# cli vars > active project > local project
|
||||
if var_name in self._config.cli_vars:
|
||||
return self._config.cli_vars[var_name]
|
||||
|
||||
adapter_type = self._config.credentials.type
|
||||
lookup = FQNLookup(self._project_name)
|
||||
active_vars = self._config.vars.vars_for(lookup, adapter_type)
|
||||
all_vars = MultiDict([active_vars])
|
||||
|
||||
if self._config.project_name != my_config.project_name:
|
||||
all_vars.add(my_config.vars.vars_for(lookup, adapter_type))
|
||||
|
||||
if var_name in all_vars:
|
||||
return all_vars[var_name]
|
||||
|
||||
if default is not Var._VAR_NOTSET:
|
||||
return default
|
||||
|
||||
return self.get_missing_var(var_name)
|
||||
|
||||
|
||||
class SchemaYamlVars():
|
||||
def __init__(self):
|
||||
self.env_vars = {}
|
||||
self.vars = {}
|
||||
|
||||
|
||||
class SchemaYamlContext(ConfiguredContext):
|
||||
# subclass is DocsRuntimeContext
|
||||
def __init__(self, config, project_name: str, schema_yaml_vars: Optional[SchemaYamlVars]):
|
||||
super().__init__(config)
|
||||
self._project_name = project_name
|
||||
self.schema_yaml_vars = schema_yaml_vars
|
||||
|
||||
@contextproperty
|
||||
def var(self) -> ConfiguredVar:
|
||||
return ConfiguredVar(
|
||||
self._ctx, self.config, self._project_name
|
||||
)
|
||||
|
||||
@contextmember
|
||||
def env_var(self, var: str, default: Optional[str] = None) -> str:
|
||||
return_value = None
|
||||
if var.startswith(SECRET_ENV_PREFIX):
|
||||
disallow_secret_env_var(var)
|
||||
if var in os.environ:
|
||||
return_value = os.environ[var]
|
||||
elif default is not None:
|
||||
return_value = default
|
||||
|
||||
if return_value is not None:
|
||||
if self.schema_yaml_vars:
|
||||
self.schema_yaml_vars.env_vars[var] = return_value
|
||||
return return_value
|
||||
else:
|
||||
msg = f"Env var required but not provided: '{var}'"
|
||||
raise_parsing_error(msg)
|
||||
|
||||
|
||||
class MacroResolvingContext(ConfiguredContext):
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
|
||||
@contextproperty
|
||||
def var(self) -> ConfiguredVar:
|
||||
return ConfiguredVar(
|
||||
self._ctx, self.config, self.config.project_name
|
||||
)
|
||||
|
||||
|
||||
def generate_schema_yml_context(
|
||||
config: AdapterRequiredConfig, project_name: str, schema_yaml_vars: SchemaYamlVars = None
|
||||
) -> Dict[str, Any]:
|
||||
ctx = SchemaYamlContext(config, project_name, schema_yaml_vars)
|
||||
return ctx.to_dict()
|
||||
|
||||
|
||||
def generate_macro_context(
|
||||
config: AdapterRequiredConfig,
|
||||
) -> Dict[str, Any]:
|
||||
ctx = MacroResolvingContext(config)
|
||||
return ctx.to_dict()
|
||||
313
core/dbt/context/context_config.py
Normal file
313
core/dbt/context/context_config.py
Normal file
@@ -0,0 +1,313 @@
|
||||
from abc import abstractmethod
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Iterator, Dict, Any, TypeVar, Generic
|
||||
|
||||
from dbt.config import RuntimeConfig, Project, IsFQNResource
|
||||
from dbt.contracts.graph.model_config import BaseConfig, get_config_for
|
||||
from dbt.exceptions import InternalException
|
||||
from dbt.node_types import NodeType
|
||||
from dbt.utils import fqn_search
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelParts(IsFQNResource):
|
||||
fqn: List[str]
|
||||
resource_type: NodeType
|
||||
package_name: str
|
||||
|
||||
|
||||
T = TypeVar('T') # any old type
|
||||
C = TypeVar('C', bound=BaseConfig)
|
||||
|
||||
|
||||
class ConfigSource:
|
||||
def __init__(self, project):
|
||||
self.project = project
|
||||
|
||||
def get_config_dict(self, resource_type: NodeType):
|
||||
...
|
||||
|
||||
|
||||
class UnrenderedConfig(ConfigSource):
|
||||
def __init__(self, project: Project):
|
||||
self.project = project
|
||||
|
||||
def get_config_dict(self, resource_type: NodeType) -> Dict[str, Any]:
|
||||
unrendered = self.project.unrendered.project_dict
|
||||
if resource_type == NodeType.Seed:
|
||||
model_configs = unrendered.get('seeds')
|
||||
elif resource_type == NodeType.Snapshot:
|
||||
model_configs = unrendered.get('snapshots')
|
||||
elif resource_type == NodeType.Source:
|
||||
model_configs = unrendered.get('sources')
|
||||
elif resource_type == NodeType.Test:
|
||||
model_configs = unrendered.get('tests')
|
||||
else:
|
||||
model_configs = unrendered.get('models')
|
||||
|
||||
if model_configs is None:
|
||||
return {}
|
||||
else:
|
||||
return model_configs
|
||||
|
||||
|
||||
class RenderedConfig(ConfigSource):
|
||||
def __init__(self, project: Project):
|
||||
self.project = project
|
||||
|
||||
def get_config_dict(self, resource_type: NodeType) -> Dict[str, Any]:
|
||||
if resource_type == NodeType.Seed:
|
||||
model_configs = self.project.seeds
|
||||
elif resource_type == NodeType.Snapshot:
|
||||
model_configs = self.project.snapshots
|
||||
elif resource_type == NodeType.Source:
|
||||
model_configs = self.project.sources
|
||||
elif resource_type == NodeType.Test:
|
||||
model_configs = self.project.tests
|
||||
else:
|
||||
model_configs = self.project.models
|
||||
return model_configs
|
||||
|
||||
|
||||
class BaseContextConfigGenerator(Generic[T]):
|
||||
def __init__(self, active_project: RuntimeConfig):
|
||||
self._active_project = active_project
|
||||
|
||||
def get_config_source(self, project: Project) -> ConfigSource:
|
||||
return RenderedConfig(project)
|
||||
|
||||
def get_node_project(self, project_name: str):
|
||||
if project_name == self._active_project.project_name:
|
||||
return self._active_project
|
||||
dependencies = self._active_project.load_dependencies()
|
||||
if project_name not in dependencies:
|
||||
raise InternalException(
|
||||
f'Project name {project_name} not found in dependencies '
|
||||
f'(found {list(dependencies)})'
|
||||
)
|
||||
return dependencies[project_name]
|
||||
|
||||
def _project_configs(
|
||||
self, project: Project, fqn: List[str], resource_type: NodeType
|
||||
) -> Iterator[Dict[str, Any]]:
|
||||
src = self.get_config_source(project)
|
||||
model_configs = src.get_config_dict(resource_type)
|
||||
for level_config in fqn_search(model_configs, fqn):
|
||||
result = {}
|
||||
for key, value in level_config.items():
|
||||
if key.startswith('+'):
|
||||
result[key[1:].strip()] = deepcopy(value)
|
||||
elif not isinstance(value, dict):
|
||||
result[key] = deepcopy(value)
|
||||
|
||||
yield result
|
||||
|
||||
def _active_project_configs(
|
||||
self, fqn: List[str], resource_type: NodeType
|
||||
) -> Iterator[Dict[str, Any]]:
|
||||
return self._project_configs(self._active_project, fqn, resource_type)
|
||||
|
||||
@abstractmethod
|
||||
def _update_from_config(
|
||||
self, result: T, partial: Dict[str, Any], validate: bool = False
|
||||
) -> T:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def initial_result(self, resource_type: NodeType, base: bool) -> T:
|
||||
...
|
||||
|
||||
def calculate_node_config(
|
||||
self,
|
||||
config_call_dict: Dict[str, Any],
|
||||
fqn: List[str],
|
||||
resource_type: NodeType,
|
||||
project_name: str,
|
||||
base: bool,
|
||||
patch_config_dict: Dict[str, Any] = None
|
||||
) -> BaseConfig:
|
||||
own_config = self.get_node_project(project_name)
|
||||
|
||||
result = self.initial_result(resource_type=resource_type, base=base)
|
||||
|
||||
project_configs = self._project_configs(own_config, fqn, resource_type)
|
||||
for fqn_config in project_configs:
|
||||
result = self._update_from_config(result, fqn_config)
|
||||
|
||||
# When schema files patch config, it has lower precedence than
|
||||
# config in the models (config_call_dict), so we add the patch_config_dict
|
||||
# before the config_call_dict
|
||||
if patch_config_dict:
|
||||
result = self._update_from_config(result, patch_config_dict)
|
||||
|
||||
# config_calls are created in the 'experimental' model parser and
|
||||
# the ParseConfigObject (via add_config_call)
|
||||
result = self._update_from_config(result, config_call_dict)
|
||||
|
||||
if own_config.project_name != self._active_project.project_name:
|
||||
for fqn_config in self._active_project_configs(fqn, resource_type):
|
||||
result = self._update_from_config(result, fqn_config)
|
||||
|
||||
# this is mostly impactful in the snapshot config case
|
||||
return result
|
||||
|
||||
@abstractmethod
|
||||
def calculate_node_config_dict(
|
||||
self,
|
||||
config_call_dict: Dict[str, Any],
|
||||
fqn: List[str],
|
||||
resource_type: NodeType,
|
||||
project_name: str,
|
||||
base: bool,
|
||||
patch_config_dict: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
...
|
||||
|
||||
|
||||
class ContextConfigGenerator(BaseContextConfigGenerator[C]):
|
||||
def __init__(self, active_project: RuntimeConfig):
|
||||
self._active_project = active_project
|
||||
|
||||
def get_config_source(self, project: Project) -> ConfigSource:
|
||||
return RenderedConfig(project)
|
||||
|
||||
def initial_result(self, resource_type: NodeType, base: bool) -> C:
|
||||
# defaults, own_config, config calls, active_config (if != own_config)
|
||||
config_cls = get_config_for(resource_type, base=base)
|
||||
# Calculate the defaults. We don't want to validate the defaults,
|
||||
# because it might be invalid in the case of required config members
|
||||
# (such as on snapshots!)
|
||||
result = config_cls.from_dict({})
|
||||
return result
|
||||
|
||||
def _update_from_config(
|
||||
self, result: C, partial: Dict[str, Any], validate: bool = False
|
||||
) -> C:
|
||||
translated = self._active_project.credentials.translate_aliases(
|
||||
partial
|
||||
)
|
||||
return result.update_from(
|
||||
translated,
|
||||
self._active_project.credentials.type,
|
||||
validate=validate
|
||||
)
|
||||
|
||||
def calculate_node_config_dict(
|
||||
self,
|
||||
config_call_dict: Dict[str, Any],
|
||||
fqn: List[str],
|
||||
resource_type: NodeType,
|
||||
project_name: str,
|
||||
base: bool,
|
||||
patch_config_dict: dict = None
|
||||
) -> Dict[str, Any]:
|
||||
config = self.calculate_node_config(
|
||||
config_call_dict=config_call_dict,
|
||||
fqn=fqn,
|
||||
resource_type=resource_type,
|
||||
project_name=project_name,
|
||||
base=base,
|
||||
patch_config_dict=patch_config_dict
|
||||
)
|
||||
finalized = config.finalize_and_validate()
|
||||
return finalized.to_dict(omit_none=True)
|
||||
|
||||
|
||||
class UnrenderedConfigGenerator(BaseContextConfigGenerator[Dict[str, Any]]):
|
||||
def get_config_source(self, project: Project) -> ConfigSource:
|
||||
return UnrenderedConfig(project)
|
||||
|
||||
def calculate_node_config_dict(
|
||||
self,
|
||||
config_call_dict: Dict[str, Any],
|
||||
fqn: List[str],
|
||||
resource_type: NodeType,
|
||||
project_name: str,
|
||||
base: bool,
|
||||
patch_config_dict: dict = None
|
||||
) -> Dict[str, Any]:
|
||||
return self.calculate_node_config(
|
||||
config_call_dict=config_call_dict,
|
||||
fqn=fqn,
|
||||
resource_type=resource_type,
|
||||
project_name=project_name,
|
||||
base=base,
|
||||
patch_config_dict=patch_config_dict
|
||||
)
|
||||
|
||||
def initial_result(
|
||||
self,
|
||||
resource_type: NodeType,
|
||||
base: bool
|
||||
) -> Dict[str, Any]:
|
||||
return {}
|
||||
|
||||
def _update_from_config(
|
||||
self,
|
||||
result: Dict[str, Any],
|
||||
partial: Dict[str, Any],
|
||||
validate: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
translated = self._active_project.credentials.translate_aliases(
|
||||
partial
|
||||
)
|
||||
result.update(translated)
|
||||
return result
|
||||
|
||||
|
||||
class ContextConfig:
|
||||
def __init__(
|
||||
self,
|
||||
active_project: RuntimeConfig,
|
||||
fqn: List[str],
|
||||
resource_type: NodeType,
|
||||
project_name: str,
|
||||
) -> None:
|
||||
self._config_call_dict: Dict[str, Any] = {}
|
||||
self._active_project = active_project
|
||||
self._fqn = fqn
|
||||
self._resource_type = resource_type
|
||||
self._project_name = project_name
|
||||
|
||||
def add_config_call(self, opts: Dict[str, Any]) -> None:
|
||||
dct = self._config_call_dict
|
||||
self._add_config_call(dct, opts)
|
||||
|
||||
@classmethod
|
||||
def _add_config_call(cls, config_call_dict, opts: Dict[str, Any]) -> None:
|
||||
for k, v in opts.items():
|
||||
# MergeBehavior for post-hook and pre-hook is to collect all
|
||||
# values, instead of overwriting
|
||||
if k in BaseConfig.mergebehavior['append']:
|
||||
if not isinstance(v, list):
|
||||
v = [v]
|
||||
if k in BaseConfig.mergebehavior['update'] and not isinstance(v, dict):
|
||||
raise InternalException(f'expected dict, got {v}')
|
||||
if k in config_call_dict and isinstance(config_call_dict[k], list):
|
||||
config_call_dict[k].extend(v)
|
||||
elif k in config_call_dict and isinstance(config_call_dict[k], dict):
|
||||
config_call_dict[k].update(v)
|
||||
else:
|
||||
config_call_dict[k] = v
|
||||
|
||||
def build_config_dict(
|
||||
self,
|
||||
base: bool = False,
|
||||
*,
|
||||
rendered: bool = True,
|
||||
patch_config_dict: dict = None
|
||||
) -> Dict[str, Any]:
|
||||
if rendered:
|
||||
src = ContextConfigGenerator(self._active_project)
|
||||
else:
|
||||
src = UnrenderedConfigGenerator(self._active_project)
|
||||
|
||||
return src.calculate_node_config_dict(
|
||||
config_call_dict=self._config_call_dict,
|
||||
fqn=self._fqn,
|
||||
resource_type=self._resource_type,
|
||||
project_name=self._project_name,
|
||||
base=base,
|
||||
patch_config_dict=patch_config_dict
|
||||
)
|
||||
86
core/dbt/context/docs.py
Normal file
86
core/dbt/context/docs.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from typing import (
|
||||
Any, Dict, Union
|
||||
)
|
||||
|
||||
from dbt.exceptions import (
|
||||
doc_invalid_args,
|
||||
doc_target_not_found,
|
||||
)
|
||||
from dbt.config.runtime import RuntimeConfig
|
||||
from dbt.contracts.graph.compiled import CompileResultNode
|
||||
from dbt.contracts.graph.manifest import Manifest
|
||||
from dbt.contracts.graph.parsed import ParsedMacro
|
||||
|
||||
from dbt.context.base import contextmember
|
||||
from dbt.context.configured import SchemaYamlContext
|
||||
|
||||
|
||||
class DocsRuntimeContext(SchemaYamlContext):
|
||||
def __init__(
|
||||
self,
|
||||
config: RuntimeConfig,
|
||||
node: Union[ParsedMacro, CompileResultNode],
|
||||
manifest: Manifest,
|
||||
current_project: str,
|
||||
) -> None:
|
||||
super().__init__(config, current_project, None)
|
||||
self.node = node
|
||||
self.manifest = manifest
|
||||
|
||||
@contextmember
|
||||
def doc(self, *args: str) -> str:
|
||||
"""The `doc` function is used to reference docs blocks in schema.yml
|
||||
files. It is analogous to the `ref` function. For more information,
|
||||
consult the Documentation guide.
|
||||
|
||||
> orders.md:
|
||||
|
||||
{% docs orders %}
|
||||
# docs
|
||||
- go
|
||||
- here
|
||||
{% enddocs %}
|
||||
|
||||
> schema.yml
|
||||
|
||||
version: 2
|
||||
models:
|
||||
- name: orders
|
||||
description: "{{ doc('orders') }}"
|
||||
"""
|
||||
# when you call doc(), this is what happens at runtime
|
||||
if len(args) == 1:
|
||||
doc_package_name = None
|
||||
doc_name = args[0]
|
||||
elif len(args) == 2:
|
||||
doc_package_name, doc_name = args
|
||||
else:
|
||||
doc_invalid_args(self.node, args)
|
||||
|
||||
# ParsedDocumentation
|
||||
target_doc = self.manifest.resolve_doc(
|
||||
doc_name,
|
||||
doc_package_name,
|
||||
self._project_name,
|
||||
self.node.package_name,
|
||||
)
|
||||
if target_doc:
|
||||
file_id = target_doc.file_id
|
||||
if file_id in self.manifest.files:
|
||||
source_file = self.manifest.files[file_id]
|
||||
source_file.add_node(self.node.unique_id)
|
||||
else:
|
||||
doc_target_not_found(self.node, doc_name, doc_package_name)
|
||||
|
||||
return target_doc.block_contents
|
||||
|
||||
|
||||
def generate_runtime_docs_context(
|
||||
config: RuntimeConfig,
|
||||
target: Any,
|
||||
manifest: Manifest,
|
||||
current_project: str,
|
||||
) -> Dict[str, Any]:
|
||||
ctx = DocsRuntimeContext(config, target, manifest, current_project)
|
||||
# This is not a Mashumaro to_dict call
|
||||
return ctx.to_dict()
|
||||
199
core/dbt/context/macro_resolver.py
Normal file
199
core/dbt/context/macro_resolver.py
Normal file
@@ -0,0 +1,199 @@
|
||||
from typing import (
|
||||
Dict, MutableMapping, Optional
|
||||
)
|
||||
from dbt.contracts.graph.parsed import ParsedMacro
|
||||
from dbt.exceptions import raise_duplicate_macro_name, raise_compiler_error
|
||||
from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME
|
||||
from dbt.clients.jinja import MacroGenerator
|
||||
|
||||
MacroNamespace = Dict[str, ParsedMacro]
|
||||
|
||||
|
||||
# This class builds the MacroResolver by adding macros
|
||||
# to various categories for finding macros in the right order,
|
||||
# so that higher precedence macros are found first.
|
||||
# This functionality is also provided by the MacroNamespace,
|
||||
# but the intention is to eventually replace that class.
|
||||
# This enables us to get the macro unique_id without
|
||||
# processing every macro in the project.
|
||||
# Note: the root project macros override everything in the
|
||||
# dbt internal projects. External projects (dependencies) will
|
||||
# use their own macros first, then pull from the root project
|
||||
# followed by dbt internal projects.
|
||||
class MacroResolver:
|
||||
def __init__(
|
||||
self,
|
||||
macros: MutableMapping[str, ParsedMacro],
|
||||
root_project_name: str,
|
||||
internal_package_names,
|
||||
) -> None:
|
||||
self.root_project_name = root_project_name
|
||||
self.macros = macros
|
||||
# internal packages comes from get_adapter_package_names
|
||||
self.internal_package_names = internal_package_names
|
||||
|
||||
# To be filled in from macros.
|
||||
self.internal_packages: Dict[str, MacroNamespace] = {}
|
||||
self.packages: Dict[str, MacroNamespace] = {}
|
||||
self.root_package_macros: MacroNamespace = {}
|
||||
|
||||
# add the macros to internal_packages, packages, and root packages
|
||||
self.add_macros()
|
||||
self._build_internal_packages_namespace()
|
||||
self._build_macros_by_name()
|
||||
|
||||
def _build_internal_packages_namespace(self):
|
||||
# Iterate in reverse-order and overwrite: the packages that are first
|
||||
# in the list are the ones we want to "win".
|
||||
self.internal_packages_namespace: MacroNamespace = {}
|
||||
for pkg in reversed(self.internal_package_names):
|
||||
if pkg in self.internal_packages:
|
||||
# Turn the internal packages into a flat namespace
|
||||
self.internal_packages_namespace.update(
|
||||
self.internal_packages[pkg])
|
||||
|
||||
# search order:
|
||||
# local_namespace (package of particular node), not including
|
||||
# the internal packages or the root package
|
||||
# This means that within an extra package, it uses its own macros
|
||||
# root package namespace
|
||||
# non-internal packages (that aren't local or root)
|
||||
# dbt internal packages
|
||||
def _build_macros_by_name(self):
|
||||
macros_by_name = {}
|
||||
|
||||
# all internal packages (already in the right order)
|
||||
for macro in self.internal_packages_namespace.values():
|
||||
macros_by_name[macro.name] = macro
|
||||
|
||||
# non-internal packages
|
||||
for fnamespace in self.packages.values():
|
||||
for macro in fnamespace.values():
|
||||
macros_by_name[macro.name] = macro
|
||||
|
||||
# root package macros
|
||||
for macro in self.root_package_macros.values():
|
||||
macros_by_name[macro.name] = macro
|
||||
|
||||
self.macros_by_name = macros_by_name
|
||||
|
||||
def _add_macro_to(
|
||||
self,
|
||||
package_namespaces: Dict[str, MacroNamespace],
|
||||
macro: ParsedMacro,
|
||||
):
|
||||
if macro.package_name in package_namespaces:
|
||||
namespace = package_namespaces[macro.package_name]
|
||||
else:
|
||||
namespace = {}
|
||||
package_namespaces[macro.package_name] = namespace
|
||||
|
||||
if macro.name in namespace:
|
||||
raise_duplicate_macro_name(
|
||||
macro, macro, macro.package_name
|
||||
)
|
||||
package_namespaces[macro.package_name][macro.name] = macro
|
||||
|
||||
def add_macro(self, macro: ParsedMacro):
|
||||
macro_name: str = macro.name
|
||||
|
||||
# internal macros (from plugins) will be processed separately from
|
||||
# project macros, so store them in a different place
|
||||
if macro.package_name in self.internal_package_names:
|
||||
self._add_macro_to(self.internal_packages, macro)
|
||||
else:
|
||||
# if it's not an internal package
|
||||
self._add_macro_to(self.packages, macro)
|
||||
# add to root_package_macros if it's in the root package
|
||||
if macro.package_name == self.root_project_name:
|
||||
self.root_package_macros[macro_name] = macro
|
||||
|
||||
def add_macros(self):
|
||||
for macro in self.macros.values():
|
||||
self.add_macro(macro)
|
||||
|
||||
def get_macro(self, local_package, macro_name):
|
||||
local_package_macros = {}
|
||||
if (local_package not in self.internal_package_names and
|
||||
local_package in self.packages):
|
||||
local_package_macros = self.packages[local_package]
|
||||
# First: search the local packages for this macro
|
||||
if macro_name in local_package_macros:
|
||||
return local_package_macros[macro_name]
|
||||
# Now look up in the standard search order
|
||||
if macro_name in self.macros_by_name:
|
||||
return self.macros_by_name[macro_name]
|
||||
return None
|
||||
|
||||
def get_macro_id(self, local_package, macro_name):
|
||||
macro = self.get_macro(local_package, macro_name)
|
||||
if macro is None:
|
||||
return None
|
||||
else:
|
||||
return macro.unique_id
|
||||
|
||||
|
||||
# Currently this is just used by test processing in the schema
|
||||
# parser (in connection with the MacroResolver). Future work
|
||||
# will extend the use of these classes to other parsing areas.
|
||||
# One of the features of this class compared to the MacroNamespace
|
||||
# is that you can limit the number of macros provided to the
|
||||
# context dictionary in the 'to_dict' manifest method.
|
||||
class TestMacroNamespace:
|
||||
def __init__(
|
||||
self, macro_resolver, ctx, node, thread_ctx, depends_on_macros
|
||||
):
|
||||
self.macro_resolver = macro_resolver
|
||||
self.ctx = ctx
|
||||
self.node = node # can be none
|
||||
self.thread_ctx = thread_ctx
|
||||
self.local_namespace = {}
|
||||
self.project_namespace = {}
|
||||
if depends_on_macros:
|
||||
dep_macros = []
|
||||
self.recursively_get_depends_on_macros(depends_on_macros, dep_macros)
|
||||
for macro_unique_id in dep_macros:
|
||||
if macro_unique_id in self.macro_resolver.macros:
|
||||
# Split up the macro unique_id to get the project_name
|
||||
(_, project_name, macro_name) = macro_unique_id.split('.')
|
||||
# Save the plain macro_name in the local_namespace
|
||||
macro = self.macro_resolver.macros[macro_unique_id]
|
||||
macro_gen = MacroGenerator(
|
||||
macro, self.ctx, self.node, self.thread_ctx,
|
||||
)
|
||||
self.local_namespace[macro_name] = macro_gen
|
||||
# We also need the two part macro name
|
||||
if project_name not in self.project_namespace:
|
||||
self.project_namespace[project_name] = {}
|
||||
self.project_namespace[project_name][macro_name] = macro_gen
|
||||
|
||||
def recursively_get_depends_on_macros(self, depends_on_macros, dep_macros):
|
||||
for macro_unique_id in depends_on_macros:
|
||||
if macro_unique_id in dep_macros:
|
||||
continue
|
||||
dep_macros.append(macro_unique_id)
|
||||
if macro_unique_id in self.macro_resolver.macros:
|
||||
macro = self.macro_resolver.macros[macro_unique_id]
|
||||
if macro.depends_on.macros:
|
||||
self.recursively_get_depends_on_macros(macro.depends_on.macros, dep_macros)
|
||||
|
||||
def get_from_package(
|
||||
self, package_name: Optional[str], name: str
|
||||
) -> Optional[MacroGenerator]:
|
||||
macro = None
|
||||
if package_name is None:
|
||||
macro = self.macro_resolver.macros_by_name.get(name)
|
||||
elif package_name == GLOBAL_PROJECT_NAME:
|
||||
macro = self.macro_resolver.internal_packages_namespace.get(name)
|
||||
elif package_name in self.macro_resolver.packages:
|
||||
macro = self.macro_resolver.packages[package_name].get(name)
|
||||
else:
|
||||
raise_compiler_error(
|
||||
f"Could not find package '{package_name}'"
|
||||
)
|
||||
if not macro:
|
||||
return None
|
||||
macro_func = MacroGenerator(
|
||||
macro, self.ctx, self.node, self.thread_ctx
|
||||
)
|
||||
return macro_func
|
||||
182
core/dbt/context/macros.py
Normal file
182
core/dbt/context/macros.py
Normal file
@@ -0,0 +1,182 @@
|
||||
from typing import (
|
||||
Any, Dict, Iterable, Union, Optional, List, Iterator, Mapping, Set
|
||||
)
|
||||
|
||||
from dbt.clients.jinja import MacroGenerator, MacroStack
|
||||
from dbt.contracts.graph.parsed import ParsedMacro
|
||||
from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME
|
||||
from dbt.exceptions import (
|
||||
raise_duplicate_macro_name, raise_compiler_error
|
||||
)
|
||||
|
||||
|
||||
FlatNamespace = Dict[str, MacroGenerator]
|
||||
NamespaceMember = Union[FlatNamespace, MacroGenerator]
|
||||
FullNamespace = Dict[str, NamespaceMember]
|
||||
|
||||
|
||||
# The point of this class is to collect the various macros
|
||||
# and provide the ability to flatten them into the ManifestContexts
|
||||
# that are created for jinja, so that macro calls can be resolved.
|
||||
# Creates special iterators and _keys methods to flatten the lists.
|
||||
# When this class is created it has a static 'local_namespace' which
|
||||
# depends on the package of the node, so it only works for one
|
||||
# particular local package at a time for "flattening" into a context.
|
||||
# 'get_by_package' should work for any macro.
|
||||
class MacroNamespace(Mapping):
|
||||
def __init__(
|
||||
self,
|
||||
global_namespace: FlatNamespace, # root package macros
|
||||
local_namespace: FlatNamespace, # packages for *this* node
|
||||
global_project_namespace: FlatNamespace, # internal packages
|
||||
packages: Dict[str, FlatNamespace], # non-internal packages
|
||||
):
|
||||
self.global_namespace: FlatNamespace = global_namespace
|
||||
self.local_namespace: FlatNamespace = local_namespace
|
||||
self.packages: Dict[str, FlatNamespace] = packages
|
||||
self.global_project_namespace: FlatNamespace = global_project_namespace
|
||||
|
||||
def _search_order(self) -> Iterable[Union[FullNamespace, FlatNamespace]]:
|
||||
yield self.local_namespace # local package
|
||||
yield self.global_namespace # root package
|
||||
yield self.packages # non-internal packages
|
||||
yield {
|
||||
GLOBAL_PROJECT_NAME: self.global_project_namespace, # dbt
|
||||
}
|
||||
yield self.global_project_namespace # other internal project besides dbt
|
||||
|
||||
# provides special keys method for MacroNamespace iterator
|
||||
# returns keys from local_namespace, global_namespace, packages,
|
||||
# global_project_namespace
|
||||
def _keys(self) -> Set[str]:
|
||||
keys: Set[str] = set()
|
||||
for search in self._search_order():
|
||||
keys.update(search)
|
||||
return keys
|
||||
|
||||
# special iterator using special keys
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
for key in self._keys():
|
||||
yield key
|
||||
|
||||
def __len__(self):
|
||||
return len(self._keys())
|
||||
|
||||
def __getitem__(self, key: str) -> NamespaceMember:
|
||||
for dct in self._search_order():
|
||||
if key in dct:
|
||||
return dct[key]
|
||||
raise KeyError(key)
|
||||
|
||||
def get_from_package(
|
||||
self, package_name: Optional[str], name: str
|
||||
) -> Optional[MacroGenerator]:
|
||||
pkg: FlatNamespace
|
||||
if package_name is None:
|
||||
return self.get(name)
|
||||
elif package_name == GLOBAL_PROJECT_NAME:
|
||||
return self.global_project_namespace.get(name)
|
||||
elif package_name in self.packages:
|
||||
return self.packages[package_name].get(name)
|
||||
else:
|
||||
raise_compiler_error(
|
||||
f"Could not find package '{package_name}'"
|
||||
)
|
||||
|
||||
|
||||
# This class builds the MacroNamespace by adding macros to
|
||||
# internal_packages or packages, and locals/globals.
|
||||
# Call 'build_namespace' to return a MacroNamespace.
|
||||
# This is used by ManifestContext (and subclasses)
|
||||
class MacroNamespaceBuilder:
|
||||
def __init__(
|
||||
self,
|
||||
root_package: str,
|
||||
search_package: str,
|
||||
thread_ctx: MacroStack,
|
||||
internal_packages: List[str],
|
||||
node: Optional[Any] = None,
|
||||
) -> None:
|
||||
self.root_package = root_package
|
||||
self.search_package = search_package
|
||||
# internal packages comes from get_adapter_package_names
|
||||
self.internal_package_names = set(internal_packages)
|
||||
self.internal_package_names_order = internal_packages
|
||||
# macro_func is added here if in root package, since
|
||||
# the root package acts as a "global" namespace, overriding
|
||||
# everything else except local external package macro calls
|
||||
self.globals: FlatNamespace = {}
|
||||
# macro_func is added here if it's the package for this node
|
||||
self.locals: FlatNamespace = {}
|
||||
# Create a dictionary of [package name][macro name] =
|
||||
# MacroGenerator object which acts like a function
|
||||
self.internal_packages: Dict[str, FlatNamespace] = {}
|
||||
self.packages: Dict[str, FlatNamespace] = {}
|
||||
self.thread_ctx = thread_ctx
|
||||
self.node = node
|
||||
|
||||
def _add_macro_to(
|
||||
self,
|
||||
hierarchy: Dict[str, FlatNamespace],
|
||||
macro: ParsedMacro,
|
||||
macro_func: MacroGenerator,
|
||||
):
|
||||
if macro.package_name in hierarchy:
|
||||
namespace = hierarchy[macro.package_name]
|
||||
else:
|
||||
namespace = {}
|
||||
hierarchy[macro.package_name] = namespace
|
||||
|
||||
if macro.name in namespace:
|
||||
raise_duplicate_macro_name(
|
||||
macro_func.macro, macro, macro.package_name
|
||||
)
|
||||
hierarchy[macro.package_name][macro.name] = macro_func
|
||||
|
||||
def add_macro(self, macro: ParsedMacro, ctx: Dict[str, Any]):
|
||||
macro_name: str = macro.name
|
||||
|
||||
# MacroGenerator is in clients/jinja.py
|
||||
# a MacroGenerator object is a callable object that will
|
||||
# execute the MacroGenerator.__call__ function
|
||||
macro_func: MacroGenerator = MacroGenerator(
|
||||
macro, ctx, self.node, self.thread_ctx
|
||||
)
|
||||
|
||||
# internal macros (from plugins) will be processed separately from
|
||||
# project macros, so store them in a different place
|
||||
if macro.package_name in self.internal_package_names:
|
||||
self._add_macro_to(self.internal_packages, macro, macro_func)
|
||||
else:
|
||||
# if it's not an internal package
|
||||
self._add_macro_to(self.packages, macro, macro_func)
|
||||
# add to locals if it's the package this node is in
|
||||
if macro.package_name == self.search_package:
|
||||
self.locals[macro_name] = macro_func
|
||||
# add to globals if it's in the root package
|
||||
elif macro.package_name == self.root_package:
|
||||
self.globals[macro_name] = macro_func
|
||||
|
||||
def add_macros(self, macros: Iterable[ParsedMacro], ctx: Dict[str, Any]):
|
||||
for macro in macros:
|
||||
self.add_macro(macro, ctx)
|
||||
|
||||
def build_namespace(
|
||||
self, macros: Iterable[ParsedMacro], ctx: Dict[str, Any]
|
||||
) -> MacroNamespace:
|
||||
self.add_macros(macros, ctx)
|
||||
|
||||
# Iterate in reverse-order and overwrite: the packages that are first
|
||||
# in the list are the ones we want to "win".
|
||||
global_project_namespace: FlatNamespace = {}
|
||||
for pkg in reversed(self.internal_package_names_order):
|
||||
if pkg in self.internal_packages:
|
||||
# add the macros pointed to by this package name
|
||||
global_project_namespace.update(self.internal_packages[pkg])
|
||||
|
||||
return MacroNamespace(
|
||||
global_namespace=self.globals, # root package macros
|
||||
local_namespace=self.locals, # packages for *this* node
|
||||
global_project_namespace=global_project_namespace, # internal packages
|
||||
packages=self.packages, # non internal_packages
|
||||
)
|
||||
83
core/dbt/context/manifest.py
Normal file
83
core/dbt/context/manifest.py
Normal file
@@ -0,0 +1,83 @@
|
||||
from typing import List
|
||||
|
||||
from dbt.clients.jinja import MacroStack
|
||||
from dbt.contracts.connection import AdapterRequiredConfig
|
||||
from dbt.contracts.graph.manifest import Manifest
|
||||
from dbt.context.macro_resolver import TestMacroNamespace
|
||||
|
||||
|
||||
from .configured import ConfiguredContext
|
||||
from .macros import MacroNamespaceBuilder
|
||||
|
||||
|
||||
class ManifestContext(ConfiguredContext):
|
||||
"""The Macro context has everything in the target context, plus the macros
|
||||
in the manifest.
|
||||
|
||||
The given macros can override any previous context values, which will be
|
||||
available as if they were accessed relative to the package name.
|
||||
"""
|
||||
# subclasses are QueryHeaderContext and ProviderContext
|
||||
def __init__(
|
||||
self,
|
||||
config: AdapterRequiredConfig,
|
||||
manifest: Manifest,
|
||||
search_package: str,
|
||||
) -> None:
|
||||
super().__init__(config)
|
||||
self.manifest = manifest
|
||||
# this is the package of the node for which this context was built
|
||||
self.search_package = search_package
|
||||
self.macro_stack = MacroStack()
|
||||
# This namespace is used by the BaseDatabaseWrapper in jinja rendering.
|
||||
# The namespace is passed to it when it's constructed. It expects
|
||||
# to be able to do: namespace.get_from_package(..)
|
||||
self.namespace = self._build_namespace()
|
||||
|
||||
def _build_namespace(self):
|
||||
# this takes all the macros in the manifest and adds them
|
||||
# to the MacroNamespaceBuilder stored in self.namespace
|
||||
builder = self._get_namespace_builder()
|
||||
return builder.build_namespace(
|
||||
self.manifest.macros.values(), self._ctx
|
||||
)
|
||||
|
||||
def _get_namespace_builder(self) -> MacroNamespaceBuilder:
|
||||
# avoid an import loop
|
||||
from dbt.adapters.factory import get_adapter_package_names
|
||||
internal_packages: List[str] = get_adapter_package_names(
|
||||
self.config.credentials.type
|
||||
)
|
||||
return MacroNamespaceBuilder(
|
||||
self.config.project_name,
|
||||
self.search_package,
|
||||
self.macro_stack,
|
||||
internal_packages,
|
||||
None,
|
||||
)
|
||||
|
||||
# This does not use the Mashumaro code
|
||||
def to_dict(self):
|
||||
dct = super().to_dict()
|
||||
# This moves all of the macros in the 'namespace' into top level
|
||||
# keys in the manifest dictionary
|
||||
if isinstance(self.namespace, TestMacroNamespace):
|
||||
dct.update(self.namespace.local_namespace)
|
||||
dct.update(self.namespace.project_namespace)
|
||||
else:
|
||||
dct.update(self.namespace)
|
||||
return dct
|
||||
|
||||
|
||||
class QueryHeaderContext(ManifestContext):
|
||||
def __init__(
|
||||
self, config: AdapterRequiredConfig, manifest: Manifest
|
||||
) -> None:
|
||||
super().__init__(config, manifest, config.project_name)
|
||||
|
||||
|
||||
def generate_query_header_context(
|
||||
config: AdapterRequiredConfig, manifest: Manifest
|
||||
):
|
||||
ctx = QueryHeaderContext(config, manifest)
|
||||
return ctx.to_dict()
|
||||
@@ -1,29 +0,0 @@
|
||||
import dbt.context.common
|
||||
from dbt.context import runtime
|
||||
from dbt.exceptions import raise_compiler_error
|
||||
|
||||
|
||||
class RefResolver(runtime.RefResolver):
|
||||
def __call__(self, *args):
|
||||
# When you call ref(), this is what happens at operation runtime
|
||||
target_model, name = self.resolve(args)
|
||||
return self.create_relation(target_model, name)
|
||||
|
||||
def create_ephemeral_relation(self, target_model, name):
|
||||
# In operations, we can't ref() ephemeral nodes, because ParsedMacros
|
||||
# do not support set_cte
|
||||
raise_compiler_error(
|
||||
'Operations can not ref() ephemeral nodes, but {} is ephemeral'
|
||||
.format(target_model.name),
|
||||
self.model
|
||||
)
|
||||
|
||||
|
||||
class Provider(runtime.Provider):
|
||||
ref = RefResolver
|
||||
|
||||
|
||||
def generate(model, runtime_config, manifest):
|
||||
return dbt.context.common.generate_execute_macro(
|
||||
model, runtime_config, manifest, Provider()
|
||||
)
|
||||
@@ -1,150 +0,0 @@
|
||||
import dbt.exceptions
|
||||
|
||||
import dbt.context.common
|
||||
from dbt.adapters.factory import get_adapter
|
||||
from dbt.contracts.graph.parsed import Docref
|
||||
|
||||
|
||||
def docs(unparsed, docrefs, column_name=None):
|
||||
|
||||
def do_docs(*args):
|
||||
if len(args) != 1 and len(args) != 2:
|
||||
dbt.exceptions.doc_invalid_args(unparsed, args)
|
||||
doc_package_name = ''
|
||||
doc_name = args[0]
|
||||
if len(args) == 2:
|
||||
doc_package_name = args[1]
|
||||
|
||||
docref = Docref(documentation_package=doc_package_name,
|
||||
documentation_name=doc_name,
|
||||
column_name=column_name)
|
||||
docrefs.append(docref)
|
||||
|
||||
# At parse time, nothing should care about what doc() returns
|
||||
return ''
|
||||
|
||||
return do_docs
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, model, source_config):
|
||||
self.model = model
|
||||
self.source_config = source_config
|
||||
|
||||
def _transform_config(self, config):
|
||||
for oldkey in ('pre_hook', 'post_hook'):
|
||||
if oldkey in config:
|
||||
newkey = oldkey.replace('_', '-')
|
||||
if newkey in config:
|
||||
dbt.exceptions.raise_compiler_error(
|
||||
'Invalid config, has conflicting keys "{}" and "{}"'
|
||||
.format(oldkey, newkey),
|
||||
self.model
|
||||
)
|
||||
config[newkey] = config.pop(oldkey)
|
||||
return config
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
if len(args) == 1 and len(kwargs) == 0:
|
||||
opts = args[0]
|
||||
elif len(args) == 0 and len(kwargs) > 0:
|
||||
opts = kwargs
|
||||
else:
|
||||
dbt.exceptions.raise_compiler_error(
|
||||
"Invalid inline model config",
|
||||
self.model)
|
||||
|
||||
opts = self._transform_config(opts)
|
||||
|
||||
self.source_config.update_in_model_config(opts)
|
||||
return ''
|
||||
|
||||
def set(self, name, value):
|
||||
return self.__call__({name: value})
|
||||
|
||||
def require(self, name, validator=None):
|
||||
return ''
|
||||
|
||||
def get(self, name, validator=None, default=None):
|
||||
return ''
|
||||
|
||||
|
||||
class DatabaseWrapper(dbt.context.common.BaseDatabaseWrapper):
|
||||
"""The parser subclass of the database wrapper applies any explicit
|
||||
parse-time overrides.
|
||||
"""
|
||||
def __getattr__(self, name):
|
||||
override = (name in self.adapter._available_ and
|
||||
name in self.adapter._parse_replacements_)
|
||||
|
||||
if override:
|
||||
return self.adapter._parse_replacements_[name]
|
||||
elif name in self.adapter._available_:
|
||||
return getattr(self.adapter, name)
|
||||
else:
|
||||
raise AttributeError(
|
||||
"'{}' object has no attribute '{}'".format(
|
||||
self.__class__.__name__, name
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Var(dbt.context.base.Var):
|
||||
def get_missing_var(self, var_name):
|
||||
# in the parser, just always return None.
|
||||
return None
|
||||
|
||||
|
||||
class RefResolver(dbt.context.common.BaseResolver):
|
||||
def __call__(self, *args):
|
||||
# When you call ref(), this is what happens at parse time
|
||||
if len(args) == 1 or len(args) == 2:
|
||||
self.model.refs.append(list(args))
|
||||
|
||||
else:
|
||||
dbt.exceptions.ref_invalid_args(self.model, args)
|
||||
|
||||
return self.Relation.create_from(self.config, self.model)
|
||||
|
||||
|
||||
class SourceResolver(dbt.context.common.BaseResolver):
|
||||
def __call__(self, *args):
|
||||
# When you call source(), this is what happens at parse time
|
||||
if len(args) == 2:
|
||||
self.model.sources.append(list(args))
|
||||
|
||||
else:
|
||||
dbt.exceptions.raise_compiler_error(
|
||||
"source() takes exactly two arguments ({} given)"
|
||||
.format(len(args)), self.model)
|
||||
|
||||
return self.Relation.create_from(self.config, self.model)
|
||||
|
||||
|
||||
class Provider(dbt.context.common.Provider):
|
||||
execute = False
|
||||
Config = Config
|
||||
DatabaseWrapper = DatabaseWrapper
|
||||
Var = Var
|
||||
ref = RefResolver
|
||||
source = SourceResolver
|
||||
|
||||
|
||||
def generate(model, runtime_config, manifest, source_config):
|
||||
# during parsing, we don't have a connection, but we might need one, so we
|
||||
# have to acquire it.
|
||||
# In the future, it would be nice to lazily open the connection, as in some
|
||||
# projects it would be possible to parse without connecting to the db
|
||||
with get_adapter(runtime_config).connection_for(model):
|
||||
return dbt.context.common.generate(
|
||||
model, runtime_config, manifest, Provider(), source_config
|
||||
)
|
||||
|
||||
|
||||
def generate_macro(model, runtime_config, manifest):
|
||||
# parser.generate_macro is called by the get_${attr}_func family of Parser
|
||||
# methods, which preparse and cache the generate_${attr}_name family of
|
||||
# macros for use during parsing
|
||||
return dbt.context.common.generate_execute_macro(
|
||||
model, runtime_config, manifest, Provider()
|
||||
)
|
||||
1523
core/dbt/context/providers.py
Normal file
1523
core/dbt/context/providers.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,162 +0,0 @@
|
||||
from dbt.utils import get_materialization, add_ephemeral_model_prefix
|
||||
|
||||
import dbt.clients.jinja
|
||||
import dbt.context.base
|
||||
import dbt.context.common
|
||||
import dbt.flags
|
||||
from dbt.parser.util import ParserUtils
|
||||
|
||||
from dbt.logger import GLOBAL_LOGGER as logger # noqa
|
||||
|
||||
|
||||
class RefResolver(dbt.context.common.BaseResolver):
|
||||
def resolve(self, args):
|
||||
name = None
|
||||
package = None
|
||||
|
||||
if len(args) == 1:
|
||||
name = args[0]
|
||||
elif len(args) == 2:
|
||||
package, name = args
|
||||
else:
|
||||
dbt.exceptions.ref_invalid_args(self.model, args)
|
||||
|
||||
target_model = ParserUtils.resolve_ref(
|
||||
self.manifest,
|
||||
name,
|
||||
package,
|
||||
self.current_project,
|
||||
self.model.package_name)
|
||||
|
||||
if target_model is None or target_model is ParserUtils.DISABLED:
|
||||
dbt.exceptions.ref_target_not_found(
|
||||
self.model,
|
||||
name,
|
||||
package)
|
||||
return target_model, name
|
||||
|
||||
def create_ephemeral_relation(self, target_model, name):
|
||||
self.model.set_cte(target_model.unique_id, None)
|
||||
return self.Relation.create(
|
||||
type=self.Relation.CTE,
|
||||
identifier=add_ephemeral_model_prefix(name)
|
||||
).quote(identifier=False)
|
||||
|
||||
def create_relation(self, target_model, name):
|
||||
if get_materialization(target_model) == 'ephemeral':
|
||||
return self.create_ephemeral_relation(target_model, name)
|
||||
else:
|
||||
return self.Relation.create_from(self.config, target_model)
|
||||
|
||||
def validate(self, resolved, args):
|
||||
if resolved.unique_id not in self.model.depends_on.nodes:
|
||||
dbt.exceptions.ref_bad_context(self.model, args)
|
||||
|
||||
def __call__(self, *args):
|
||||
# When you call ref(), this is what happens at runtime
|
||||
target_model, name = self.resolve(args)
|
||||
self.validate(target_model, args)
|
||||
return self.create_relation(target_model, name)
|
||||
|
||||
|
||||
class SourceResolver(dbt.context.common.BaseResolver):
|
||||
def resolve(self, source_name, table_name):
|
||||
target_source = ParserUtils.resolve_source(
|
||||
self.manifest,
|
||||
source_name,
|
||||
table_name,
|
||||
self.current_project,
|
||||
self.model.package_name
|
||||
)
|
||||
|
||||
if target_source is None:
|
||||
dbt.exceptions.source_target_not_found(
|
||||
self.model,
|
||||
source_name,
|
||||
table_name)
|
||||
return target_source
|
||||
|
||||
def __call__(self, source_name, table_name):
|
||||
"""When you call source(), this is what happens at runtime"""
|
||||
target_source = self.resolve(source_name, table_name)
|
||||
return self.Relation.create_from_source(target_source)
|
||||
|
||||
|
||||
_MISSING = object()
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, model, source_config=None):
|
||||
self.model = model
|
||||
# we never use or get a source config, only the parser cares
|
||||
|
||||
def __call__(*args, **kwargs):
|
||||
return ''
|
||||
|
||||
def set(self, name, value):
|
||||
return self.__call__({name: value})
|
||||
|
||||
def _validate(self, validator, value):
|
||||
validator(value)
|
||||
|
||||
def _lookup(self, name, default=_MISSING):
|
||||
config = self.model.config
|
||||
|
||||
if hasattr(config, name):
|
||||
return getattr(config, name)
|
||||
elif name in config.extra:
|
||||
return config.extra[name]
|
||||
elif default is not _MISSING:
|
||||
return default
|
||||
else:
|
||||
dbt.exceptions.missing_config(self.model, name)
|
||||
|
||||
def require(self, name, validator=None):
|
||||
to_return = self._lookup(name)
|
||||
|
||||
if validator is not None:
|
||||
self._validate(validator, to_return)
|
||||
|
||||
return to_return
|
||||
|
||||
def get(self, name, validator=None, default=None):
|
||||
to_return = self._lookup(name, default)
|
||||
|
||||
if validator is not None and default is not None:
|
||||
self._validate(validator, to_return)
|
||||
|
||||
return to_return
|
||||
|
||||
|
||||
class DatabaseWrapper(dbt.context.common.BaseDatabaseWrapper):
|
||||
"""The runtime database wrapper exposes everything the adapter marks
|
||||
available.
|
||||
"""
|
||||
def __getattr__(self, name):
|
||||
if name in self.adapter._available_:
|
||||
return getattr(self.adapter, name)
|
||||
else:
|
||||
raise AttributeError(
|
||||
"'{}' object has no attribute '{}'".format(
|
||||
self.__class__.__name__, name
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Var(dbt.context.base.Var):
|
||||
pass
|
||||
|
||||
|
||||
class Provider(dbt.context.common.Provider):
|
||||
execute = True
|
||||
Config = Config
|
||||
DatabaseWrapper = DatabaseWrapper
|
||||
Var = Var
|
||||
ref = RefResolver
|
||||
source = SourceResolver
|
||||
|
||||
|
||||
def generate(model, runtime_config, manifest):
|
||||
return dbt.context.common.generate(
|
||||
model, runtime_config, manifest, Provider(), None
|
||||
)
|
||||
40
core/dbt/context/secret.py
Normal file
40
core/dbt/context/secret.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import os
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from .base import BaseContext, contextmember
|
||||
|
||||
from dbt.exceptions import raise_parsing_error
|
||||
|
||||
|
||||
class SecretContext(BaseContext):
|
||||
"""This context is used in profiles.yml + packages.yml. It can render secret
|
||||
env vars that aren't usable elsewhere"""
|
||||
|
||||
@contextmember
|
||||
def env_var(self, var: str, default: Optional[str] = None) -> str:
|
||||
"""The env_var() function. Return the environment variable named 'var'.
|
||||
If there is no such environment variable set, return the default.
|
||||
|
||||
If the default is None, raise an exception for an undefined variable.
|
||||
|
||||
In this context *only*, env_var will return the actual values of
|
||||
env vars prefixed with DBT_ENV_SECRET_
|
||||
"""
|
||||
return_value = None
|
||||
if var in os.environ:
|
||||
return_value = os.environ[var]
|
||||
elif default is not None:
|
||||
return_value = default
|
||||
|
||||
if return_value is not None:
|
||||
self.env_vars[var] = return_value
|
||||
return return_value
|
||||
else:
|
||||
msg = f"Env var required but not provided: '{var}'"
|
||||
raise_parsing_error(msg)
|
||||
|
||||
|
||||
def generate_secret_context(cli_vars: Dict[str, Any]) -> Dict[str, Any]:
|
||||
ctx = SecretContext(cli_vars)
|
||||
# This is not a Mashumaro to_dict call
|
||||
return ctx.to_dict()
|
||||
85
core/dbt/context/target.py
Normal file
85
core/dbt/context/target.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from dbt.contracts.connection import HasCredentials
|
||||
|
||||
from dbt.context.base import (
|
||||
BaseContext, contextproperty
|
||||
)
|
||||
|
||||
|
||||
class TargetContext(BaseContext):
|
||||
# subclass is ConfiguredContext
|
||||
def __init__(self, config: HasCredentials, cli_vars: Dict[str, Any]):
|
||||
super().__init__(cli_vars=cli_vars)
|
||||
self.config = config
|
||||
|
||||
@contextproperty
|
||||
def target(self) -> Dict[str, Any]:
|
||||
"""`target` contains information about your connection to the warehouse
|
||||
(specified in profiles.yml). Some configs are shared between all
|
||||
adapters, while others are adapter-specific.
|
||||
|
||||
Common:
|
||||
|
||||
|----------|-----------|------------------------------------------|
|
||||
| Variable | Example | Description |
|
||||
|----------|-----------|------------------------------------------|
|
||||
| name | dev | Name of the active target |
|
||||
|----------|-----------|------------------------------------------|
|
||||
| schema | dbt_alice | Name of the dbt schema (or, dataset on |
|
||||
| | | BigQuery) |
|
||||
|----------|-----------|------------------------------------------|
|
||||
| type | postgres | The active adapter being used. |
|
||||
|----------|-----------|------------------------------------------|
|
||||
| threads | 4 | The number of threads in use by dbt |
|
||||
|----------|-----------|------------------------------------------|
|
||||
|
||||
Snowflake:
|
||||
|
||||
|----------|-----------|------------------------------------------|
|
||||
| Variable | Example | Description |
|
||||
|----------|-----------|------------------------------------------|
|
||||
| database | RAW | The active target's database. |
|
||||
|----------|-----------|------------------------------------------|
|
||||
| warehouse| TRANSFORM | The active target's warehouse. |
|
||||
|----------|-----------|------------------------------------------|
|
||||
| user | USERNAME | The active target's user |
|
||||
|----------|-----------|------------------------------------------|
|
||||
| role | ROLENAME | The active target's role |
|
||||
|----------|-----------|------------------------------------------|
|
||||
| account | abc123 | The active target's account |
|
||||
|----------|-----------|------------------------------------------|
|
||||
|
||||
Postgres/Redshift:
|
||||
|
||||
|----------|-------------------|----------------------------------|
|
||||
| Variable | Example | Description |
|
||||
|----------|-------------------|----------------------------------|
|
||||
| dbname | analytics | The active target's database. |
|
||||
|----------|-------------------|----------------------------------|
|
||||
| host | abc123.us-west-2. | The active target's host. |
|
||||
| | redshift.amazonaws| |
|
||||
| | .com | |
|
||||
|----------|-------------------|----------------------------------|
|
||||
| user | dbt_user | The active target's user |
|
||||
|----------|-------------------|----------------------------------|
|
||||
| port | 5439 | The active target's port |
|
||||
|----------|-------------------|----------------------------------|
|
||||
|
||||
BigQuery:
|
||||
|
||||
|----------|-----------|------------------------------------------|
|
||||
| Variable | Example | Description |
|
||||
|----------|-----------|------------------------------------------|
|
||||
| project | abc-123 | The active target's project. |
|
||||
|----------|-----------|------------------------------------------|
|
||||
|
||||
"""
|
||||
return self.config.to_target_dict()
|
||||
|
||||
|
||||
def generate_target_context(
|
||||
config: HasCredentials, cli_vars: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
ctx = TargetContext(config, cli_vars)
|
||||
return ctx.to_dict()
|
||||
1
core/dbt/contracts/README.md
Normal file
1
core/dbt/contracts/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Contracts README
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
|
||||
def named_property(name, doc=None):
|
||||
def get_prop(self):
|
||||
return self._contents.get(name)
|
||||
|
||||
def set_prop(self, value):
|
||||
self._contents[name] = value
|
||||
self.validate()
|
||||
|
||||
return property(get_prop, set_prop, doc=doc)
|
||||
@@ -1,25 +1,40 @@
|
||||
import abc
|
||||
import itertools
|
||||
import hashlib
|
||||
from dataclasses import dataclass, field
|
||||
from typing import (
|
||||
Any, ClassVar, Dict, Tuple, Iterable, Optional, NewType, List, Type
|
||||
Any, ClassVar, Dict, Tuple, Iterable, Optional, List, Callable,
|
||||
)
|
||||
from typing_extensions import Protocol
|
||||
|
||||
from hologram import JsonSchemaMixin
|
||||
from hologram.helpers import (
|
||||
StrEnum, register_pattern, ExtensibleJsonSchemaMixin
|
||||
)
|
||||
|
||||
from dbt.contracts.util import Replaceable
|
||||
from dbt.exceptions import InternalException
|
||||
from dbt.utils import translate_aliases
|
||||
from dbt.events.functions import fire_event
|
||||
from dbt.events.types import NewConnectionOpening
|
||||
from typing_extensions import Protocol
|
||||
from dbt.dataclass_schema import (
|
||||
dbtClassMixin, StrEnum, ExtensibleDbtClassMixin, HyphenatedDbtClassMixin,
|
||||
ValidatedStringMixin, register_pattern
|
||||
)
|
||||
from dbt.contracts.util import Replaceable
|
||||
|
||||
|
||||
Identifier = NewType('Identifier', str)
|
||||
class Identifier(ValidatedStringMixin):
|
||||
ValidationRegex = r'^[A-Za-z_][A-Za-z0-9_]+$'
|
||||
|
||||
|
||||
# we need register_pattern for jsonschema validation
|
||||
register_pattern(Identifier, r'^[A-Za-z_][A-Za-z0-9_]+$')
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdapterResponse(dbtClassMixin):
|
||||
_message: str
|
||||
code: Optional[str] = None
|
||||
rows_affected: Optional[int] = None
|
||||
|
||||
def __str__(self):
|
||||
return self._message
|
||||
|
||||
|
||||
class ConnectionState(StrEnum):
|
||||
INIT = 'init'
|
||||
OPEN = 'open'
|
||||
@@ -27,38 +42,20 @@ class ConnectionState(StrEnum):
|
||||
FAIL = 'fail'
|
||||
|
||||
|
||||
class ConnectionOpenerProtocol(Protocol):
|
||||
@classmethod
|
||||
def open(cls, connection: 'Connection') -> Any:
|
||||
raise NotImplementedError(f'open() not implemented for {cls.__name__}')
|
||||
|
||||
|
||||
class LazyHandle:
|
||||
"""Opener must be a callable that takes a Connection object and opens the
|
||||
connection, updating the handle on the Connection.
|
||||
"""
|
||||
def __init__(self, opener: Type[ConnectionOpenerProtocol]):
|
||||
self.opener = opener
|
||||
|
||||
def resolve(self, connection: 'Connection') -> Any:
|
||||
return self.opener.open(connection)
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Connection(ExtensibleJsonSchemaMixin, Replaceable):
|
||||
class Connection(ExtensibleDbtClassMixin, Replaceable):
|
||||
type: Identifier
|
||||
name: Optional[str]
|
||||
name: Optional[str] = None
|
||||
state: ConnectionState = ConnectionState.INIT
|
||||
transaction_open: bool = False
|
||||
# prevent serialization
|
||||
_handle: Optional[Any] = None
|
||||
_credentials: JsonSchemaMixin = field(init=False)
|
||||
_credentials: Optional[Any] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
type: Identifier,
|
||||
name: Optional[str],
|
||||
credentials: JsonSchemaMixin,
|
||||
credentials: dbtClassMixin,
|
||||
state: ConnectionState = ConnectionState.INIT,
|
||||
transaction_open: bool = False,
|
||||
handle: Optional[Any] = None,
|
||||
@@ -96,13 +93,26 @@ class Connection(ExtensibleJsonSchemaMixin, Replaceable):
|
||||
self._handle = value
|
||||
|
||||
|
||||
class LazyHandle:
|
||||
"""Opener must be a callable that takes a Connection object and opens the
|
||||
connection, updating the handle on the Connection.
|
||||
"""
|
||||
|
||||
def __init__(self, opener: Callable[[Connection], Connection]):
|
||||
self.opener = opener
|
||||
|
||||
def resolve(self, connection: Connection) -> Connection:
|
||||
fire_event(NewConnectionOpening(connection_state=connection.state))
|
||||
return self.opener(connection)
|
||||
|
||||
|
||||
# see https://github.com/python/mypy/issues/4717#issuecomment-373932080
|
||||
# and https://github.com/python/mypy/issues/5374
|
||||
# for why we have type: ignore. Maybe someday dataclasses + abstract classes
|
||||
# will work.
|
||||
@dataclass # type: ignore
|
||||
class Credentials(
|
||||
ExtensibleJsonSchemaMixin,
|
||||
ExtensibleDbtClassMixin,
|
||||
Replaceable,
|
||||
metaclass=abc.ABCMeta
|
||||
):
|
||||
@@ -116,12 +126,25 @@ class Credentials(
|
||||
'type not implemented for base credentials class'
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_field(self) -> str:
|
||||
"""Hashed and included in anonymous telemetry to track adapter adoption.
|
||||
Return the field from Credentials that can uniquely identify
|
||||
one team/organization using this adapter
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
'unique_field not implemented for base credentials class'
|
||||
)
|
||||
|
||||
def hashed_unique_field(self) -> str:
|
||||
return hashlib.md5(self.unique_field.encode('utf-8')).hexdigest()
|
||||
|
||||
def connection_info(
|
||||
self, *, with_aliases: bool = False
|
||||
) -> Iterable[Tuple[str, Any]]:
|
||||
"""Return an ordered iterator of key/value pairs for pretty-printing.
|
||||
"""
|
||||
as_dict = self.to_dict(omit_none=False, with_aliases=with_aliases)
|
||||
as_dict = self.to_dict(omit_none=False)
|
||||
connection_keys = set(self._connection_keys())
|
||||
aliases: List[str] = []
|
||||
if with_aliases:
|
||||
@@ -137,28 +160,75 @@ class Credentials(
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
def __pre_deserialize__(cls, data):
|
||||
data = super().__pre_deserialize__(data)
|
||||
data = cls.translate_aliases(data)
|
||||
return super().from_dict(data)
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def translate_aliases(cls, kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return translate_aliases(kwargs, cls._ALIASES)
|
||||
def translate_aliases(
|
||||
cls, kwargs: Dict[str, Any], recurse: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
return translate_aliases(kwargs, cls._ALIASES, recurse)
|
||||
|
||||
def to_dict(self, omit_none=True, validate=False, *, with_aliases=False):
|
||||
serialized = super().to_dict(omit_none=omit_none, validate=validate)
|
||||
if with_aliases:
|
||||
serialized.update({
|
||||
new_name: serialized[canonical_name]
|
||||
def __post_serialize__(self, dct):
|
||||
# no super() -- do we need it?
|
||||
if self._ALIASES:
|
||||
dct.update({
|
||||
new_name: dct[canonical_name]
|
||||
for new_name, canonical_name in self._ALIASES.items()
|
||||
if canonical_name in serialized
|
||||
if canonical_name in dct
|
||||
})
|
||||
return serialized
|
||||
return dct
|
||||
|
||||
|
||||
class UserConfigContract(Protocol):
|
||||
send_anonymous_usage_stats: bool
|
||||
use_colors: Optional[bool] = None
|
||||
partial_parse: Optional[bool] = None
|
||||
printer_width: Optional[int] = None
|
||||
|
||||
|
||||
class HasCredentials(Protocol):
|
||||
credentials: Credentials
|
||||
profile_name: str
|
||||
user_config: UserConfigContract
|
||||
target_name: str
|
||||
threads: int
|
||||
|
||||
def to_target_dict(self):
|
||||
raise NotImplementedError('to_target_dict not implemented')
|
||||
|
||||
|
||||
class AdapterRequiredConfig(HasCredentials):
|
||||
query_comment: Optional[str]
|
||||
DEFAULT_QUERY_COMMENT = '''
|
||||
{%- set comment_dict = {} -%}
|
||||
{%- do comment_dict.update(
|
||||
app='dbt',
|
||||
dbt_version=dbt_version,
|
||||
profile_name=target.get('profile_name'),
|
||||
target_name=target.get('target_name'),
|
||||
) -%}
|
||||
{%- if node is not none -%}
|
||||
{%- do comment_dict.update(
|
||||
node_id=node.unique_id,
|
||||
) -%}
|
||||
{% else %}
|
||||
{# in the node context, the connection name is the node_id #}
|
||||
{%- do comment_dict.update(connection_name=connection_name) -%}
|
||||
{%- endif -%}
|
||||
{{ return(tojson(comment_dict)) }}
|
||||
'''
|
||||
|
||||
|
||||
@dataclass
|
||||
class QueryComment(HyphenatedDbtClassMixin):
|
||||
comment: str = DEFAULT_QUERY_COMMENT
|
||||
append: bool = False
|
||||
job_label: bool = False
|
||||
|
||||
|
||||
class AdapterRequiredConfig(HasCredentials, Protocol):
|
||||
project_name: str
|
||||
query_comment: QueryComment
|
||||
cli_vars: Dict[str, Any]
|
||||
target_path: str
|
||||
|
||||
322
core/dbt/contracts/files.py
Normal file
322
core/dbt/contracts/files.py
Normal file
@@ -0,0 +1,322 @@
|
||||
import hashlib
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from mashumaro.types import SerializableType
|
||||
from typing import List, Optional, Union, Dict, Any
|
||||
|
||||
from dbt.dataclass_schema import dbtClassMixin, StrEnum
|
||||
|
||||
from .util import SourceKey
|
||||
|
||||
|
||||
MAXIMUM_SEED_SIZE = 1 * 1024 * 1024
|
||||
MAXIMUM_SEED_SIZE_NAME = '1MB'
|
||||
|
||||
|
||||
class ParseFileType(StrEnum):
|
||||
Macro = 'macro'
|
||||
Model = 'model'
|
||||
Snapshot = 'snapshot'
|
||||
Analysis = 'analysis'
|
||||
SingularTest = 'singular_test'
|
||||
GenericTest = 'generic_test'
|
||||
Seed = 'seed'
|
||||
Documentation = 'docs'
|
||||
Schema = 'schema'
|
||||
Hook = 'hook' # not a real filetype, from dbt_project.yml
|
||||
|
||||
|
||||
parse_file_type_to_parser = {
|
||||
ParseFileType.Macro: 'MacroParser',
|
||||
ParseFileType.Model: 'ModelParser',
|
||||
ParseFileType.Snapshot: 'SnapshotParser',
|
||||
ParseFileType.Analysis: 'AnalysisParser',
|
||||
ParseFileType.SingularTest: 'SingularTestParser',
|
||||
ParseFileType.GenericTest: 'GenericTestParser',
|
||||
ParseFileType.Seed: 'SeedParser',
|
||||
ParseFileType.Documentation: 'DocumentationParser',
|
||||
ParseFileType.Schema: 'SchemaParser',
|
||||
ParseFileType.Hook: 'HookParser',
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class FilePath(dbtClassMixin):
|
||||
searched_path: str
|
||||
relative_path: str
|
||||
modification_time: float
|
||||
project_root: str
|
||||
|
||||
@property
|
||||
def search_key(self) -> str:
|
||||
# TODO: should this be project name + path relative to project root?
|
||||
return self.absolute_path
|
||||
|
||||
@property
|
||||
def full_path(self) -> str:
|
||||
# useful for symlink preservation
|
||||
return os.path.join(
|
||||
self.project_root, self.searched_path, self.relative_path
|
||||
)
|
||||
|
||||
@property
|
||||
def absolute_path(self) -> str:
|
||||
return os.path.abspath(self.full_path)
|
||||
|
||||
@property
|
||||
def original_file_path(self) -> str:
|
||||
# this is mostly used for reporting errors. It doesn't show the project
|
||||
# name, should it?
|
||||
return os.path.join(
|
||||
self.searched_path, self.relative_path
|
||||
)
|
||||
|
||||
def seed_too_large(self) -> bool:
|
||||
"""Return whether the file this represents is over the seed size limit
|
||||
"""
|
||||
return os.stat(self.full_path).st_size > MAXIMUM_SEED_SIZE
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileHash(dbtClassMixin):
|
||||
name: str # the hash type name
|
||||
checksum: str # the hashlib.hash_type().hexdigest() of the file contents
|
||||
|
||||
@classmethod
|
||||
def empty(cls):
|
||||
return FileHash(name='none', checksum='')
|
||||
|
||||
@classmethod
|
||||
def path(cls, path: str):
|
||||
return FileHash(name='path', checksum=path)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, FileHash):
|
||||
return NotImplemented
|
||||
|
||||
if self.name == 'none' or self.name != other.name:
|
||||
return False
|
||||
|
||||
return self.checksum == other.checksum
|
||||
|
||||
def compare(self, contents: str) -> bool:
|
||||
"""Compare the file contents with the given hash"""
|
||||
if self.name == 'none':
|
||||
return False
|
||||
|
||||
return self.from_contents(contents, name=self.name) == self.checksum
|
||||
|
||||
@classmethod
|
||||
def from_contents(cls, contents: str, name='sha256') -> 'FileHash':
|
||||
"""Create a file hash from the given file contents. The hash is always
|
||||
the utf-8 encoding of the contents given, because dbt only reads files
|
||||
as utf-8.
|
||||
"""
|
||||
data = contents.encode('utf-8')
|
||||
checksum = hashlib.new(name, data).hexdigest()
|
||||
return cls(name=name, checksum=checksum)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RemoteFile(dbtClassMixin):
|
||||
@property
|
||||
def searched_path(self) -> str:
|
||||
return 'from remote system'
|
||||
|
||||
@property
|
||||
def relative_path(self) -> str:
|
||||
return 'from remote system'
|
||||
|
||||
@property
|
||||
def absolute_path(self) -> str:
|
||||
return 'from remote system'
|
||||
|
||||
@property
|
||||
def original_file_path(self):
|
||||
return 'from remote system'
|
||||
|
||||
@property
|
||||
def modification_time(self):
|
||||
return 'from remote system'
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseSourceFile(dbtClassMixin, SerializableType):
|
||||
"""Define a source file in dbt"""
|
||||
path: Union[FilePath, RemoteFile] # the path information
|
||||
checksum: FileHash
|
||||
# Seems like knowing which project the file came from would be useful
|
||||
project_name: Optional[str] = None
|
||||
# Parse file type: i.e. which parser will process this file
|
||||
parse_file_type: Optional[ParseFileType] = None
|
||||
# we don't want to serialize this
|
||||
contents: Optional[str] = None
|
||||
# the unique IDs contained in this file
|
||||
|
||||
@property
|
||||
def file_id(self):
|
||||
if isinstance(self.path, RemoteFile):
|
||||
return None
|
||||
return f'{self.project_name}://{self.path.original_file_path}'
|
||||
|
||||
def _serialize(self):
|
||||
dct = self.to_dict()
|
||||
return dct
|
||||
|
||||
@classmethod
|
||||
def _deserialize(cls, dct: Dict[str, int]):
|
||||
if dct['parse_file_type'] == 'schema':
|
||||
sf = SchemaSourceFile.from_dict(dct)
|
||||
else:
|
||||
sf = SourceFile.from_dict(dct)
|
||||
return sf
|
||||
|
||||
def __post_serialize__(self, dct):
|
||||
dct = super().__post_serialize__(dct)
|
||||
# remove empty lists to save space
|
||||
dct_keys = list(dct.keys())
|
||||
for key in dct_keys:
|
||||
if isinstance(dct[key], list) and not dct[key]:
|
||||
del dct[key]
|
||||
# remove contents. Schema files will still have 'dict_from_yaml'
|
||||
# from the contents
|
||||
if 'contents' in dct:
|
||||
del dct['contents']
|
||||
return dct
|
||||
|
||||
|
||||
@dataclass
|
||||
class SourceFile(BaseSourceFile):
|
||||
nodes: List[str] = field(default_factory=list)
|
||||
docs: List[str] = field(default_factory=list)
|
||||
macros: List[str] = field(default_factory=list)
|
||||
env_vars: List[str] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def big_seed(cls, path: FilePath) -> 'SourceFile':
|
||||
"""Parse seeds over the size limit with just the path"""
|
||||
self = cls(path=path, checksum=FileHash.path(path.original_file_path))
|
||||
self.contents = ''
|
||||
return self
|
||||
|
||||
def add_node(self, value):
|
||||
if value not in self.nodes:
|
||||
self.nodes.append(value)
|
||||
|
||||
# TODO: do this a different way. This remote file kludge isn't going
|
||||
# to work long term
|
||||
@classmethod
|
||||
def remote(cls, contents: str, project_name: str) -> 'SourceFile':
|
||||
self = cls(
|
||||
path=RemoteFile(),
|
||||
checksum=FileHash.from_contents(contents),
|
||||
project_name=project_name,
|
||||
contents=contents,
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
@dataclass
|
||||
class SchemaSourceFile(BaseSourceFile):
|
||||
dfy: Dict[str, Any] = field(default_factory=dict)
|
||||
# these are in the manifest.nodes dictionary
|
||||
tests: Dict[str, Any] = field(default_factory=dict)
|
||||
sources: List[str] = field(default_factory=list)
|
||||
exposures: List[str] = field(default_factory=list)
|
||||
metrics: List[str] = field(default_factory=list)
|
||||
# node patches contain models, seeds, snapshots, analyses
|
||||
ndp: List[str] = field(default_factory=list)
|
||||
# any macro patches in this file by macro unique_id.
|
||||
mcp: Dict[str, str] = field(default_factory=dict)
|
||||
# any source patches in this file. The entries are package, name pairs
|
||||
# Patches are only against external sources. Sources can be
|
||||
# created too, but those are in 'sources'
|
||||
sop: List[SourceKey] = field(default_factory=list)
|
||||
env_vars: Dict[str, Any] = field(default_factory=dict)
|
||||
pp_dict: Optional[Dict[str, Any]] = None
|
||||
pp_test_index: Optional[Dict[str, Any]] = None
|
||||
|
||||
@property
|
||||
def dict_from_yaml(self):
|
||||
return self.dfy
|
||||
|
||||
@property
|
||||
def node_patches(self):
|
||||
return self.ndp
|
||||
|
||||
@property
|
||||
def macro_patches(self):
|
||||
return self.mcp
|
||||
|
||||
@property
|
||||
def source_patches(self):
|
||||
return self.sop
|
||||
|
||||
def __post_serialize__(self, dct):
|
||||
dct = super().__post_serialize__(dct)
|
||||
# Remove partial parsing specific data
|
||||
for key in ('pp_test_index', 'pp_dict'):
|
||||
if key in dct:
|
||||
del dct[key]
|
||||
return dct
|
||||
|
||||
def append_patch(self, yaml_key, unique_id):
|
||||
self.node_patches.append(unique_id)
|
||||
|
||||
def add_test(self, node_unique_id, test_from):
|
||||
name = test_from['name']
|
||||
key = test_from['key']
|
||||
if key not in self.tests:
|
||||
self.tests[key] = {}
|
||||
if name not in self.tests[key]:
|
||||
self.tests[key][name] = []
|
||||
self.tests[key][name].append(node_unique_id)
|
||||
|
||||
def remove_tests(self, yaml_key, name):
|
||||
if yaml_key in self.tests:
|
||||
if name in self.tests[yaml_key]:
|
||||
del self.tests[yaml_key][name]
|
||||
|
||||
def get_tests(self, yaml_key, name):
|
||||
if yaml_key in self.tests:
|
||||
if name in self.tests[yaml_key]:
|
||||
return self.tests[yaml_key][name]
|
||||
return []
|
||||
|
||||
def get_key_and_name_for_test(self, test_unique_id):
|
||||
yaml_key = None
|
||||
block_name = None
|
||||
for key in self.tests.keys():
|
||||
for name in self.tests[key]:
|
||||
for unique_id in self.tests[key][name]:
|
||||
if unique_id == test_unique_id:
|
||||
yaml_key = key
|
||||
block_name = name
|
||||
break
|
||||
return (yaml_key, block_name)
|
||||
|
||||
def get_all_test_ids(self):
|
||||
test_ids = []
|
||||
for key in self.tests.keys():
|
||||
for name in self.tests[key]:
|
||||
test_ids.extend(self.tests[key][name])
|
||||
return test_ids
|
||||
|
||||
def add_env_var(self, var, yaml_key, name):
|
||||
if yaml_key not in self.env_vars:
|
||||
self.env_vars[yaml_key] = {}
|
||||
if name not in self.env_vars[yaml_key]:
|
||||
self.env_vars[yaml_key][name] = []
|
||||
if var not in self.env_vars[yaml_key][name]:
|
||||
self.env_vars[yaml_key][name].append(var)
|
||||
|
||||
def delete_from_env_vars(self, yaml_key, name):
|
||||
# We delete all vars for this yaml_key/name because the
|
||||
# entry has been scheduled for reparsing.
|
||||
if yaml_key in self.env_vars and name in self.env_vars[yaml_key]:
|
||||
del self.env_vars[yaml_key][name]
|
||||
if not self.env_vars[yaml_key]:
|
||||
del self.env_vars[yaml_key]
|
||||
|
||||
|
||||
AnySourceFile = Union[SchemaSourceFile, SourceFile]
|
||||
@@ -1,60 +1,51 @@
|
||||
from dbt.contracts.graph.parsed import (
|
||||
HasTestMetadata,
|
||||
ParsedNode,
|
||||
ParsedAnalysisNode,
|
||||
ParsedModelNode,
|
||||
ParsedSingularTestNode,
|
||||
ParsedHookNode,
|
||||
ParsedModelNode,
|
||||
ParsedExposure,
|
||||
ParsedMetric,
|
||||
ParsedResource,
|
||||
ParsedRPCNode,
|
||||
ParsedSqlNode,
|
||||
ParsedGenericTestNode,
|
||||
ParsedSeedNode,
|
||||
ParsedSnapshotNode,
|
||||
ParsedSourceDefinition,
|
||||
ParsedTestNode,
|
||||
SeedConfig,
|
||||
TestConfig,
|
||||
TestMetadata,
|
||||
PARSED_TYPES,
|
||||
same_seeds,
|
||||
)
|
||||
from dbt.node_types import NodeType
|
||||
from dbt.contracts.util import Replaceable
|
||||
from dbt.exceptions import InternalException, RuntimeException
|
||||
|
||||
from hologram import JsonSchemaMixin
|
||||
from dbt.dataclass_schema import dbtClassMixin
|
||||
from dataclasses import dataclass, field
|
||||
import sqlparse # type: ignore
|
||||
from typing import Optional, List, Union, Dict, Type
|
||||
|
||||
|
||||
@dataclass
|
||||
class InjectedCTE(JsonSchemaMixin, Replaceable):
|
||||
class InjectedCTE(dbtClassMixin, Replaceable):
|
||||
id: str
|
||||
sql: str
|
||||
|
||||
# for some frustrating reason, we can't subclass from ParsedNode directly,
|
||||
# or typing.Union will flatten CompiledNode+ParsedNode into just ParsedNode.
|
||||
# TODO: understand that issue and come up with some way for these two to share
|
||||
# logic
|
||||
|
||||
@dataclass
|
||||
class CompiledNodeMixin(dbtClassMixin):
|
||||
# this is a special mixin class to provide a required argument. If a node
|
||||
# is missing a `compiled` flag entirely, it must not be a CompiledNode.
|
||||
compiled: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompiledNode(ParsedNode):
|
||||
compiled: bool = False
|
||||
class CompiledNode(ParsedNode, CompiledNodeMixin):
|
||||
compiled_sql: Optional[str] = None
|
||||
extra_ctes_injected: bool = False
|
||||
extra_ctes: List[InjectedCTE] = field(default_factory=list)
|
||||
injected_sql: Optional[str] = None
|
||||
wrapped_sql: Optional[str] = None
|
||||
|
||||
def prepend_ctes(self, prepended_ctes: List[InjectedCTE]):
|
||||
self.extra_ctes_injected = True
|
||||
self.extra_ctes = prepended_ctes
|
||||
if self.compiled_sql is None:
|
||||
raise RuntimeException(
|
||||
'Cannot prepend ctes to an unparsed node', self
|
||||
)
|
||||
self.injected_sql = _inject_ctes_into_sql(
|
||||
self.compiled_sql,
|
||||
prepended_ctes,
|
||||
)
|
||||
self.validate(self.to_dict())
|
||||
relation_name: Optional[str] = None
|
||||
_pre_injected_sql: Optional[str] = None
|
||||
|
||||
def set_cte(self, cte_id: str, sql: str):
|
||||
"""This is the equivalent of what self.extra_ctes[cte_id] = sql would
|
||||
@@ -67,6 +58,12 @@ class CompiledNode(ParsedNode):
|
||||
else:
|
||||
self.extra_ctes.append(InjectedCTE(id=cte_id, sql=sql))
|
||||
|
||||
def __post_serialize__(self, dct):
|
||||
dct = super().__post_serialize__(dct)
|
||||
if '_pre_injected_sql' in dct:
|
||||
del dct['_pre_injected_sql']
|
||||
return dct
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompiledAnalysisNode(CompiledNode):
|
||||
@@ -86,28 +83,31 @@ class CompiledModelNode(CompiledNode):
|
||||
resource_type: NodeType = field(metadata={'restrict': [NodeType.Model]})
|
||||
|
||||
|
||||
# TODO: rm?
|
||||
@dataclass
|
||||
class CompiledRPCNode(CompiledNode):
|
||||
resource_type: NodeType = field(metadata={'restrict': [NodeType.RPCCall]})
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompiledSqlNode(CompiledNode):
|
||||
resource_type: NodeType = field(metadata={'restrict': [NodeType.SqlOperation]})
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompiledSeedNode(CompiledNode):
|
||||
# keep this in sync with ParsedSeedNode!
|
||||
resource_type: NodeType = field(metadata={'restrict': [NodeType.Seed]})
|
||||
config: SeedConfig = field(default_factory=SeedConfig)
|
||||
seed_file_path: str = ''
|
||||
|
||||
def __post_init__(self):
|
||||
if self.seed_file_path == '':
|
||||
raise InternalException(
|
||||
'Seeds should always have a seed_file_path'
|
||||
)
|
||||
|
||||
@property
|
||||
def empty(self):
|
||||
""" Seeds are never empty"""
|
||||
return False
|
||||
|
||||
def same_body(self, other) -> bool:
|
||||
return same_seeds(self, other)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompiledSnapshotNode(CompiledNode):
|
||||
@@ -115,119 +115,128 @@ class CompiledSnapshotNode(CompiledNode):
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompiledTestNode(CompiledNode):
|
||||
class CompiledSingularTestNode(CompiledNode):
|
||||
resource_type: NodeType = field(metadata={'restrict': [NodeType.Test]})
|
||||
# Was not able to make mypy happy and keep the code working. We need to
|
||||
# refactor the various configs.
|
||||
config: TestConfig = field(default_factory=TestConfig) # type:ignore
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompiledGenericTestNode(CompiledNode, HasTestMetadata):
|
||||
# keep this in sync with ParsedGenericTestNode!
|
||||
resource_type: NodeType = field(metadata={'restrict': [NodeType.Test]})
|
||||
column_name: Optional[str] = None
|
||||
config: TestConfig = field(default_factory=TestConfig)
|
||||
test_metadata: Optional[TestMetadata] = None
|
||||
file_key_name: Optional[str] = None
|
||||
# Was not able to make mypy happy and keep the code working. We need to
|
||||
# refactor the various configs.
|
||||
config: TestConfig = field(default_factory=TestConfig) # type:ignore
|
||||
|
||||
def same_contents(self, other) -> bool:
|
||||
if other is None:
|
||||
return False
|
||||
|
||||
return (
|
||||
self.same_config(other) and
|
||||
self.same_fqn(other) and
|
||||
True
|
||||
)
|
||||
|
||||
|
||||
def _inject_ctes_into_sql(sql: str, ctes: List[InjectedCTE]) -> str:
|
||||
"""
|
||||
`ctes` is a list of InjectedCTEs like:
|
||||
|
||||
[
|
||||
InjectedCTE(
|
||||
id="cte_id_1",
|
||||
sql="__dbt__CTE__ephemeral as (select * from table)",
|
||||
),
|
||||
InjectedCTE(
|
||||
id="cte_id_2",
|
||||
sql="__dbt__CTE__events as (select id, type from events)",
|
||||
),
|
||||
]
|
||||
|
||||
Given `sql` like:
|
||||
|
||||
"with internal_cte as (select * from sessions)
|
||||
select * from internal_cte"
|
||||
|
||||
This will spit out:
|
||||
|
||||
"with __dbt__CTE__ephemeral as (select * from table),
|
||||
__dbt__CTE__events as (select id, type from events),
|
||||
with internal_cte as (select * from sessions)
|
||||
select * from internal_cte"
|
||||
|
||||
(Whitespace enhanced for readability.)
|
||||
"""
|
||||
if len(ctes) == 0:
|
||||
return sql
|
||||
|
||||
parsed_stmts = sqlparse.parse(sql)
|
||||
parsed = parsed_stmts[0]
|
||||
|
||||
with_stmt = None
|
||||
for token in parsed.tokens:
|
||||
if token.is_keyword and token.normalized == 'WITH':
|
||||
with_stmt = token
|
||||
break
|
||||
|
||||
if with_stmt is None:
|
||||
# no with stmt, add one, and inject CTEs right at the beginning
|
||||
first_token = parsed.token_first()
|
||||
with_stmt = sqlparse.sql.Token(sqlparse.tokens.Keyword, 'with')
|
||||
parsed.insert_before(first_token, with_stmt)
|
||||
else:
|
||||
# stmt exists, add a comma (which will come after injected CTEs)
|
||||
trailing_comma = sqlparse.sql.Token(sqlparse.tokens.Punctuation, ',')
|
||||
parsed.insert_after(with_stmt, trailing_comma)
|
||||
|
||||
token = sqlparse.sql.Token(
|
||||
sqlparse.tokens.Keyword,
|
||||
", ".join(c.sql for c in ctes)
|
||||
)
|
||||
parsed.insert_after(with_stmt, token)
|
||||
|
||||
return str(parsed)
|
||||
CompiledTestNode = Union[CompiledSingularTestNode, CompiledGenericTestNode]
|
||||
|
||||
|
||||
COMPILED_TYPES: Dict[NodeType, Type[CompiledNode]] = {
|
||||
NodeType.Analysis: CompiledAnalysisNode,
|
||||
NodeType.Model: CompiledModelNode,
|
||||
NodeType.Operation: CompiledHookNode,
|
||||
NodeType.RPCCall: CompiledRPCNode,
|
||||
NodeType.Seed: CompiledSeedNode,
|
||||
NodeType.Snapshot: CompiledSnapshotNode,
|
||||
NodeType.Test: CompiledTestNode,
|
||||
PARSED_TYPES: Dict[Type[CompiledNode], Type[ParsedResource]] = {
|
||||
CompiledAnalysisNode: ParsedAnalysisNode,
|
||||
CompiledModelNode: ParsedModelNode,
|
||||
CompiledHookNode: ParsedHookNode,
|
||||
CompiledRPCNode: ParsedRPCNode,
|
||||
CompiledSqlNode: ParsedSqlNode,
|
||||
CompiledSeedNode: ParsedSeedNode,
|
||||
CompiledSnapshotNode: ParsedSnapshotNode,
|
||||
CompiledSingularTestNode: ParsedSingularTestNode,
|
||||
CompiledGenericTestNode: ParsedGenericTestNode,
|
||||
}
|
||||
|
||||
|
||||
def compiled_type_for(parsed: ParsedNode):
|
||||
if parsed.resource_type in COMPILED_TYPES:
|
||||
return COMPILED_TYPES[parsed.resource_type]
|
||||
COMPILED_TYPES: Dict[Type[ParsedResource], Type[CompiledNode]] = {
|
||||
ParsedAnalysisNode: CompiledAnalysisNode,
|
||||
ParsedModelNode: CompiledModelNode,
|
||||
ParsedHookNode: CompiledHookNode,
|
||||
ParsedRPCNode: CompiledRPCNode,
|
||||
ParsedSqlNode: CompiledSqlNode,
|
||||
ParsedSeedNode: CompiledSeedNode,
|
||||
ParsedSnapshotNode: CompiledSnapshotNode,
|
||||
ParsedSingularTestNode: CompiledSingularTestNode,
|
||||
ParsedGenericTestNode: CompiledGenericTestNode,
|
||||
}
|
||||
|
||||
|
||||
# for some types, the compiled type is the parsed type, so make this easy
|
||||
CompiledType = Union[Type[CompiledNode], Type[ParsedResource]]
|
||||
CompiledResource = Union[ParsedResource, CompiledNode]
|
||||
|
||||
|
||||
def compiled_type_for(parsed: ParsedNode) -> CompiledType:
|
||||
if type(parsed) in COMPILED_TYPES:
|
||||
return COMPILED_TYPES[type(parsed)]
|
||||
else:
|
||||
return type(parsed)
|
||||
|
||||
|
||||
def parsed_instance_for(compiled: CompiledNode) -> ParsedNode:
|
||||
cls = PARSED_TYPES.get(compiled.resource_type)
|
||||
def parsed_instance_for(compiled: CompiledNode) -> ParsedResource:
|
||||
cls = PARSED_TYPES.get(type(compiled))
|
||||
if cls is None:
|
||||
# how???
|
||||
raise ValueError('invalid resource_type: {}'
|
||||
.format(compiled.resource_type))
|
||||
|
||||
# validate=False to allow extra keys from compiling
|
||||
return cls.from_dict(compiled.to_dict(), validate=False)
|
||||
return cls.from_dict(compiled.to_dict(omit_none=True))
|
||||
|
||||
|
||||
NonSourceCompiledNode = Union[
|
||||
CompiledAnalysisNode,
|
||||
CompiledSingularTestNode,
|
||||
CompiledModelNode,
|
||||
CompiledHookNode,
|
||||
CompiledRPCNode,
|
||||
CompiledSqlNode,
|
||||
CompiledGenericTestNode,
|
||||
CompiledSeedNode,
|
||||
CompiledSnapshotNode,
|
||||
]
|
||||
|
||||
NonSourceParsedNode = Union[
|
||||
ParsedAnalysisNode,
|
||||
ParsedSingularTestNode,
|
||||
ParsedHookNode,
|
||||
ParsedModelNode,
|
||||
ParsedRPCNode,
|
||||
ParsedSqlNode,
|
||||
ParsedGenericTestNode,
|
||||
ParsedSeedNode,
|
||||
ParsedSnapshotNode,
|
||||
]
|
||||
|
||||
|
||||
# This is anything that can be in manifest.nodes.
|
||||
ManifestNode = Union[
|
||||
NonSourceCompiledNode,
|
||||
NonSourceParsedNode,
|
||||
]
|
||||
|
||||
# We allow either parsed or compiled nodes, or parsed sources, as some
|
||||
# 'compile()' calls in the runner actually just return the original parsed
|
||||
# node they were given.
|
||||
CompileResultNode = Union[
|
||||
CompiledAnalysisNode,
|
||||
CompiledModelNode,
|
||||
CompiledHookNode,
|
||||
CompiledRPCNode,
|
||||
CompiledSeedNode,
|
||||
CompiledSnapshotNode,
|
||||
CompiledTestNode,
|
||||
ParsedAnalysisNode,
|
||||
ParsedModelNode,
|
||||
ParsedHookNode,
|
||||
ParsedRPCNode,
|
||||
ParsedSeedNode,
|
||||
ParsedSnapshotNode,
|
||||
ManifestNode,
|
||||
ParsedSourceDefinition,
|
||||
ParsedTestNode,
|
||||
]
|
||||
|
||||
# anything that participates in the graph: sources, exposures, metrics,
|
||||
# or manifest nodes
|
||||
GraphMemberNode = Union[
|
||||
CompileResultNode,
|
||||
ParsedExposure,
|
||||
ParsedMetric,
|
||||
]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user