mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-08 00:01:38 +00:00
Compare commits
3551 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65c2a7f3b8 | ||
|
|
74b18457d4 | ||
|
|
82ac454080 | ||
|
|
111471a5ad | ||
|
|
2bc1429ee2 | ||
|
|
25fdd76af0 | ||
|
|
ff3f31cf9e | ||
|
|
0b303ffc30 | ||
|
|
4c78c5fe9f | ||
|
|
17b97d2fb1 | ||
|
|
2193d34f76 | ||
|
|
b1c212e9f6 | ||
|
|
da58ae62a8 | ||
|
|
84d04141b3 | ||
|
|
f29c697455 | ||
|
|
a1238c6397 | ||
|
|
4dfdc3a717 | ||
|
|
7e97f68013 | ||
|
|
48ef2450fd | ||
|
|
ec769703d6 | ||
|
|
e2984ca10b | ||
|
|
7400bfab70 | ||
|
|
1870b25b0b | ||
|
|
8f7eeae4a7 | ||
|
|
f2e16c52cb | ||
|
|
759f0cfb69 | ||
|
|
e1941cb220 | ||
|
|
1af1f10974 | ||
|
|
0e3d82b718 | ||
|
|
74417bbe24 | ||
|
|
3ed60c95c2 | ||
|
|
f859d5eb26 | ||
|
|
f9848c78dc | ||
|
|
60a4014d09 | ||
|
|
2884904d19 | ||
|
|
6fe196bd97 | ||
|
|
e2bbb95591 | ||
|
|
8568b24db2 | ||
|
|
d8f10dc72a | ||
|
|
98ef879a34 | ||
|
|
95b3a97925 | ||
|
|
f24629602c | ||
|
|
e5125b6853 | ||
|
|
31e8ac11a2 | ||
|
|
a9c4c006b2 | ||
|
|
378c763aa3 | ||
|
|
e46c837416 | ||
|
|
a478d45258 | ||
|
|
5476c4d14f | ||
|
|
2759d59cbf | ||
|
|
d2213709f0 | ||
|
|
d2840b95d5 | ||
|
|
49cdad014b | ||
|
|
532271130c | ||
|
|
628ef4aeff | ||
|
|
34b344baf9 | ||
|
|
b2daeffa36 | ||
|
|
3440fa7415 | ||
|
|
77bf001195 | ||
|
|
5d0003ce93 | ||
|
|
a0c4be1d5c | ||
|
|
95be4e2d53 | ||
|
|
5f8705fc09 | ||
|
|
975833bf87 | ||
|
|
b4e3f25c1e | ||
|
|
bb198c8c1a | ||
|
|
40899de989 | ||
|
|
01a6781454 | ||
|
|
f448a75da4 | ||
|
|
8e5f5c9a6b | ||
|
|
04156061c4 | ||
|
|
36f96f75c7 | ||
|
|
197bcb3599 | ||
|
|
1474335a79 | ||
|
|
0f8c361dcd | ||
|
|
beb75dbc20 | ||
|
|
cbb95a47f8 | ||
|
|
d7e36bc5eb | ||
|
|
ef14359d9b | ||
|
|
b1444d7c04 | ||
|
|
c3c7d629f7 | ||
|
|
eb33b6521b | ||
|
|
932fe5db02 | ||
|
|
6885bcae92 | ||
|
|
d35e9fcae4 | ||
|
|
861279e614 | ||
|
|
b80dd15f4b | ||
|
|
ae95c8fdea | ||
|
|
090b5937ab | ||
|
|
2323e130b1 | ||
|
|
6ef127f283 | ||
|
|
266f00d3a8 | ||
|
|
5c61867e1f | ||
|
|
0bbe1cc958 | ||
|
|
d1e02e4695 | ||
|
|
f707c83e1a | ||
|
|
ae67319975 | ||
|
|
dffdd2e190 | ||
|
|
e3ba323764 | ||
|
|
2d7153e151 | ||
|
|
333ad533c1 | ||
|
|
f91ebfabcb | ||
|
|
cae58ffb96 | ||
|
|
caddda2b1c | ||
|
|
f63a04a123 | ||
|
|
fe13508f95 | ||
|
|
53e2b5b563 | ||
|
|
af42342e08 | ||
|
|
1da63a6be0 | ||
|
|
0448bfef28 | ||
|
|
6fc4726e34 | ||
|
|
a0363357ef | ||
|
|
134a7d3d83 | ||
|
|
79f83f34fd | ||
|
|
a34b6610d2 | ||
|
|
24f2efb833 | ||
|
|
d77d873a42 | ||
|
|
707d938656 | ||
|
|
ccaa76026c | ||
|
|
ac540b75a7 | ||
|
|
be573c8ae4 | ||
|
|
6076111d83 | ||
|
|
7c1ee239c7 | ||
|
|
d3a02f9d25 | ||
|
|
b8501195ad | ||
|
|
49192cb604 | ||
|
|
5e8bbd4ce4 | ||
|
|
5a85572a9c | ||
|
|
d2d48f772b | ||
|
|
25e7196a37 | ||
|
|
8a4fabb4c9 | ||
|
|
7825a66768 | ||
|
|
2b6891c12d | ||
|
|
70fb68d81b | ||
|
|
6b15628d81 | ||
|
|
7c88793e8f | ||
|
|
896f7f5d37 | ||
|
|
46d05e37d0 | ||
|
|
9dc87a060d | ||
|
|
3e638dd35e | ||
|
|
e4ad4fb26c | ||
|
|
cc63aee62c | ||
|
|
31806438a9 | ||
|
|
74ac351aa4 | ||
|
|
7e157dab42 | ||
|
|
8b2586ead2 | ||
|
|
ebcfd18457 | ||
|
|
cbb496e491 | ||
|
|
1ff93b1051 | ||
|
|
2fdb6156e7 | ||
|
|
f7c13a3063 | ||
|
|
c0470c35a9 | ||
|
|
304a897290 | ||
|
|
af2499a0ea | ||
|
|
52ccd735ca | ||
|
|
ffcb1c4ddf | ||
|
|
0dd74e226c | ||
|
|
bd5149d3f8 | ||
|
|
7c6649b24f | ||
|
|
cfd9447f39 | ||
|
|
820698d9d4 | ||
|
|
7645edee6b | ||
|
|
7e6f7b8bab | ||
|
|
ee77ae8319 | ||
|
|
0f2eba580d | ||
|
|
1cdf160b35 | ||
|
|
7e68c108c1 | ||
|
|
8ecbe67054 | ||
|
|
a6016825ff | ||
|
|
15221a1a20 | ||
|
|
6718938c1a | ||
|
|
acd1a9ad91 | ||
|
|
cce2894dac | ||
|
|
877ea0cc19 | ||
|
|
cd4c1fc7ac | ||
|
|
09292fd28b | ||
|
|
ccef86d2a0 | ||
|
|
ba34ef4494 | ||
|
|
26eca09bb9 | ||
|
|
64d4cc00e4 | ||
|
|
33b4de86a9 | ||
|
|
f33da85518 | ||
|
|
93ecbf479e | ||
|
|
ca2d6541ce | ||
|
|
db7154dca9 | ||
|
|
72bba3d948 | ||
|
|
07bbf4d4ea | ||
|
|
7aafbbe58e | ||
|
|
c2058211fe | ||
|
|
08a6cbb270 | ||
|
|
c5da8963d4 | ||
|
|
89b854ea57 | ||
|
|
42fd8cd1e8 | ||
|
|
0e0f49c540 | ||
|
|
f0b1b62e79 | ||
|
|
7606a40a58 | ||
|
|
ac5098522b | ||
|
|
d84ff8d392 | ||
|
|
4a0687cfe9 | ||
|
|
19e386ed21 | ||
|
|
8165c72606 | ||
|
|
5267992e31 | ||
|
|
1949b3346c | ||
|
|
38375b1710 | ||
|
|
281e790260 | ||
|
|
2b8b2521d1 | ||
|
|
52601edb35 | ||
|
|
5475f281eb | ||
|
|
b1f8893783 | ||
|
|
640cb322d7 | ||
|
|
c4f15cbf3a | ||
|
|
bef392cf7a | ||
|
|
abb49ed336 | ||
|
|
fe5bc5569d | ||
|
|
18615f246d | ||
|
|
7958632046 | ||
|
|
3e8bff23c4 | ||
|
|
0221781a05 | ||
|
|
e6ced7fff6 | ||
|
|
484238ece2 | ||
|
|
ee32bb87f0 | ||
|
|
73803acb89 | ||
|
|
a40df40f9b | ||
|
|
a33b89788c | ||
|
|
54f815e503 | ||
|
|
e54d3d274a | ||
|
|
b7a20ceb4f | ||
|
|
bbc965d38f | ||
|
|
8935cf7041 | ||
|
|
4583f6d996 | ||
|
|
92282fb493 | ||
|
|
65ea328f2a | ||
|
|
2da4833a0d | ||
|
|
631ce68126 | ||
|
|
480aca680d | ||
|
|
6e3ab6700d | ||
|
|
61319fa08e | ||
|
|
673e9f88ad | ||
|
|
f2b8200a3b | ||
|
|
0383624c72 | ||
|
|
cb03b8d9d4 | ||
|
|
e7e821ca3d | ||
|
|
6b21fdbcc6 | ||
|
|
ee326c6fe3 | ||
|
|
8945fd163c | ||
|
|
4dab0bd4bb | ||
|
|
49ec0c6df4 | ||
|
|
f3d29c47e2 | ||
|
|
41b4577665 | ||
|
|
2ca813f209 | ||
|
|
66734f07fa | ||
|
|
90844effa7 | ||
|
|
4765c6e186 | ||
|
|
d2f52fd7bf | ||
|
|
85c55c5aa8 | ||
|
|
6043411825 | ||
|
|
72ca010a5f | ||
|
|
e34f7164d8 | ||
|
|
ef975437a6 | ||
|
|
68863cd44b | ||
|
|
737deb8e39 | ||
|
|
67048fce86 | ||
|
|
97adde0f5e | ||
|
|
998bdfd40d | ||
|
|
05a04a0d23 | ||
|
|
6651ad20ef | ||
|
|
75cd22d645 | ||
|
|
00d0708d2d | ||
|
|
2d5550e09e | ||
|
|
11969364d3 | ||
|
|
b7c0a86b1f | ||
|
|
1eb1942085 | ||
|
|
7073d8b6b4 | ||
|
|
0e90d10f17 | ||
|
|
e989167267 | ||
|
|
49128d5559 | ||
|
|
d3c7af784f | ||
|
|
41dd34ec9e | ||
|
|
c565f9aa0f | ||
|
|
f40f631810 | ||
|
|
130ee7b371 | ||
|
|
4bbb97b749 | ||
|
|
3fb96aaab6 | ||
|
|
7d64ca2057 | ||
|
|
37f6a9694a | ||
|
|
77df7c5fea | ||
|
|
4fc08c78d3 | ||
|
|
c31461b9e7 | ||
|
|
1875eadbfe | ||
|
|
50ac49c713 | ||
|
|
def079d944 | ||
|
|
bc5c468581 | ||
|
|
020ddee777 | ||
|
|
3e7124946e | ||
|
|
395916bc86 | ||
|
|
e80ed0e700 | ||
|
|
8db7e32bd2 | ||
|
|
d263327997 | ||
|
|
93a6e4d835 | ||
|
|
9dab94bd7b | ||
|
|
d3a108ae9c | ||
|
|
3b39f90a0e | ||
|
|
e994c769a6 | ||
|
|
bbc974fb69 | ||
|
|
71bf658e17 | ||
|
|
8211d5df8c | ||
|
|
10e54e49a5 | ||
|
|
6b9ac2700e | ||
|
|
012616a285 | ||
|
|
2669b1bff6 | ||
|
|
2aeebd17a4 | ||
|
|
e43802e197 | ||
|
|
16b3d2b006 | ||
|
|
f777ec177c | ||
|
|
19304f95e8 | ||
|
|
5b49b8c957 | ||
|
|
f1e599d535 | ||
|
|
752b502399 | ||
|
|
8e3d562830 | ||
|
|
5b6c7a30d7 | ||
|
|
5b7e8b6e01 | ||
|
|
8bd30af109 | ||
|
|
828b196414 | ||
|
|
83117a1eca | ||
|
|
bb65265930 | ||
|
|
14ea845aa3 | ||
|
|
c1bb4bf7fa | ||
|
|
38dcdea6d5 | ||
|
|
bc222d2a91 | ||
|
|
c421904b78 | ||
|
|
f6d0f14b49 | ||
|
|
f4c6c7d6d8 | ||
|
|
cad4e7b59e | ||
|
|
e05dad2717 | ||
|
|
74bc4596ed | ||
|
|
cc3d79b3b9 | ||
|
|
4e0ae154a5 | ||
|
|
435388aa0b | ||
|
|
e66ff54c3c | ||
|
|
a7da52a485 | ||
|
|
ab65890455 | ||
|
|
f8dafa294d | ||
|
|
19ea4bbb9c | ||
|
|
53f40eb9eb | ||
|
|
793f230c30 | ||
|
|
6964f10aa3 | ||
|
|
3f6f6a191d | ||
|
|
9388b8497c | ||
|
|
28ca319632 | ||
|
|
d5ad1cb2fb | ||
|
|
c12ee6438c | ||
|
|
e18eb0931e | ||
|
|
2c0295d674 | ||
|
|
ced1e84567 | ||
|
|
5adc996f3e | ||
|
|
a3b3353e71 | ||
|
|
f084096658 | ||
|
|
d9188da059 | ||
|
|
12c150f64d | ||
|
|
6d25ada6a4 | ||
|
|
c0cd121a91 | ||
|
|
e8e508eb18 | ||
|
|
deda162375 | ||
|
|
b69601c5c2 | ||
|
|
87f9aa37b5 | ||
|
|
b74e2e1fd2 | ||
|
|
e40cbc32a6 | ||
|
|
636a49b1a6 | ||
|
|
7239f252da | ||
|
|
5d85ea63b0 | ||
|
|
cf50fcc78f | ||
|
|
eead1bd8b9 | ||
|
|
206a09aad3 | ||
|
|
eb365a3eb5 | ||
|
|
1690daccb5 | ||
|
|
233eeb0744 | ||
|
|
3f17525169 | ||
|
|
100df45d46 | ||
|
|
cb00e6de9f | ||
|
|
82585dc28a | ||
|
|
cb16747125 | ||
|
|
8632c98556 | ||
|
|
6c3a805a4d | ||
|
|
f6b949e4c1 | ||
|
|
1f9624ad5c | ||
|
|
9c9ddff973 | ||
|
|
f1636c7768 | ||
|
|
0ebefda760 | ||
|
|
5a335fb57b | ||
|
|
db95cbcfa4 | ||
|
|
33d9949283 | ||
|
|
41078d5ff6 | ||
|
|
52316774ad | ||
|
|
ce545ad062 | ||
|
|
2e5df12df1 | ||
|
|
46b9de642d | ||
|
|
a9d600234c | ||
|
|
5c8b69e8b7 | ||
|
|
29792677d7 | ||
|
|
7de9e986e0 | ||
|
|
2b711be6a4 | ||
|
|
16ef9e60d5 | ||
|
|
4d1a31c6bf | ||
|
|
5a5b98cccb | ||
|
|
f94afedfa8 | ||
|
|
c9996f4942 | ||
|
|
d32942a1d7 | ||
|
|
95d1e4ab7c | ||
|
|
dd4fb084e6 | ||
|
|
2c039c3730 | ||
|
|
0cef0aecb5 | ||
|
|
4ed9d7ac1f | ||
|
|
21b2093b55 | ||
|
|
d4ea2ec978 | ||
|
|
8cffa6e394 | ||
|
|
58337e0722 | ||
|
|
cedbae36b7 | ||
|
|
d5e9df41fb | ||
|
|
e7323f0b74 | ||
|
|
00d3600881 | ||
|
|
4c799aaf10 | ||
|
|
a8938f84f0 | ||
|
|
ab5aa02bf8 | ||
|
|
42d33786a1 | ||
|
|
683435cfb8 | ||
|
|
6b8170dd2f | ||
|
|
941fe97785 | ||
|
|
f87e013ec4 | ||
|
|
fc792bf454 | ||
|
|
b4b9bd2436 | ||
|
|
0e455a2e40 | ||
|
|
b384bdb503 | ||
|
|
10a6348ddd | ||
|
|
74be07a9ef | ||
|
|
5607d659fb | ||
|
|
da1ef497a1 | ||
|
|
ac4fef0e4b | ||
|
|
0bc44582af | ||
|
|
baf0c7863f | ||
|
|
b00077151b | ||
|
|
842e46d060 | ||
|
|
bad4866bf7 | ||
|
|
3f5d96e13b | ||
|
|
a0dc04e7b0 | ||
|
|
23b0b0f203 | ||
|
|
83d464d167 | ||
|
|
1ba9f73fbd | ||
|
|
0a21f2c959 | ||
|
|
62b7b5d84b | ||
|
|
7e12a8f0a7 | ||
|
|
d347c65fcb | ||
|
|
51f109ffa7 | ||
|
|
a5e7d6ff6c | ||
|
|
2260e7df50 | ||
|
|
08fc3bdb6f | ||
|
|
0754a9b176 | ||
|
|
448d58f9ba | ||
|
|
bdc330405e | ||
|
|
abe1d5381d | ||
|
|
be0eff7e14 | ||
|
|
f88a125966 | ||
|
|
623ff1fae9 | ||
|
|
63d7b5568b | ||
|
|
7c8a87673a | ||
|
|
a3b814f758 | ||
|
|
1989d72f4f | ||
|
|
63b1ca7e30 | ||
|
|
a328ac8ea9 | ||
|
|
2188bfa704 | ||
|
|
0f5adbe211 | ||
|
|
d0251182de | ||
|
|
a04345fb10 | ||
|
|
80440255ab | ||
|
|
7b3cc6d819 | ||
|
|
76d3ead61b | ||
|
|
21ca008a47 | ||
|
|
96aa4f3bd2 | ||
|
|
883979f5f5 | ||
|
|
b03a43777d | ||
|
|
a0e4be4b50 | ||
|
|
115caf2486 | ||
|
|
d5255615fd | ||
|
|
d949ca2cad | ||
|
|
4b0533ff0e | ||
|
|
d1e87df2df | ||
|
|
dc8f6c3b5e | ||
|
|
70640c1ddb | ||
|
|
a72b81f99e | ||
|
|
89a7c86840 | ||
|
|
a086c1c2a8 | ||
|
|
be3c6f10c7 | ||
|
|
1c9f65f7ca | ||
|
|
b023ecf8ce | ||
|
|
0a0cb2a18b | ||
|
|
a02afe76fc | ||
|
|
0b24beca30 | ||
|
|
7dfa236bc1 | ||
|
|
b7400b9010 | ||
|
|
50c5986c3e | ||
|
|
fff892300b | ||
|
|
adbee45073 | ||
|
|
2d091c8ca0 | ||
|
|
933fc19379 | ||
|
|
2bb2684165 | ||
|
|
868e5e1ab6 | ||
|
|
d537067908 | ||
|
|
a9b8a14d8e | ||
|
|
f3d654fc76 | ||
|
|
62a01bbcfd | ||
|
|
e60e1e939b | ||
|
|
5305f1bda0 | ||
|
|
6126e6ac67 | ||
|
|
58e2f74700 | ||
|
|
dcaf892e95 | ||
|
|
e8b2dee02d | ||
|
|
267d63fcd6 | ||
|
|
566a0c0345 | ||
|
|
6ed3f8ebfc | ||
|
|
51c7a542e3 | ||
|
|
ee68669652 | ||
|
|
e7e653d395 | ||
|
|
e6a4eb7fd9 | ||
|
|
25cdf2e544 | ||
|
|
5e1702018c | ||
|
|
a404af0582 | ||
|
|
741b69ff2d | ||
|
|
da1f808c06 | ||
|
|
39a8bf9485 | ||
|
|
53d1ce5ddb | ||
|
|
432ef09129 | ||
|
|
647da9f980 | ||
|
|
cc50ae28cd | ||
|
|
64ed92692a | ||
|
|
2dd810ba69 | ||
|
|
5922d939e2 | ||
|
|
14eaa9f32f | ||
|
|
f935a6bdfc | ||
|
|
29ceda7f43 | ||
|
|
f950c863f4 | ||
|
|
90f9931dca | ||
|
|
4c357d5281 | ||
|
|
0abf2ceeca | ||
|
|
3088570449 | ||
|
|
800943c401 | ||
|
|
3bedb57443 | ||
|
|
668ded91e2 | ||
|
|
293e1c1d9a | ||
|
|
7596215bbe | ||
|
|
7c6bbaf107 | ||
|
|
5271368776 | ||
|
|
430a30e2d2 | ||
|
|
b0eae53f80 | ||
|
|
dd03bafaec | ||
|
|
ded3ea104b | ||
|
|
0d9c6a77b6 | ||
|
|
ae46ae8738 | ||
|
|
06a4c7a8c9 | ||
|
|
f89f730957 | ||
|
|
80fc40a9c7 | ||
|
|
2bb0088ade | ||
|
|
d113b3ec8e | ||
|
|
97e13b92be | ||
|
|
dc832b8c7f | ||
|
|
56c33fec87 | ||
|
|
48862997b0 | ||
|
|
59dd01f1e8 | ||
|
|
d639e8e728 | ||
|
|
1c0e6ebf9c | ||
|
|
c289fb08f1 | ||
|
|
a64d6f1215 | ||
|
|
b0f05c2dea | ||
|
|
46d4e78b79 | ||
|
|
0562639715 | ||
|
|
51de288f27 | ||
|
|
7cfb16c1f5 | ||
|
|
f0cddbe7c2 | ||
|
|
06840c2608 | ||
|
|
87db64897d | ||
|
|
683d47175b | ||
|
|
fac8c11798 | ||
|
|
b5f5291e14 | ||
|
|
194b93a7ee | ||
|
|
55099e6835 | ||
|
|
4a199c7b6f | ||
|
|
3facd05a94 | ||
|
|
bb443be367 | ||
|
|
1952aa2026 | ||
|
|
d206ac4518 | ||
|
|
6b19ba1933 | ||
|
|
bcf9c051f0 | ||
|
|
4934809b88 | ||
|
|
55298f0134 | ||
|
|
7e9207ae3c | ||
|
|
7915f97bd5 | ||
|
|
1231627412 | ||
|
|
40977cedc7 | ||
|
|
d500196dee | ||
|
|
994d489226 | ||
|
|
602c47a900 | ||
|
|
de4315b4b7 | ||
|
|
9bbdae6986 | ||
|
|
c7899ba401 | ||
|
|
4b9a8cc235 | ||
|
|
4ae5cdee83 | ||
|
|
1393ed3ca6 | ||
|
|
6ec24c87cd | ||
|
|
a404311097 | ||
|
|
a7d8260de5 | ||
|
|
63fe8b53f9 | ||
|
|
4ad4711b84 | ||
|
|
f13625719b | ||
|
|
5ae29742ce | ||
|
|
ec6f36cf82 | ||
|
|
c18cf75b4f | ||
|
|
7b6673b43b | ||
|
|
d1dea2593f | ||
|
|
aebec7fa94 | ||
|
|
7f79bf0e87 | ||
|
|
0e0d45322e | ||
|
|
b7f572149f | ||
|
|
b07bd82f60 | ||
|
|
086c7469c5 | ||
|
|
37a968a142 | ||
|
|
dab05fb5c5 | ||
|
|
115dde8c2f | ||
|
|
38c78228aa | ||
|
|
9999abe462 | ||
|
|
d16ce28ee5 | ||
|
|
effa972a40 | ||
|
|
e998bcfde6 | ||
|
|
c9023d4792 | ||
|
|
c30931545f | ||
|
|
ed62abe464 | ||
|
|
34e42a1076 | ||
|
|
451d945095 | ||
|
|
cfb44548ab | ||
|
|
c6de3de370 | ||
|
|
59b653f92a | ||
|
|
b509e35cd1 | ||
|
|
079553e8bb | ||
|
|
220cbbac80 | ||
|
|
6993137430 | ||
|
|
d0a378413f | ||
|
|
c314637847 | ||
|
|
219e9ee8da | ||
|
|
d47268f45c | ||
|
|
d5eef1faf5 | ||
|
|
a7097a7310 | ||
|
|
0335ea7056 | ||
|
|
71777652cf | ||
|
|
7a91faab2b | ||
|
|
ed073877a6 | ||
|
|
8a46365f51 | ||
|
|
04fded6d94 | ||
|
|
15670fc7c4 | ||
|
|
cf27d4d9cc | ||
|
|
48c30dc266 | ||
|
|
d2430323b2 | ||
|
|
2a38699595 | ||
|
|
e76b71e245 | ||
|
|
92174438f6 | ||
|
|
0c85abf074 | ||
|
|
e9ea536aaf | ||
|
|
d2bbbb3b73 | ||
|
|
6735c361a4 | ||
|
|
1243ece157 | ||
|
|
7573013da4 | ||
|
|
b79c48718e | ||
|
|
8354c63a62 | ||
|
|
c163d9ac46 | ||
|
|
dcf63e203a | ||
|
|
8fb01205ea | ||
|
|
3e85b268a0 | ||
|
|
78d93428f2 | ||
|
|
4454e55b1e | ||
|
|
f1229fe8ce | ||
|
|
09581ae654 | ||
|
|
03fd8c296d | ||
|
|
155c29cc55 | ||
|
|
a017621a3d | ||
|
|
bea1c1c22d | ||
|
|
02c7628840 | ||
|
|
b5a9f302df | ||
|
|
7b62c14ce5 | ||
|
|
c668eb5db8 | ||
|
|
2d53459291 | ||
|
|
b25ca66cc6 | ||
|
|
ae4578758a | ||
|
|
790d38b646 | ||
|
|
cccc51283a | ||
|
|
da43e5fc5b | ||
|
|
97defccf9e | ||
|
|
2fd5d33094 | ||
|
|
c9cda88f7f | ||
|
|
5cb7299b64 | ||
|
|
7e99c0d0a5 | ||
|
|
a2e5452255 | ||
|
|
21d5dbe6e3 | ||
|
|
e648a01d95 | ||
|
|
a526d519bd | ||
|
|
78fc9b0478 | ||
|
|
cd9f5b927e | ||
|
|
4faf940689 | ||
|
|
6c956f472a | ||
|
|
5060e05c21 | ||
|
|
128cb39d4b | ||
|
|
0773bea679 | ||
|
|
effbae9289 | ||
|
|
f04dd95c38 | ||
|
|
b5c400044a | ||
|
|
3a9f294bd0 | ||
|
|
1707eff9a6 | ||
|
|
5bc294f62e | ||
|
|
0b927d5390 | ||
|
|
1a39e03b33 | ||
|
|
ffa5fd5b36 | ||
|
|
2705508c4d | ||
|
|
748f2a9417 | ||
|
|
739ec52243 | ||
|
|
53866cdcbd | ||
|
|
750397e213 | ||
|
|
438656a549 | ||
|
|
fa70d9cbed | ||
|
|
b5f9b85324 | ||
|
|
8d5acc195c | ||
|
|
cf78f4b397 | ||
|
|
a2cdc7ce31 | ||
|
|
c4cca8cf42 | ||
|
|
5d03661357 | ||
|
|
fa9d167025 | ||
|
|
01aaff9b83 | ||
|
|
e26cda1d6b | ||
|
|
7f9b31bcc2 | ||
|
|
08800e8152 | ||
|
|
6f7a93c517 | ||
|
|
9f0c288374 | ||
|
|
0a49ab8474 | ||
|
|
40b8f02a2e | ||
|
|
7d686b9d91 | ||
|
|
75ebe459be | ||
|
|
02c6665051 | ||
|
|
7a3b19b64b | ||
|
|
7d186f2281 | ||
|
|
1aa4d85161 | ||
|
|
93ad0e7251 | ||
|
|
1590b7e927 | ||
|
|
f9b90b4ce6 | ||
|
|
9c92aa5972 | ||
|
|
d3c0da36aa | ||
|
|
374530df4e | ||
|
|
a3adde2661 | ||
|
|
bf9940516d | ||
|
|
09a1e09c30 | ||
|
|
3955f0d7ae | ||
|
|
1483559254 | ||
|
|
f09441ac28 | ||
|
|
90cff02b26 | ||
|
|
cb2228b823 | ||
|
|
e5bd3e6bc0 | ||
|
|
223e017b9e | ||
|
|
9535d05584 | ||
|
|
f01f050ffd | ||
|
|
f48f486f64 | ||
|
|
31fa445733 | ||
|
|
3eea0cea08 | ||
|
|
ff6364c77b | ||
|
|
893b63c5d5 | ||
|
|
b1ec0b9b83 | ||
|
|
5db66a2fb3 | ||
|
|
e233b88969 | ||
|
|
eca4377c5a | ||
|
|
3c706aed5e | ||
|
|
450bcf4e66 | ||
|
|
ff4568235a | ||
|
|
6f598f9e72 | ||
|
|
68d4337b15 | ||
|
|
34b061b11c | ||
|
|
f943890cfb | ||
|
|
f75994f735 | ||
|
|
2c83b13192 | ||
|
|
08bcd64289 | ||
|
|
e81cfe9990 | ||
|
|
5bc80eba6a | ||
|
|
727a100f81 | ||
|
|
23204e545b | ||
|
|
639a9152c2 | ||
|
|
ffbe879062 | ||
|
|
f233b13e51 | ||
|
|
dbe2e22511 | ||
|
|
b998ef860b | ||
|
|
e29327a0d9 | ||
|
|
c94cf22fcc | ||
|
|
07ad008f3d | ||
|
|
9fa282d18e | ||
|
|
84ec84f4ac | ||
|
|
1475fd50ba | ||
|
|
03917fb70b | ||
|
|
3046cbf3b9 | ||
|
|
74fc224a84 | ||
|
|
0ea88630e2 | ||
|
|
f1f351a8c0 | ||
|
|
5f85bec1ec | ||
|
|
c498067e75 | ||
|
|
25c4bba3fa | ||
|
|
034e8faaf4 | ||
|
|
9db0bdedb1 | ||
|
|
48117a6894 | ||
|
|
3cd890a1f5 | ||
|
|
dddbb0ed8f | ||
|
|
5d4f672411 | ||
|
|
9c3a0964b6 | ||
|
|
5f0774a84f | ||
|
|
e560b80611 | ||
|
|
4da6fad049 | ||
|
|
8f76d94b86 | ||
|
|
a28bce71df | ||
|
|
eed55490ef | ||
|
|
b1c7685afe | ||
|
|
4d79e9de4f | ||
|
|
1ae54db7de | ||
|
|
6d63df24a3 | ||
|
|
85dd32e0ce | ||
|
|
28e418ff23 | ||
|
|
4eb89b187f | ||
|
|
c5734beef6 | ||
|
|
f4735ebd80 | ||
|
|
43ae6a4a37 | ||
|
|
f362f58f95 | ||
|
|
6d211264fc | ||
|
|
3d919f5df6 | ||
|
|
f9d5f9852a | ||
|
|
0e79035765 | ||
|
|
d5cf38eaca | ||
|
|
1cfa14d8d2 | ||
|
|
bf5a50eb2a | ||
|
|
f296579aad | ||
|
|
16bb53d0e4 | ||
|
|
b6e2549436 | ||
|
|
0814173210 | ||
|
|
375ffada5c | ||
|
|
ae37de0dd2 | ||
|
|
ce4b4771db | ||
|
|
56c61ac723 | ||
|
|
9900dd64b8 | ||
|
|
53400b6322 | ||
|
|
47537ab30a | ||
|
|
6a3692d7f4 | ||
|
|
eef2b95948 | ||
|
|
7012bef28d | ||
|
|
b3b44d144e | ||
|
|
841eba79a3 | ||
|
|
77234f9e3d | ||
|
|
14478d7831 | ||
|
|
50aa7d937e | ||
|
|
2c7e01e003 | ||
|
|
a6ce5f04aa | ||
|
|
8bc6814b42 | ||
|
|
024177b0c7 | ||
|
|
b7faa0acae | ||
|
|
0dbdbc7a13 | ||
|
|
08271e60bf | ||
|
|
ec74698001 | ||
|
|
6cecacd334 | ||
|
|
c3d27900e1 | ||
|
|
f10df3607f | ||
|
|
416be24722 | ||
|
|
e53b4a2285 | ||
|
|
a88320b1b2 | ||
|
|
76f9a144ac | ||
|
|
a673772cc1 | ||
|
|
9e6d8195eb | ||
|
|
91d97c4a2c | ||
|
|
5e1df9263b | ||
|
|
e54921ad71 | ||
|
|
1b8d0877f3 | ||
|
|
a4e962560c | ||
|
|
be7d3ceb15 | ||
|
|
1e652d5725 | ||
|
|
1e7e5422be | ||
|
|
723e9e2bb1 | ||
|
|
1f572cc95b | ||
|
|
fb63eea4a0 | ||
|
|
7efb37010d | ||
|
|
6372af8d8a | ||
|
|
0b823ea43e | ||
|
|
cebb92199f | ||
|
|
6deabf8a66 | ||
|
|
5de74a51e0 | ||
|
|
85d6305874 | ||
|
|
30d685a6f7 | ||
|
|
fcc8a58839 | ||
|
|
5a608a9b62 | ||
|
|
eb9c127a10 | ||
|
|
ed55690ff3 | ||
|
|
502afa5213 | ||
|
|
24185d66ce | ||
|
|
181ba65c63 | ||
|
|
702f36a529 | ||
|
|
e2f73bf858 | ||
|
|
7265e8c6f4 | ||
|
|
b8b9808e94 | ||
|
|
7639773c40 | ||
|
|
6ab7370149 | ||
|
|
73994fe603 | ||
|
|
3fa646723d | ||
|
|
eb08b1fbdc | ||
|
|
93ac820005 | ||
|
|
c100e25ab9 | ||
|
|
716489ceed | ||
|
|
07d5f5e52c | ||
|
|
b889debd5e | ||
|
|
b273fe1f68 | ||
|
|
376cd6e83f | ||
|
|
e8cb1a7b9f | ||
|
|
9f0c5beae7 | ||
|
|
0ea2f16322 | ||
|
|
13ca2e8d93 | ||
|
|
3833256c8c | ||
|
|
30521612b2 | ||
|
|
d069cfc309 | ||
|
|
27461b067a | ||
|
|
017712742b | ||
|
|
afce21a1bd | ||
|
|
030e2e270f | ||
|
|
c69a86b535 | ||
|
|
b64e4cf3dc | ||
|
|
a2e06adbbe | ||
|
|
43b3397541 | ||
|
|
bd0bb1542c | ||
|
|
a92a07f9c0 | ||
|
|
42ed5509ee | ||
|
|
a6582503f2 | ||
|
|
7aecb889d2 | ||
|
|
c273f87cc7 | ||
|
|
76d00c993a | ||
|
|
013b47e6e7 | ||
|
|
9f1e9934ff | ||
|
|
48b218bd9c | ||
|
|
af5baa4f3a | ||
|
|
a2cf38d904 | ||
|
|
185522d943 | ||
|
|
a42e4dd080 | ||
|
|
3a5486889f | ||
|
|
1a1f100902 | ||
|
|
c67b214298 | ||
|
|
3ad1d5c661 | ||
|
|
13400d9bde | ||
|
|
048e8dfef5 | ||
|
|
aaf7a89192 | ||
|
|
e3ee9135ff | ||
|
|
a774fc0beb | ||
|
|
f3429bd537 | ||
|
|
37876acfda | ||
|
|
2a6dd0d1a2 | ||
|
|
b0626dd37a | ||
|
|
ed0ed8d7fc | ||
|
|
d67d999930 | ||
|
|
ac79cff6b9 | ||
|
|
50aadc6ea7 | ||
|
|
9036d114ed | ||
|
|
75c19104ae | ||
|
|
d9b7f88287 | ||
|
|
ae28c09560 | ||
|
|
6ffc738a51 | ||
|
|
82dcc4de6a | ||
|
|
f7a426f65a | ||
|
|
a94ef78066 | ||
|
|
62d738f5c2 | ||
|
|
1c56a0a608 | ||
|
|
dc3976bdda | ||
|
|
454778b190 | ||
|
|
5e78c93b71 | ||
|
|
3aefe21f16 | ||
|
|
0fc7958ccc | ||
|
|
13dc4e74c9 | ||
|
|
a17fa16841 | ||
|
|
b13757f5d3 | ||
|
|
b9df6f4762 | ||
|
|
b7d1c62486 | ||
|
|
90e5f1b665 | ||
|
|
3132fd7783 | ||
|
|
87808902e6 | ||
|
|
fb33d8186e | ||
|
|
8bd2e7f879 | ||
|
|
e66744e3f1 | ||
|
|
85f2979313 | ||
|
|
a85ee9b108 | ||
|
|
9ab2f38436 | ||
|
|
5bcdca4fcc | ||
|
|
729edb65be | ||
|
|
db8afb769b | ||
|
|
7dfc93892c | ||
|
|
d278cb6939 | ||
|
|
bced5172d2 | ||
|
|
bb5beb66a7 | ||
|
|
f849b6ddb7 | ||
|
|
d2733a53a2 | ||
|
|
1b1ae44f5d | ||
|
|
8515dc2616 | ||
|
|
ba7a8d8937 | ||
|
|
d543fb9917 | ||
|
|
f4d390b77b | ||
|
|
ffbce1fd25 | ||
|
|
2d78ec6edd | ||
|
|
9cacdd166f | ||
|
|
9af0a5d843 | ||
|
|
3313295532 | ||
|
|
fdf6c147dc | ||
|
|
323dbd5ca9 | ||
|
|
d01fd74fa3 | ||
|
|
8c33b88e3e | ||
|
|
5d11397fca | ||
|
|
995321978f | ||
|
|
448789dad0 | ||
|
|
e9ba6819ba | ||
|
|
3056c7b803 | ||
|
|
f2c28fd1f7 | ||
|
|
11e4ff1eb5 | ||
|
|
81cd74c244 | ||
|
|
faade7c057 | ||
|
|
0032066e1d | ||
|
|
dd938baced | ||
|
|
b835b6ee36 | ||
|
|
3660d65df6 | ||
|
|
3e0b4125e0 | ||
|
|
9820a3d81e | ||
|
|
b670a4cee6 | ||
|
|
a5dd5275c8 | ||
|
|
9b6ad2fa60 | ||
|
|
1d80028c93 | ||
|
|
a013e95fcf | ||
|
|
eb4d6ece3f | ||
|
|
a50d1ef456 | ||
|
|
c179ed732c | ||
|
|
a85a313ebb | ||
|
|
534ccd275d | ||
|
|
3c3d043276 | ||
|
|
786adb7c44 | ||
|
|
bb6c8dc225 | ||
|
|
a7cd88b2be | ||
|
|
a9fad337e2 | ||
|
|
d4dc1b1589 | ||
|
|
ad94adbb53 | ||
|
|
b692799dcb | ||
|
|
04dcf47746 | ||
|
|
aebb3c44fe | ||
|
|
8cf345196a | ||
|
|
173fdb2297 | ||
|
|
120db6e7d8 | ||
|
|
55555506be | ||
|
|
41965e962d | ||
|
|
30fdd00d65 | ||
|
|
37e3fd904d | ||
|
|
dc22b024b8 | ||
|
|
f412d5ad4c | ||
|
|
24cfe807e6 | ||
|
|
6a721ac2c1 | ||
|
|
4a4b22dfba | ||
|
|
6d4524c153 | ||
|
|
d7b2f82a4a | ||
|
|
844a2fe1e8 | ||
|
|
baf822c685 | ||
|
|
f3169a631c | ||
|
|
d171db36bc | ||
|
|
34c7576cd5 | ||
|
|
f859d0678b | ||
|
|
0986cb3fd9 | ||
|
|
645fd9a135 | ||
|
|
9582e6840a | ||
|
|
a8a9cfb2ab | ||
|
|
5519b33a08 | ||
|
|
976ef0252e | ||
|
|
e6829d0804 | ||
|
|
9f985a7b26 | ||
|
|
a628aeb1a8 | ||
|
|
d81c80b150 | ||
|
|
63ee016691 | ||
|
|
4935385572 | ||
|
|
30069d3039 | ||
|
|
3ef8a5a762 | ||
|
|
b12fda5007 | ||
|
|
26925c30c1 | ||
|
|
4085816fa3 | ||
|
|
7e36e5abe6 | ||
|
|
2037189148 | ||
|
|
c7781e66e1 | ||
|
|
8843675ad4 | ||
|
|
c05a1ea6b4 | ||
|
|
d9a5ac849b | ||
|
|
51d4c29dd5 | ||
|
|
c2bb9cbdaf | ||
|
|
d185765831 | ||
|
|
f57f311f16 | ||
|
|
4c81849c60 | ||
|
|
156c8319d9 | ||
|
|
b8de3310d0 | ||
|
|
f28cf664cb | ||
|
|
02b876155a | ||
|
|
97bd1f71c3 | ||
|
|
8be4445f0d | ||
|
|
550cf47db4 | ||
|
|
05d32eec08 | ||
|
|
59c181eeda | ||
|
|
dd5fd2a2c3 | ||
|
|
6ab8fbf538 | ||
|
|
509919da84 | ||
|
|
04bd5f36a0 | ||
|
|
801f5b7861 | ||
|
|
09d86e1220 | ||
|
|
6110aa1d32 | ||
|
|
11e6c80dbf | ||
|
|
1f32536ff7 | ||
|
|
7979206f21 | ||
|
|
f7901790ad | ||
|
|
7fae16f962 | ||
|
|
1dd76012f8 | ||
|
|
8fd3f4ee7d | ||
|
|
e30b8ed53e | ||
|
|
e0960d9113 | ||
|
|
35dda1cd34 | ||
|
|
ef2253fe58 | ||
|
|
ecea3aed7e | ||
|
|
2e81cae271 | ||
|
|
080eede356 | ||
|
|
fe37c687e4 | ||
|
|
27efef1d9b | ||
|
|
52aa1ac0da | ||
|
|
b5c23fdb83 | ||
|
|
0b16c9aef4 | ||
|
|
3be97acd9c | ||
|
|
8df8e6797f | ||
|
|
156ba44656 | ||
|
|
1b3663d60c | ||
|
|
8f0ea2f6a5 | ||
|
|
5e34b12e5c | ||
|
|
d124575a91 | ||
|
|
f5364ab4d0 | ||
|
|
b5580c5649 | ||
|
|
e9200ea8fb | ||
|
|
2e0c280ea6 | ||
|
|
3948a414b5 | ||
|
|
2c83068605 | ||
|
|
6f6ccad00b | ||
|
|
bd18f14137 | ||
|
|
d54ca7ee43 | ||
|
|
19452c2461 | ||
|
|
4e2e96a6dd | ||
|
|
7957d131c0 | ||
|
|
ca9dfaff1d | ||
|
|
7e9475791b | ||
|
|
c8fb44a7c4 | ||
|
|
bb70183bc7 | ||
|
|
ff80ba1814 | ||
|
|
5d292dcaf7 | ||
|
|
bcc5c4520f | ||
|
|
aa7ea59b5e | ||
|
|
16e85d6d5c | ||
|
|
453e65ec53 | ||
|
|
4cbcb9418c | ||
|
|
f2120229e2 | ||
|
|
4d2db30000 | ||
|
|
ca575b267b | ||
|
|
3216666a94 | ||
|
|
4ef5606f05 | ||
|
|
6122dc3353 | ||
|
|
14ae792091 | ||
|
|
9da5065700 | ||
|
|
22e155998d | ||
|
|
939a1afbbf | ||
|
|
ecda9fe232 | ||
|
|
76eb79eb2d | ||
|
|
325e597772 | ||
|
|
c6334d2bcd | ||
|
|
72ff9ff7e9 | ||
|
|
b45ad5dcaf | ||
|
|
3d14964365 | ||
|
|
d15bab5a29 | ||
|
|
c4deb29b21 | ||
|
|
0eafee3105 | ||
|
|
4d116e81c8 | ||
|
|
3a41b31e19 | ||
|
|
f9786468db | ||
|
|
a49124d1d0 | ||
|
|
3087b4b776 | ||
|
|
aa595fe623 | ||
|
|
30a571f56c | ||
|
|
7b0a8dce1e | ||
|
|
6fce941640 | ||
|
|
dbfbf61ddc | ||
|
|
5d618de296 | ||
|
|
3e556425ec | ||
|
|
370db726a4 | ||
|
|
12d6c8c3f5 | ||
|
|
0fdb6544cc | ||
|
|
ca52845c65 | ||
|
|
fac32f95d8 | ||
|
|
f27b37d21a | ||
|
|
e066fcc172 | ||
|
|
14535e814f | ||
|
|
1cfb873122 | ||
|
|
de16285bf2 | ||
|
|
d01f3f31ff | ||
|
|
edec98579d | ||
|
|
ea583e98b2 | ||
|
|
c4a080e081 | ||
|
|
4e35845ecf | ||
|
|
9d76fcbcf1 | ||
|
|
60290473e1 | ||
|
|
f94456ce7f | ||
|
|
65cda68f40 | ||
|
|
576731c386 | ||
|
|
0b56af76b7 | ||
|
|
10e6cbabcf | ||
|
|
f33529a1d4 | ||
|
|
7a5a6a8db2 | ||
|
|
c43c4a8b07 | ||
|
|
0e0e1332c7 | ||
|
|
ee19c5e25f | ||
|
|
c0c1355216 | ||
|
|
da3394f3fd | ||
|
|
c856723945 | ||
|
|
5e10ccdbdd | ||
|
|
a49f645647 | ||
|
|
cf8d714ba7 | ||
|
|
17bb625d0d | ||
|
|
5e7a858d55 | ||
|
|
8276474314 | ||
|
|
d108261654 | ||
|
|
aa98be443c | ||
|
|
eecb583f10 | ||
|
|
809aae27b1 | ||
|
|
028ca15498 | ||
|
|
be4403331f | ||
|
|
b84025debf | ||
|
|
4685f29aba | ||
|
|
a832698366 | ||
|
|
cf894fd0bd | ||
|
|
fd6c04bd94 | ||
|
|
49a2352d6d | ||
|
|
291871ec45 | ||
|
|
73c21a1156 | ||
|
|
27eed06617 | ||
|
|
a440cbbbdc | ||
|
|
eb9ca5eb1d | ||
|
|
5eb1277691 | ||
|
|
6de424b185 | ||
|
|
f89491d801 | ||
|
|
154de4818e | ||
|
|
c5895a3082 | ||
|
|
e085257a51 | ||
|
|
d8e84cf045 | ||
|
|
a5eb61421d | ||
|
|
cddbea2718 | ||
|
|
c9bf5158e4 | ||
|
|
7822b36f97 | ||
|
|
20d541ca8e | ||
|
|
72af9fb4a9 | ||
|
|
a2972a3329 | ||
|
|
97b74c0c8f | ||
|
|
332519e5d4 | ||
|
|
881641e2b4 | ||
|
|
82bfe74175 | ||
|
|
bac3451c21 | ||
|
|
c97495ab05 | ||
|
|
aa3dad1e07 | ||
|
|
2d4e15504c | ||
|
|
3cd41e3d0f | ||
|
|
a94518c48d | ||
|
|
2837671ed7 | ||
|
|
e49eed2a24 | ||
|
|
edfc27c960 | ||
|
|
41a10932cb | ||
|
|
119538c10c | ||
|
|
0a03fbb82e | ||
|
|
b838054e2f | ||
|
|
45ac118381 | ||
|
|
e1600eadbc | ||
|
|
cc23f98078 | ||
|
|
b68cc671eb | ||
|
|
8c215a0a0b | ||
|
|
374c6a9367 | ||
|
|
5d93d9893e | ||
|
|
010c26ea89 | ||
|
|
5da90f7585 | ||
|
|
5356591d9c | ||
|
|
8df5d22805 | ||
|
|
5684ab3c05 | ||
|
|
d7b8f4c228 | ||
|
|
66fe03bbcd | ||
|
|
e7496dc9cb | ||
|
|
3b52af0b8a | ||
|
|
4ffe709ab9 | ||
|
|
c0c15a3ee5 | ||
|
|
b724330cb1 | ||
|
|
8b9ce17959 | ||
|
|
26116474c5 | ||
|
|
3411fd8557 | ||
|
|
0bee3e38a0 | ||
|
|
dcc2224657 | ||
|
|
1bf1c43f23 | ||
|
|
53b336aee9 | ||
|
|
94b5407cb4 | ||
|
|
e7357e69fb | ||
|
|
d5a036dfc6 | ||
|
|
e256de06d9 | ||
|
|
59beba616c | ||
|
|
735747e0d4 | ||
|
|
ab167d8c4c | ||
|
|
944f010a8c | ||
|
|
dd064a3843 | ||
|
|
244b20e1f0 | ||
|
|
88308a352b | ||
|
|
c63df9d245 | ||
|
|
2780ad2edc | ||
|
|
00b3122c2c | ||
|
|
18221be556 | ||
|
|
80abd9284f | ||
|
|
87b6cb073f | ||
|
|
e2cbbb2c93 | ||
|
|
c771b84463 | ||
|
|
2460e6957f | ||
|
|
0ec42eb796 | ||
|
|
b78b5ea9e1 | ||
|
|
d26bfc9aab | ||
|
|
c64730e07b | ||
|
|
f8b00b92b4 | ||
|
|
3c9ec2578e | ||
|
|
5f79f46e30 | ||
|
|
30c250c314 | ||
|
|
69f504d91c | ||
|
|
c505dd9c2b | ||
|
|
a7638dee0a | ||
|
|
b8e5ad5107 | ||
|
|
0b2d04bc6f | ||
|
|
803025b8c5 | ||
|
|
a7154da0b6 | ||
|
|
d04b33d1d9 | ||
|
|
614edc22ca | ||
|
|
eeb760260a | ||
|
|
eb5d876487 | ||
|
|
3bf2c5c8b6 | ||
|
|
3c24049d66 | ||
|
|
1f1b6d45e3 | ||
|
|
7b8a17a544 | ||
|
|
95d1b1295e | ||
|
|
4abd0407c8 | ||
|
|
65f229875d | ||
|
|
9be84501ec | ||
|
|
6e400cabd0 | ||
|
|
1f00614551 | ||
|
|
1da61d076e | ||
|
|
2ce04b4dd2 | ||
|
|
94a52c80cd | ||
|
|
af71cf9a82 | ||
|
|
594c7d6d29 | ||
|
|
4575c3576f | ||
|
|
243a6d20cc | ||
|
|
9f27cc155a | ||
|
|
64bfa122bd | ||
|
|
5be9a3f219 | ||
|
|
b0a478c156 | ||
|
|
96b6a0bc2c | ||
|
|
c3b79c5330 | ||
|
|
da54738902 | ||
|
|
ced508443a | ||
|
|
22d0446da4 | ||
|
|
8c7b3455c9 | ||
|
|
eac145f010 | ||
|
|
8765d06c2f | ||
|
|
7d19450da7 | ||
|
|
4edaeee883 | ||
|
|
e44ea5dbed | ||
|
|
7ae61b0c6d | ||
|
|
f0ffdc371f | ||
|
|
50f2040eb2 | ||
|
|
b76a8f7d76 | ||
|
|
9b0ab6b1c1 | ||
|
|
bf899f5044 | ||
|
|
75a20b66d3 | ||
|
|
018862d012 | ||
|
|
87d3714ed0 | ||
|
|
f8f5ee7f25 | ||
|
|
45e772acde | ||
|
|
0b7a79bce0 | ||
|
|
4a9d571cb1 | ||
|
|
1d5a8ec81b | ||
|
|
f6c4e26b3b | ||
|
|
536fded762 | ||
|
|
4a79b3f42c | ||
|
|
6e5052f6ab | ||
|
|
fddaeca050 | ||
|
|
5b53ba33ab | ||
|
|
583fb8d6d2 | ||
|
|
9fa51836c7 | ||
|
|
6432dd1fef | ||
|
|
341d61444c | ||
|
|
40f71cc703 | ||
|
|
a8155b9a39 | ||
|
|
f5e9aea2ac | ||
|
|
be41f0ba11 | ||
|
|
f92ca02907 | ||
|
|
7ccddd3d33 | ||
|
|
545c4ec30f | ||
|
|
7aa58c8287 | ||
|
|
32bd11dccc | ||
|
|
1c6488312e | ||
|
|
d767e33da5 | ||
|
|
cee82e7408 | ||
|
|
8361a47259 | ||
|
|
1032421fdd | ||
|
|
17e52e2598 | ||
|
|
4265f86c48 | ||
|
|
f82535c497 | ||
|
|
d25b80ee9c | ||
|
|
0c019d07a2 | ||
|
|
dcaa9c56c0 | ||
|
|
87d45840d9 | ||
|
|
8174aae392 | ||
|
|
2a916d1d45 | ||
|
|
b628c34b20 | ||
|
|
f161b165b2 | ||
|
|
316ee693e3 | ||
|
|
82bfb99548 | ||
|
|
ed5ccf1faa | ||
|
|
fd3bec8371 | ||
|
|
9e2bf9cbbe | ||
|
|
ec198e818a | ||
|
|
aeeba5c668 | ||
|
|
0ca5c74ce7 | ||
|
|
b6c1ad5ce7 | ||
|
|
4c64671cb8 | ||
|
|
7773c49112 | ||
|
|
5e451e4fe3 | ||
|
|
0801ef66a0 | ||
|
|
31f0064e53 | ||
|
|
142191caeb | ||
|
|
d5ee92f12a | ||
|
|
2cd36e6244 | ||
|
|
7b4b637ec3 | ||
|
|
ca931d1caa | ||
|
|
e6773a09c0 | ||
|
|
006b0f1c9d | ||
|
|
14c87f427e | ||
|
|
310cc87a5e | ||
|
|
7533eb0540 | ||
|
|
e5d9a84fc8 | ||
|
|
fb2bb0cb09 | ||
|
|
4a7ce7ebfb | ||
|
|
8eaaabe5da | ||
|
|
f78bdf834d | ||
|
|
86cc187eed | ||
|
|
9ae4ee1430 | ||
|
|
2a71a0f0be | ||
|
|
b3cf0c1bde | ||
|
|
b4e01737c7 | ||
|
|
8243fe8846 | ||
|
|
44b9a3ca8a | ||
|
|
52becd0255 | ||
|
|
4774227a76 | ||
|
|
faaeeb5f72 | ||
|
|
499eb45064 | ||
|
|
86d2dc725b | ||
|
|
39104183e9 | ||
|
|
ce41283a71 | ||
|
|
ad8aaa1738 | ||
|
|
7ff381cacf | ||
|
|
e522f76db6 | ||
|
|
106e3544a8 | ||
|
|
704fd3bea8 | ||
|
|
c8f13eedbc | ||
|
|
959cf3d483 | ||
|
|
7d59ceb9d1 | ||
|
|
cdcb071826 | ||
|
|
2829c4c26a | ||
|
|
c810874014 | ||
|
|
e1d9aef2d7 | ||
|
|
71e526370c | ||
|
|
3c7df4974c | ||
|
|
f5c95d2ba0 | ||
|
|
a4f09c02e8 | ||
|
|
be8363786c | ||
|
|
7710711def | ||
|
|
7320136079 | ||
|
|
eaced6942a | ||
|
|
5411724696 | ||
|
|
ef3c282bfa | ||
|
|
1a1cd223d3 | ||
|
|
3d55591436 | ||
|
|
b5b9cfe2aa | ||
|
|
39db76f189 | ||
|
|
303e32fe5d | ||
|
|
dd4841c82c | ||
|
|
add970c0ae | ||
|
|
7df3e70722 | ||
|
|
ea4459b89e | ||
|
|
259c952636 | ||
|
|
15b1ce370c | ||
|
|
f7ab4aef4e | ||
|
|
a1e6459dc1 | ||
|
|
31a3dcd2f7 | ||
|
|
f0120fef63 | ||
|
|
5095e6af14 | ||
|
|
19f21a9453 | ||
|
|
676908daca | ||
|
|
66a5d0472d | ||
|
|
cc30e307e9 | ||
|
|
57e3eb5c8e | ||
|
|
b855f6876c | ||
|
|
8edc06ba41 | ||
|
|
69aa31566b | ||
|
|
19d3483209 | ||
|
|
c1c7e65a3c | ||
|
|
2d0044de95 | ||
|
|
418e3af903 | ||
|
|
c3ddeae3f3 | ||
|
|
a9f0e5ba16 | ||
|
|
8bf8d45ebe | ||
|
|
1777c762b3 | ||
|
|
0b1337070e | ||
|
|
b158496bea | ||
|
|
a79b23e090 | ||
|
|
bdb56240f0 | ||
|
|
6dddf3eb30 | ||
|
|
7bd8569151 | ||
|
|
b03c9f1e35 | ||
|
|
057b5ff760 | ||
|
|
ba512b4159 | ||
|
|
a298aea2fe | ||
|
|
f433463074 | ||
|
|
afae08d6fe | ||
|
|
7cf2a08aff | ||
|
|
7df6781985 | ||
|
|
ae0f5e62e3 | ||
|
|
14c8356c6b | ||
|
|
45ffd4a793 | ||
|
|
eb8d39025e | ||
|
|
1f739e1c63 | ||
|
|
82111236fb | ||
|
|
813a94f8d6 | ||
|
|
e83b75e2c3 | ||
|
|
ce1e880ed0 | ||
|
|
427672065e | ||
|
|
055c5d5e54 | ||
|
|
4de7794e04 | ||
|
|
79686fd8ce | ||
|
|
cc5df0198b | ||
|
|
abc6e55ba7 | ||
|
|
0c8afb7fd6 | ||
|
|
c0c2cca44e | ||
|
|
faa645cb97 | ||
|
|
725c19aafc | ||
|
|
cc3b4c974d | ||
|
|
6ce64fad72 | ||
|
|
c1af67d4a3 | ||
|
|
802cb15007 | ||
|
|
b34bf3e56a | ||
|
|
bf37700088 | ||
|
|
4a43ddfc25 | ||
|
|
650a1f5154 | ||
|
|
5eda7e30b0 | ||
|
|
8a26f547e5 | ||
|
|
343088913f | ||
|
|
5a0272fd5b | ||
|
|
dc93503625 | ||
|
|
6ea6c0889b | ||
|
|
99ab72df3f | ||
|
|
99bda1385e | ||
|
|
7ce3b4a8c0 | ||
|
|
495722d0d6 | ||
|
|
aca31be5d7 | ||
|
|
b9b7ae8d99 | ||
|
|
0d46c1d13a | ||
|
|
6b63ecdc19 | ||
|
|
f9ca0323a1 | ||
|
|
c50aa4d2e8 | ||
|
|
a72ded9079 | ||
|
|
cbabbee075 | ||
|
|
f55a344b7a | ||
|
|
d84f8418ff | ||
|
|
30c5e92de6 | ||
|
|
5f618a7f65 | ||
|
|
3e833419db | ||
|
|
0d94bae0b5 | ||
|
|
f5dec96ffb | ||
|
|
e91d12caaf | ||
|
|
fd5a1faa58 | ||
|
|
90a9212793 | ||
|
|
7e582ac1fc | ||
|
|
65a740569c | ||
|
|
a47ef0e1f5 | ||
|
|
b75ad006f1 | ||
|
|
dbc3f0cd83 | ||
|
|
ea2750f970 | ||
|
|
a2eb5a2483 | ||
|
|
54178543d6 | ||
|
|
5436f21bc0 | ||
|
|
839768a2a5 | ||
|
|
2e195d5aa1 | ||
|
|
66811f8eb5 | ||
|
|
a92326790d | ||
|
|
d405767fb0 | ||
|
|
8d7c6d3835 | ||
|
|
e362591b7a | ||
|
|
ee5f4b73e8 | ||
|
|
0d15eb2898 | ||
|
|
4af50206ad | ||
|
|
c596937006 | ||
|
|
17eb61e1eb | ||
|
|
a333185e84 | ||
|
|
f6863ae2d6 | ||
|
|
36830250b5 | ||
|
|
4ca1c3537b | ||
|
|
eeab09eacb | ||
|
|
af16967257 | ||
|
|
75e2bf5a9a | ||
|
|
4db3bc409b | ||
|
|
32ccf414ea | ||
|
|
615e48fffc | ||
|
|
93bf3fce29 | ||
|
|
899601569a | ||
|
|
b1805b64a2 | ||
|
|
58190343b1 | ||
|
|
99d48b1939 | ||
|
|
82b66d53cb | ||
|
|
3200de56cc | ||
|
|
0a627d5c79 | ||
|
|
22399deb79 | ||
|
|
6a77617e3b | ||
|
|
2868ef99ae | ||
|
|
21557f9892 | ||
|
|
d2385ae62d | ||
|
|
a84efef389 | ||
|
|
310bcd1585 | ||
|
|
753f44deb2 | ||
|
|
df1f0f8f09 | ||
|
|
45e1b50674 | ||
|
|
0a2b048fb1 | ||
|
|
e3c5dca09d | ||
|
|
88339b7214 | ||
|
|
1f2bb18bc1 | ||
|
|
74977a6154 | ||
|
|
00413fe7a4 | ||
|
|
9bb9d331ad | ||
|
|
f022ffdff4 | ||
|
|
28dade2a34 | ||
|
|
7378b9d843 | ||
|
|
71075e95bf | ||
|
|
108990cf06 | ||
|
|
ebfdf4b052 | ||
|
|
dbf4073216 | ||
|
|
83214eaaf8 | ||
|
|
1100fdd456 | ||
|
|
481bfa5440 | ||
|
|
30282c7fbb | ||
|
|
382bc71b21 | ||
|
|
f3fba97652 | ||
|
|
7f51e35bd4 | ||
|
|
95beb8e62a | ||
|
|
1a9de867f9 | ||
|
|
b42946bbe1 | ||
|
|
40b2fd09ff | ||
|
|
a3d560a8a2 | ||
|
|
ed20fe252e | ||
|
|
375e36ff96 | ||
|
|
e7108b108e | ||
|
|
6d59daad19 | ||
|
|
21c693921b | ||
|
|
7bcd5fbed7 | ||
|
|
7104970e17 | ||
|
|
1a2950b580 | ||
|
|
085b24e1c5 | ||
|
|
8688ce6328 | ||
|
|
fbdfed81e7 | ||
|
|
94fe20607e | ||
|
|
6c62483e8e | ||
|
|
54689129c6 | ||
|
|
e9e8dd5a82 | ||
|
|
00e764b118 | ||
|
|
cee7eb970a | ||
|
|
daed17fac8 | ||
|
|
8708f4f93f | ||
|
|
c7c1bfbeba | ||
|
|
0418438b6f | ||
|
|
a2ea4d036e | ||
|
|
dc7a29908f | ||
|
|
794db5d2a4 | ||
|
|
e5f9db129b | ||
|
|
a6aecf4e9d | ||
|
|
b59bc4ec90 | ||
|
|
41920f7865 | ||
|
|
4630bf5681 | ||
|
|
1c78ebd20e | ||
|
|
80d17cfda3 | ||
|
|
a154007927 | ||
|
|
bd8274cc27 | ||
|
|
fb08991c05 | ||
|
|
7c1f06fdf7 | ||
|
|
93b38b9f95 | ||
|
|
7ffc97d301 | ||
|
|
280301f258 | ||
|
|
40daf38f80 | ||
|
|
d24925cd5f | ||
|
|
cd42d54b43 | ||
|
|
53d8ecb6bc | ||
|
|
98e87d0297 | ||
|
|
400b4af769 | ||
|
|
368701afb1 | ||
|
|
a501b89ecd | ||
|
|
91cddd72e5 | ||
|
|
8a1f0c9dbf | ||
|
|
e3e5318b4f | ||
|
|
b060664c9f | ||
|
|
83fbf0e8ac | ||
|
|
537a926618 | ||
|
|
f791a59b1d | ||
|
|
0b8e41f993 | ||
|
|
f540fa2a38 | ||
|
|
2d7bc2f34a | ||
|
|
c2dea0a4d7 | ||
|
|
42cbfbf8ed | ||
|
|
137e79b012 | ||
|
|
5849ed3ecc | ||
|
|
d3dc1e1197 | ||
|
|
c20f0bef44 | ||
|
|
c572b6b182 | ||
|
|
a1392dbf86 | ||
|
|
4e719bab5e | ||
|
|
34b51ea64a | ||
|
|
5a2a72f530 | ||
|
|
2ea80c41ab | ||
|
|
6f987958e8 | ||
|
|
ae4007aad5 | ||
|
|
c4401f8bd4 | ||
|
|
0e7472de50 | ||
|
|
e998c78609 | ||
|
|
c30b92cd38 | ||
|
|
2bf2d2aef7 | ||
|
|
cdc04b0803 | ||
|
|
5f5875acc1 | ||
|
|
d306c5e0a3 | ||
|
|
19a815cffe | ||
|
|
da0c559293 | ||
|
|
a2c91ef7b3 | ||
|
|
722b94ca32 | ||
|
|
299742fe03 | ||
|
|
3964cbf911 | ||
|
|
63e4947ad5 | ||
|
|
e3cb13a414 | ||
|
|
01fec79d78 | ||
|
|
a7043a1359 | ||
|
|
91a93ecd62 | ||
|
|
c52fdf6395 | ||
|
|
1d1dad4b30 | ||
|
|
f07a57e478 | ||
|
|
ebacd9b4b4 | ||
|
|
f010e59597 | ||
|
|
a184d7a8e0 | ||
|
|
807f54c549 | ||
|
|
24684abc1d | ||
|
|
1f1a49976c | ||
|
|
562fda3079 | ||
|
|
05642f3c14 | ||
|
|
251e2774aa | ||
|
|
2089589d34 | ||
|
|
c48b135c43 | ||
|
|
70121a6ebf | ||
|
|
c23e53585a | ||
|
|
89e964163e | ||
|
|
0357774ba6 | ||
|
|
93cf750249 | ||
|
|
b712f7a344 | ||
|
|
4159a5cbb8 | ||
|
|
2e78a291d4 | ||
|
|
3f1705c2a5 | ||
|
|
bb1f5f7059 | ||
|
|
75b7d0c419 | ||
|
|
41a6c11c55 | ||
|
|
57d908e369 | ||
|
|
64274fdb33 | ||
|
|
da919fd189 | ||
|
|
cfa25f12d3 | ||
|
|
05bc1c1263 | ||
|
|
939c79c37f | ||
|
|
d352ddeea1 | ||
|
|
72a683f2b1 | ||
|
|
784399f345 | ||
|
|
710be4371b | ||
|
|
eece358aec | ||
|
|
b43ada4f83 | ||
|
|
9030af4faf | ||
|
|
38b424b62e | ||
|
|
1d9bf0b1aa | ||
|
|
d3b7700c07 | ||
|
|
d9513e159f | ||
|
|
6ddfdf2514 | ||
|
|
478804bd5c | ||
|
|
b61165a753 | ||
|
|
b3814ae7be | ||
|
|
019c363a74 | ||
|
|
da5f80e704 | ||
|
|
b37b10e669 | ||
|
|
8ca92eda39 | ||
|
|
81dbbc36db | ||
|
|
7065101b87 | ||
|
|
00c302e545 | ||
|
|
703530ce7f | ||
|
|
7ac15042d8 | ||
|
|
a80ec52027 | ||
|
|
4da4132220 | ||
|
|
8682e66eb0 | ||
|
|
34bf205d37 | ||
|
|
d6c2c6a2c3 | ||
|
|
f45639e6e2 | ||
|
|
82968e29bf | ||
|
|
5d3d571545 | ||
|
|
6999c13877 | ||
|
|
82a551e88f | ||
|
|
1b1a0c876c | ||
|
|
b262c4a898 | ||
|
|
22d1055d82 | ||
|
|
fe38565a9a | ||
|
|
a25d14e83f | ||
|
|
15b21dd8d7 | ||
|
|
caedcde49b | ||
|
|
8091e23e00 | ||
|
|
08e1090b15 | ||
|
|
f76b5cb2eb | ||
|
|
edc4311dcb | ||
|
|
a613bff664 | ||
|
|
8f875d2a9c | ||
|
|
fb60e0b389 | ||
|
|
2199fb2828 | ||
|
|
b7d052a6b3 | ||
|
|
b333816dc8 | ||
|
|
90160da042 | ||
|
|
6f2ebf8d2d | ||
|
|
a65635365e | ||
|
|
0eee6979b0 | ||
|
|
ec796e9f84 | ||
|
|
aaed2a6d86 | ||
|
|
0ea7c500e1 | ||
|
|
d90c884cf2 | ||
|
|
93700c01a8 | ||
|
|
1df5662d4f | ||
|
|
338eeba944 | ||
|
|
9651e4abb1 | ||
|
|
ed1f3400ac | ||
|
|
e9d9353fbb | ||
|
|
00adf4ca46 | ||
|
|
870fc27c72 | ||
|
|
bd38b7479f | ||
|
|
a567599eae | ||
|
|
5e6f9353c2 | ||
|
|
7de1179b7e | ||
|
|
ea7c80c3a1 | ||
|
|
f252f757f1 | ||
|
|
b27c63d0d7 | ||
|
|
bcce1a4472 | ||
|
|
9d9655512d | ||
|
|
7af75f31e4 | ||
|
|
83f02c377f | ||
|
|
ce4f74bc61 | ||
|
|
66651d0eed | ||
|
|
ec0e143361 | ||
|
|
250e0188f7 | ||
|
|
3123e472fc | ||
|
|
c12f7f1123 | ||
|
|
7e706518c5 | ||
|
|
d8ca573983 | ||
|
|
2225625cd8 | ||
|
|
89f0f01fd2 | ||
|
|
a36282d114 | ||
|
|
a8c92b7f9a | ||
|
|
f505dac8f3 | ||
|
|
8e4730a3bd | ||
|
|
b094bb344b | ||
|
|
2685aa049d | ||
|
|
b738d57433 | ||
|
|
539b870754 | ||
|
|
abeb0998ea | ||
|
|
82faddd985 | ||
|
|
b8084c270e | ||
|
|
22c7da420c | ||
|
|
45a3c89b0b | ||
|
|
8fc9e6d1ee | ||
|
|
7f0b286d8e | ||
|
|
4f664df087 | ||
|
|
dff48e3146 | ||
|
|
0fefa19f80 | ||
|
|
88e07ddbaa | ||
|
|
44a3ef0d70 | ||
|
|
5e793f171f | ||
|
|
e9bc63bee8 | ||
|
|
5636876e42 | ||
|
|
f2f7f549b0 | ||
|
|
1fc6e4f781 | ||
|
|
d641458fb4 | ||
|
|
517d44fa3c | ||
|
|
80ee0bf9a8 | ||
|
|
0934b70414 | ||
|
|
f74168e2c7 | ||
|
|
bf4a6e6cde | ||
|
|
0e09675779 | ||
|
|
40e92ca3d2 | ||
|
|
e776919bfd | ||
|
|
84bfeffe46 | ||
|
|
1360abbecb | ||
|
|
2a13accfe4 | ||
|
|
e26dac3993 | ||
|
|
1b7a43e82b | ||
|
|
141aca9e25 | ||
|
|
4f99eb6f07 | ||
|
|
81075bb000 | ||
|
|
33057faaab | ||
|
|
28b831c6a2 | ||
|
|
ef4d3d2659 | ||
|
|
09c0c18fce | ||
|
|
ff80150216 | ||
|
|
a8203baa50 | ||
|
|
aa33dc83d4 | ||
|
|
4155e2bb64 | ||
|
|
9660cafa99 | ||
|
|
c55b9cfe96 | ||
|
|
78ea96767e | ||
|
|
d11d7a8ffc | ||
|
|
bec789d2fb | ||
|
|
0cda3fca31 | ||
|
|
4f8980184f | ||
|
|
ffa096d988 | ||
|
|
6e1b1ed9d5 | ||
|
|
48526b815e | ||
|
|
1ded893e7b | ||
|
|
c61bd01c0f | ||
|
|
f0adcc90c7 | ||
|
|
2abb13bb4c | ||
|
|
8f69c4c820 | ||
|
|
5652c52d96 | ||
|
|
ff29cc192e | ||
|
|
7428d0e734 | ||
|
|
caad9e999c | ||
|
|
24cb225381 | ||
|
|
2e3195c5ee | ||
|
|
bfa039a612 | ||
|
|
49f8988912 | ||
|
|
7e214dbe3b | ||
|
|
42ad12d8d8 | ||
|
|
842e6ef788 | ||
|
|
56b87039c2 | ||
|
|
1767a0889d | ||
|
|
3036366de5 | ||
|
|
a8f1031e0f | ||
|
|
054107c3b9 | ||
|
|
c478b22ab9 | ||
|
|
2f712499ea | ||
|
|
39b9622cdb | ||
|
|
59a3a68357 | ||
|
|
d920dbd79e | ||
|
|
49dd390c6b | ||
|
|
d20b4bc334 | ||
|
|
a973807e3e | ||
|
|
431b5f4f30 | ||
|
|
636799e567 | ||
|
|
d003e3fa1b | ||
|
|
07edbe6619 | ||
|
|
dc4a5a05fe | ||
|
|
a8c86eb53d | ||
|
|
8ef9a62dc9 | ||
|
|
29c9ed4135 | ||
|
|
0426a4ca0d | ||
|
|
af7bbe3cca | ||
|
|
3c55153752 | ||
|
|
a82ff4bb4e | ||
|
|
73759e9611 | ||
|
|
71d6d08f5a | ||
|
|
2f2be201a7 | ||
|
|
49aaca7172 | ||
|
|
f29c64984b | ||
|
|
a50329edf3 | ||
|
|
15d163abf4 | ||
|
|
852a116c9d | ||
|
|
e9c18d0c01 | ||
|
|
5e6d4ecb1c | ||
|
|
97635311db | ||
|
|
3c7ddfd236 | ||
|
|
e0aa0eb4a3 | ||
|
|
8eba9d252f | ||
|
|
5966680a31 | ||
|
|
eb1563b1e7 | ||
|
|
84f52668b7 | ||
|
|
cc60095344 | ||
|
|
5a1f237b30 | ||
|
|
934a671344 | ||
|
|
b81ea8e8c7 | ||
|
|
817920940e | ||
|
|
e0f7ebbcba | ||
|
|
8ce18960fe | ||
|
|
6a879927a7 | ||
|
|
9c22114aa5 | ||
|
|
add9bef046 | ||
|
|
64b6cfea93 | ||
|
|
c8e76d5727 | ||
|
|
4fda0b6aaa | ||
|
|
4634237879 | ||
|
|
5cc91fef53 | ||
|
|
4a3c408ec5 | ||
|
|
23f0c55053 | ||
|
|
e1cf21328a | ||
|
|
7ce5f982b3 | ||
|
|
8fb9439eff | ||
|
|
700e348dbd | ||
|
|
91a86a8663 | ||
|
|
293270c03d | ||
|
|
af13113161 | ||
|
|
59b9dca5ec | ||
|
|
bc5d4e8efb | ||
|
|
91111a62f6 | ||
|
|
5525324620 | ||
|
|
29bd632e45 | ||
|
|
369e3c7269 | ||
|
|
383fed7b3d | ||
|
|
849dd8d436 | ||
|
|
6340a35cf2 | ||
|
|
1b2ea1d4bd | ||
|
|
9087872bc2 | ||
|
|
d914e06f42 | ||
|
|
e12af0c870 | ||
|
|
b0b9e2a7de | ||
|
|
5b88d8b9ca | ||
|
|
b7010b099f | ||
|
|
5484d39d90 | ||
|
|
181a2f8949 | ||
|
|
cee0f97850 | ||
|
|
fd91388b7a | ||
|
|
beae08a99d | ||
|
|
aef614aeae | ||
|
|
c87bc39ad8 | ||
|
|
d0b78f8e81 | ||
|
|
f126cc1d6c | ||
|
|
2d8c3427c4 | ||
|
|
c2acd926af | ||
|
|
b904d77497 | ||
|
|
c093b92c0b | ||
|
|
734b8bfd40 | ||
|
|
e337d1f116 | ||
|
|
8434ac1e2f | ||
|
|
349cdbf582 | ||
|
|
693aeae9e5 | ||
|
|
e4ea8e156d | ||
|
|
ca2b7dd674 | ||
|
|
8a744aa7fc | ||
|
|
7f2beb4d80 | ||
|
|
103c421b31 | ||
|
|
0f4238e9a7 | ||
|
|
c9508d2dac | ||
|
|
5d293b4318 | ||
|
|
3f4b814c0b | ||
|
|
aca71d8db1 | ||
|
|
87dbe3c945 | ||
|
|
c254fc946f | ||
|
|
5a4718eae8 | ||
|
|
935c52f291 | ||
|
|
04fe93d3b8 | ||
|
|
22f279e309 | ||
|
|
a0cff87e5f | ||
|
|
943d327975 | ||
|
|
6c4aced95e | ||
|
|
ad80fd2a91 | ||
|
|
43d50734a4 | ||
|
|
52d6057365 | ||
|
|
8dd5a5dd8a | ||
|
|
4799b33e0e | ||
|
|
d25ae7de81 | ||
|
|
83cbbbf0b7 | ||
|
|
4794578688 | ||
|
|
446da392d9 | ||
|
|
07cbd4cdbb | ||
|
|
f17cbdc111 | ||
|
|
28c5d277a0 | ||
|
|
939840b702 | ||
|
|
78485ae4e9 | ||
|
|
cc47a93872 | ||
|
|
af122eeb5c | ||
|
|
ec118968ed | ||
|
|
2de416fe81 | ||
|
|
d10a3b91e3 | ||
|
|
cdadf68f30 | ||
|
|
900a123141 | ||
|
|
4b2f9488ce | ||
|
|
02d7f45988 | ||
|
|
cc34dbb88e | ||
|
|
5e9d99083c | ||
|
|
295bf74a1b | ||
|
|
7928437dc6 | ||
|
|
83283b7b6b | ||
|
|
09a289b4c4 | ||
|
|
6b940b9d01 | ||
|
|
cb492e0183 | ||
|
|
92799d57ae | ||
|
|
066100f218 | ||
|
|
cd4dd44004 | ||
|
|
40a2fdb7fd | ||
|
|
b60cf11668 | ||
|
|
0fa617c580 | ||
|
|
1cfa08612e | ||
|
|
2cebef9d4b | ||
|
|
c75313cdf4 | ||
|
|
ae1eaac037 | ||
|
|
01465a898a | ||
|
|
d6a7917ffd | ||
|
|
d9946088ab | ||
|
|
a2ad6a1037 | ||
|
|
34e240b40a | ||
|
|
ca1f33ade6 | ||
|
|
d8bddb1c21 | ||
|
|
64bab14483 | ||
|
|
2d1830f4fc | ||
|
|
ab00f2bd42 | ||
|
|
6d9505a4c0 | ||
|
|
40e3cb8ce5 | ||
|
|
4783ec6696 | ||
|
|
3bd746fe91 | ||
|
|
a91a82eecc | ||
|
|
a9564583cb | ||
|
|
f988c8879e | ||
|
|
825cad81a2 | ||
|
|
b9c0ea065a | ||
|
|
50ef633573 | ||
|
|
10202df7d7 | ||
|
|
a7815b41db | ||
|
|
cf467eb868 | ||
|
|
a3be19154f | ||
|
|
a7c19c689c | ||
|
|
1c78e3aac0 | ||
|
|
b9cc3d77b3 | ||
|
|
1feb81adf3 | ||
|
|
fd937758e6 | ||
|
|
fcc3d674c2 | ||
|
|
ce74264a01 | ||
|
|
aaf6448563 | ||
|
|
4a696635f5 | ||
|
|
beb14befca | ||
|
|
c91703364d | ||
|
|
597cea17cd | ||
|
|
9585f6c598 | ||
|
|
e356fe3e85 | ||
|
|
55e5b86ec4 | ||
|
|
bf29a56aeb | ||
|
|
07c57d4197 | ||
|
|
146db31cb5 | ||
|
|
14239fcd47 | ||
|
|
8dc6a17295 | ||
|
|
76f9a6c746 | ||
|
|
eb155a5690 | ||
|
|
b78575aa8f | ||
|
|
91a5cd5c69 | ||
|
|
dd3e6420b6 | ||
|
|
d6a65861e0 | ||
|
|
fe77ff3f60 | ||
|
|
326cccd525 | ||
|
|
b41ca0f0be | ||
|
|
02fa092775 | ||
|
|
57860dc5a6 | ||
|
|
e28f2fb8cd | ||
|
|
0423dd4069 | ||
|
|
5e38137916 | ||
|
|
dc90fb9c94 | ||
|
|
2e2575c360 | ||
|
|
2732abbc93 | ||
|
|
e9d5d676a5 | ||
|
|
18bab4044e | ||
|
|
2ccc4a6932 | ||
|
|
d01d02e700 | ||
|
|
56c6f6cabe | ||
|
|
adb1e58937 | ||
|
|
a59c893652 | ||
|
|
93bcd5f43b | ||
|
|
d7453a7841 | ||
|
|
4fe3dc052a | ||
|
|
c88c755785 | ||
|
|
31f83d33f5 | ||
|
|
597256d048 | ||
|
|
62594a2898 | ||
|
|
00582d486c | ||
|
|
cda626b01c | ||
|
|
7d84da1520 | ||
|
|
11b96b488f | ||
|
|
1853c0ca32 | ||
|
|
0b8fb177c4 | ||
|
|
4e80434956 | ||
|
|
c2f53577ab | ||
|
|
4974150357 | ||
|
|
1586d97295 | ||
|
|
5f65898c33 | ||
|
|
88e7941db3 | ||
|
|
6c715263e0 | ||
|
|
7088962d44 | ||
|
|
429bb0957d | ||
|
|
424fda55dd | ||
|
|
1b26a11281 | ||
|
|
56f52c8623 | ||
|
|
908edff878 | ||
|
|
487e1dc4c1 | ||
|
|
244398e096 | ||
|
|
fafd9e2bd8 | ||
|
|
367ea4df39 | ||
|
|
630abbd0fc | ||
|
|
fe20428a14 | ||
|
|
0e36681ec1 | ||
|
|
884cbc52a3 | ||
|
|
88c17af8ef | ||
|
|
549670e45f | ||
|
|
4fa0e58e80 | ||
|
|
d60b9b2b47 | ||
|
|
3368bd3879 | ||
|
|
dbc47c5420 | ||
|
|
f86b5a2bf3 | ||
|
|
0e0c126726 | ||
|
|
45e0e57668 | ||
|
|
7ee1edbab8 | ||
|
|
747ad9f29a | ||
|
|
7e128dc6c3 | ||
|
|
5e1352077a | ||
|
|
22fc54b2fa | ||
|
|
b67e068991 | ||
|
|
40f5bb07d8 | ||
|
|
c1063d1967 | ||
|
|
964cd19949 | ||
|
|
f55305a800 | ||
|
|
8392856ec5 | ||
|
|
01e1551838 | ||
|
|
d3f042433d | ||
|
|
0e5635cc2a | ||
|
|
73677544a3 | ||
|
|
7c46d8548e | ||
|
|
186381426a | ||
|
|
af1e695661 | ||
|
|
4ccd51269a | ||
|
|
560cfe225f | ||
|
|
e9e4c3d333 | ||
|
|
dbca6e3b88 | ||
|
|
ad465ed20c | ||
|
|
9370f7ce15 | ||
|
|
d9151a866b | ||
|
|
7937fd00d4 | ||
|
|
d2199a5b9c | ||
|
|
6e765325c1 | ||
|
|
18119b3d64 | ||
|
|
378a7c2d6c | ||
|
|
1270a315b2 | ||
|
|
931b2cc700 | ||
|
|
e145ac0ad1 | ||
|
|
ab8e882e94 | ||
|
|
b66d671b74 | ||
|
|
f662a13778 | ||
|
|
845aa122e1 | ||
|
|
bb19336d06 | ||
|
|
774948cf9d | ||
|
|
e26e077c83 | ||
|
|
f264ffd040 | ||
|
|
7e16e4880b | ||
|
|
dd1ee6ff44 | ||
|
|
90d628cc75 | ||
|
|
d5a0b33f04 | ||
|
|
470e7826f1 | ||
|
|
54e9ae568f | ||
|
|
d2ae5173fc | ||
|
|
e9b5133151 | ||
|
|
7959d35f3f | ||
|
|
b42dfb2021 | ||
|
|
b68b773b95 | ||
|
|
e89a926d53 | ||
|
|
6132c03893 | ||
|
|
2db54fc67a | ||
|
|
96095453d5 | ||
|
|
32c02c36c9 | ||
|
|
1eb7ce3896 | ||
|
|
da4f29049b | ||
|
|
d46dd46732 | ||
|
|
8eb72ae6e7 | ||
|
|
6a421d3b78 | ||
|
|
f71a14126e | ||
|
|
35c2024eec | ||
|
|
e570341f93 | ||
|
|
45a9f97fc8 | ||
|
|
6238a4c127 | ||
|
|
e38ec13dac | ||
|
|
0268858784 | ||
|
|
0bd4eefeca | ||
|
|
216f2920b9 | ||
|
|
1fe3e69c56 | ||
|
|
f301dac442 | ||
|
|
dae5cff728 | ||
|
|
f605afb647 | ||
|
|
ed832956ea | ||
|
|
402ff9e8d0 | ||
|
|
2fc51dba17 | ||
|
|
fd8358af90 | ||
|
|
4cd835577a | ||
|
|
bc1c11e650 | ||
|
|
765f432ef2 | ||
|
|
4cd92a1372 | ||
|
|
a2b3975c12 | ||
|
|
4b6cca8dd8 | ||
|
|
55b43b6bc0 | ||
|
|
c987861f02 | ||
|
|
f92c4d18db | ||
|
|
eeebf56a78 | ||
|
|
a04f231c9e | ||
|
|
7df2293f1b | ||
|
|
7dfdf4cdbd | ||
|
|
62fa5fef79 | ||
|
|
ce9fe17994 | ||
|
|
ff54449d1d | ||
|
|
43a8900c24 | ||
|
|
e1660aa909 | ||
|
|
5f6306911f | ||
|
|
268d07938a | ||
|
|
42a4ce5006 | ||
|
|
251b0a0a93 | ||
|
|
153aca30dc | ||
|
|
d9b9aa7de4 | ||
|
|
003d41a496 | ||
|
|
18bc459850 | ||
|
|
97f6781d8a | ||
|
|
7a33c5e18c | ||
|
|
971e2ff76a | ||
|
|
007a378f2b | ||
|
|
2f02148e36 | ||
|
|
475fb4fa2e | ||
|
|
f3d2ef86f8 | ||
|
|
35d2fd4cbc | ||
|
|
c4f1a7eb70 | ||
|
|
c83430a537 | ||
|
|
c398d30f37 | ||
|
|
f60246846f | ||
|
|
3184de1392 | ||
|
|
921324d968 | ||
|
|
c74cdeb773 | ||
|
|
64ecf51ad9 | ||
|
|
518ad04815 | ||
|
|
12ca54f6ba | ||
|
|
0a0ca9ef03 | ||
|
|
9ef7b2f80a | ||
|
|
86b0ed0a04 | ||
|
|
65e77e07a8 | ||
|
|
309308ed59 | ||
|
|
bb82ca0557 | ||
|
|
e5e5db335d | ||
|
|
490d0a7815 | ||
|
|
7ff7c71b4e | ||
|
|
13fa01c4e2 | ||
|
|
d3dfcc3248 | ||
|
|
69d57b7a13 | ||
|
|
b4959547a3 | ||
|
|
3c7085f073 | ||
|
|
0ffb2ab7a7 | ||
|
|
fa4f18b59e | ||
|
|
bdbe034c13 | ||
|
|
b677e8b4b2 | ||
|
|
68745703f8 | ||
|
|
615d571aef | ||
|
|
d23003ab0c | ||
|
|
c29fc410ad | ||
|
|
cbdaa143ea | ||
|
|
ff92cb53cc | ||
|
|
3890af9e1a | ||
|
|
21d70bbcb2 | ||
|
|
1f80e029b8 | ||
|
|
9372b87d5b | ||
|
|
be3f886a57 | ||
|
|
896d7a045a | ||
|
|
545c9ea8dd | ||
|
|
ae1c658065 | ||
|
|
ebea409db6 | ||
|
|
f406fa2445 | ||
|
|
97784c92cf | ||
|
|
8c59241abb | ||
|
|
73bfd6abaa | ||
|
|
b4ccc83696 | ||
|
|
1b557d9769 | ||
|
|
1f69f55437 | ||
|
|
2c049dc38e | ||
|
|
276c14f507 | ||
|
|
117538754e | ||
|
|
f0c22e32df | ||
|
|
30d480debc | ||
|
|
d8bbf71c19 | ||
|
|
574a29363c | ||
|
|
c3382d1501 | ||
|
|
d5058d153e | ||
|
|
9b64bf422d | ||
|
|
da90239e2b | ||
|
|
e15a93ebcb | ||
|
|
286f512f40 | ||
|
|
ff10649a21 | ||
|
|
923c74b8f0 | ||
|
|
95a92aec8f | ||
|
|
9894f5c7fb | ||
|
|
b54a3959d9 | ||
|
|
ee92a56ba9 | ||
|
|
65bbe9ffe4 | ||
|
|
d144ce3135 | ||
|
|
a54a29a3ac | ||
|
|
8a18df0e7f | ||
|
|
0d0e867ef6 | ||
|
|
15a16135e3 | ||
|
|
4444974a9e | ||
|
|
1a32f2a6f8 | ||
|
|
ff43f8474e | ||
|
|
7577e4385c | ||
|
|
0feee6e007 | ||
|
|
d78d68b4da | ||
|
|
e7eea5b9d2 | ||
|
|
0256a3f267 | ||
|
|
a13fef6237 | ||
|
|
357c295fec | ||
|
|
a7a7bc3ebe | ||
|
|
5d02d73737 | ||
|
|
4213b4739e | ||
|
|
b41a6b1d60 | ||
|
|
587fbadd7c | ||
|
|
9e2e0d9bb8 | ||
|
|
24282e4289 | ||
|
|
8659df3c4c | ||
|
|
9913014c4c | ||
|
|
04daf6f0bb | ||
|
|
a9917432d4 | ||
|
|
c23cfd121e | ||
|
|
11efa4fc9e | ||
|
|
1d0c463e3b | ||
|
|
87f9d8f8c3 | ||
|
|
3904177d16 | ||
|
|
9910bb5dc7 | ||
|
|
e1d76a93c9 | ||
|
|
6a5807e94b | ||
|
|
6e9cbdd898 | ||
|
|
ed5f743422 | ||
|
|
e3abe13def | ||
|
|
e8325c13de | ||
|
|
ff55b452eb | ||
|
|
62a0a064aa | ||
|
|
8d5c8f33f2 | ||
|
|
c1e7af620f | ||
|
|
9e0641d8e1 | ||
|
|
b5d07cf5dc | ||
|
|
e8d333a46b | ||
|
|
85f8a012c7 | ||
|
|
aeaa421de6 | ||
|
|
0f8bf26746 | ||
|
|
ee89aa649a | ||
|
|
8cc401a5bf | ||
|
|
c69934e10c | ||
|
|
152d856b24 | ||
|
|
0541d21364 | ||
|
|
47bdad65c2 | ||
|
|
8fdc7839fb | ||
|
|
69905abb9f | ||
|
|
dfa80244ce | ||
|
|
601b5fd57d | ||
|
|
822ba6051c | ||
|
|
c827f193f2 | ||
|
|
dfd755c0da | ||
|
|
72b63c4339 | ||
|
|
52e3b0ee8e | ||
|
|
41521f4c04 | ||
|
|
f35067c9ba | ||
|
|
4cd538e8c1 | ||
|
|
9d0de5df22 | ||
|
|
19e9e9e287 | ||
|
|
cd450a48e6 | ||
|
|
cd1ca91b7f | ||
|
|
9fd0562f98 | ||
|
|
89a4711a77 | ||
|
|
6a7c3a60ec | ||
|
|
0ee3f11345 | ||
|
|
773311439f | ||
|
|
6d2ee67536 | ||
|
|
b90f291998 | ||
|
|
d34034065f | ||
|
|
ae425d25ec | ||
|
|
3c98fc460a | ||
|
|
7725d32788 | ||
|
|
bbd4dd736e | ||
|
|
a1967bc706 | ||
|
|
d06a38d09e | ||
|
|
5d8b06b239 | ||
|
|
2caa782b83 | ||
|
|
f085dac4a0 | ||
|
|
5092e3fc65 | ||
|
|
0f0cb0f28d | ||
|
|
47cfdedd1f | ||
|
|
6347e1f779 | ||
|
|
0afffb4ee2 | ||
|
|
ccc5b2ac44 | ||
|
|
b52ea80ab8 | ||
|
|
86d478f25c | ||
|
|
07e5cfef93 | ||
|
|
f644727e1c | ||
|
|
a00c20296e | ||
|
|
8968833003 | ||
|
|
239fcba631 | ||
|
|
bc555b75dc | ||
|
|
30f72975f7 | ||
|
|
3acf6e50a7 | ||
|
|
34567480a2 | ||
|
|
4323140799 | ||
|
|
df04318366 | ||
|
|
6a27e4388c | ||
|
|
71da849ba9 | ||
|
|
4fb73e6073 | ||
|
|
1f3f23a4f8 | ||
|
|
64554f51a6 | ||
|
|
12c0b33cf1 | ||
|
|
bb5eab886d | ||
|
|
af48f5de3f | ||
|
|
6c002bb135 | ||
|
|
ca77243f4f | ||
|
|
7cbe37033d | ||
|
|
08e790ebbb | ||
|
|
75eec07207 | ||
|
|
22d841144c | ||
|
|
8a9a08a2d2 | ||
|
|
497251186d | ||
|
|
2c0a005d3e | ||
|
|
bea7981bdb | ||
|
|
0be104fd02 | ||
|
|
ea1cdd553c | ||
|
|
6a14964f8f | ||
|
|
cb9f5eab14 | ||
|
|
4ad972f7fe | ||
|
|
3925d6a467 | ||
|
|
6fcc59169c | ||
|
|
02eebf186c | ||
|
|
5b7d0fd3b8 | ||
|
|
aa4e72844b | ||
|
|
f9617a101b | ||
|
|
c641596d42 | ||
|
|
d389b8ad14 | ||
|
|
6248958c94 | ||
|
|
e0e4c46329 | ||
|
|
fdca4c2822 | ||
|
|
cbc6eeabda | ||
|
|
dea2958d9d | ||
|
|
a0c410be0e | ||
|
|
4cc1a97a0a | ||
|
|
ae6ac851c7 | ||
|
|
0f39b991df | ||
|
|
6e9068c952 | ||
|
|
891e5967db | ||
|
|
542f21d58d | ||
|
|
1af3f9f196 | ||
|
|
f0ca2e2601 | ||
|
|
84f0296917 | ||
|
|
96ad8c15c6 | ||
|
|
d77d45b5bc | ||
|
|
dc788a68f8 | ||
|
|
d3af49972c | ||
|
|
c7a732a61e | ||
|
|
d28a412204 | ||
|
|
2722d97a7d | ||
|
|
79c62d86cc | ||
|
|
9f1dcc4c9f | ||
|
|
4230f49bd9 | ||
|
|
9fe9171798 | ||
|
|
dac3b79f4d | ||
|
|
083c2f4e9b | ||
|
|
17f88eb4e7 | ||
|
|
2798e89925 | ||
|
|
79db8f2df3 | ||
|
|
089aadd729 | ||
|
|
201e37d185 | ||
|
|
9d6b569ddb | ||
|
|
c7b026fd1d | ||
|
|
66b95abb96 | ||
|
|
e2ef8a293d | ||
|
|
b25f6e041c | ||
|
|
4607580e6f | ||
|
|
f2d48c0e8f | ||
|
|
c9d12e21d8 | ||
|
|
2f4764a3f2 | ||
|
|
8b67039fa6 | ||
|
|
8cf8d51c79 | ||
|
|
90226c6981 | ||
|
|
91a248bdbe | ||
|
|
0d3bfacc84 | ||
|
|
ac3c156a0b | ||
|
|
f24d403705 | ||
|
|
da3b16c0d4 | ||
|
|
2b2cb03784 | ||
|
|
19d0ff3d46 | ||
|
|
04b6b0ad76 | ||
|
|
6a5fb33306 | ||
|
|
d0262ea6ae | ||
|
|
512c2ee000 | ||
|
|
e708e885f6 | ||
|
|
cc3b7b8124 | ||
|
|
39c9deb456 | ||
|
|
7991790f94 | ||
|
|
5f418d3f1a | ||
|
|
5047bf5466 | ||
|
|
dca7c26b9d | ||
|
|
c6173e2957 | ||
|
|
b646023c41 | ||
|
|
45e3b01b15 | ||
|
|
97515ab758 | ||
|
|
d85328e729 | ||
|
|
0aa7082391 | ||
|
|
946431b83f | ||
|
|
a101e3e7a6 | ||
|
|
f73120dbaa | ||
|
|
a55879c93a | ||
|
|
fe02af151d | ||
|
|
f1a963fa9c | ||
|
|
8c6dee4213 | ||
|
|
9208b4c4dd | ||
|
|
0e04b34852 | ||
|
|
796e35e8a4 | ||
|
|
72f0ae906f | ||
|
|
464482d197 | ||
|
|
48ce39a645 | ||
|
|
f993240d2b | ||
|
|
83fd9babef | ||
|
|
66fb0cf8fc | ||
|
|
459ac84d29 | ||
|
|
736b833d52 | ||
|
|
dc37020d73 | ||
|
|
93916d4ed1 | ||
|
|
1d1e48acb7 | ||
|
|
1884e1a111 | ||
|
|
2d0396da21 | ||
|
|
cce47ba723 | ||
|
|
2831680d14 | ||
|
|
5e3374acbc | ||
|
|
13d7de9501 | ||
|
|
d78abc92f2 | ||
|
|
07672eb874 | ||
|
|
e7225ce487 | ||
|
|
cb24d3bf78 | ||
|
|
8360019080 | ||
|
|
f3b34bea26 | ||
|
|
edf09a2d7b | ||
|
|
1de445c5b8 | ||
|
|
8d7f307173 | ||
|
|
0a23a6d084 | ||
|
|
87fa70be2c | ||
|
|
b57c62fe1b | ||
|
|
a8c1051e0f | ||
|
|
d7ba12e729 | ||
|
|
1d3c47f3fd | ||
|
|
4ae81bae99 | ||
|
|
2fc301d061 | ||
|
|
fc6e6d1ab6 | ||
|
|
9cb4ee9d6f | ||
|
|
0e1da6982b | ||
|
|
28573b47a8 | ||
|
|
851bd1ef14 | ||
|
|
0c7d64563d | ||
|
|
b984f62bbf | ||
|
|
a89e0936c2 | ||
|
|
677146d905 | ||
|
|
00b7ead8bb | ||
|
|
dce5016261 | ||
|
|
2bc759778c | ||
|
|
3aa6869a4b | ||
|
|
209fdfd5b9 | ||
|
|
eec0df14b5 | ||
|
|
7e2810d33d | ||
|
|
78404c8cd3 | ||
|
|
f05ceecf8e | ||
|
|
bcef526213 | ||
|
|
00d5767246 | ||
|
|
6655301bfe | ||
|
|
5beff97f95 | ||
|
|
cfa6b49bab | ||
|
|
a659d5fada | ||
|
|
c1521bfa3f | ||
|
|
096e6c911a | ||
|
|
26f7cd38e5 | ||
|
|
802541c09f | ||
|
|
cfd36c2836 | ||
|
|
7689ac7bed | ||
|
|
7a5ba99b36 | ||
|
|
8f4a40bc9a | ||
|
|
e3ab846d70 | ||
|
|
29db574bc5 | ||
|
|
4851d5b62f | ||
|
|
caef16bdee | ||
|
|
1a0f9ab66a | ||
|
|
021c3bfb13 | ||
|
|
b3dfa41df6 | ||
|
|
5f94263db2 | ||
|
|
68d8e46b4c | ||
|
|
ed221b0d7b | ||
|
|
1243563cd4 | ||
|
|
1170457a39 | ||
|
|
435ed9f568 | ||
|
|
81884e48d0 | ||
|
|
a7be6d233b | ||
|
|
584ddba1a5 | ||
|
|
2bc6c8bca0 | ||
|
|
fc1e81a01d | ||
|
|
eebfaaf373 | ||
|
|
652223d9bc | ||
|
|
e75664fd2e | ||
|
|
556278b216 | ||
|
|
f9bfaa98bb | ||
|
|
b6bd2da6ce | ||
|
|
7c36a6b601 | ||
|
|
413924b11a | ||
|
|
251883dae5 | ||
|
|
7e4d0da8fb | ||
|
|
3fc2aeed4d | ||
|
|
7f4f785f0b | ||
|
|
9f5920989d | ||
|
|
62bceb30c5 | ||
|
|
8c736e52ac | ||
|
|
2669079a31 | ||
|
|
cbfd93e440 | ||
|
|
ffd1c297e5 | ||
|
|
1a11cb04f9 | ||
|
|
438fd5b59a | ||
|
|
2fef5e2cfa | ||
|
|
db8ad38fd3 | ||
|
|
cbb2722291 | ||
|
|
be1c7f2167 | ||
|
|
61f4a137b0 | ||
|
|
dac9e91428 | ||
|
|
83c64f1f71 | ||
|
|
443f4e707b | ||
|
|
70bf2a05f3 | ||
|
|
33a4747677 | ||
|
|
a4cce17767 | ||
|
|
67cd03d3f1 | ||
|
|
1f88c18f94 | ||
|
|
d2fc706b17 | ||
|
|
b5e5786813 | ||
|
|
38e741c788 | ||
|
|
e9a0b85682 | ||
|
|
5ab3602c2a | ||
|
|
dd4bf7b144 | ||
|
|
922326c5ce | ||
|
|
c69be414ca | ||
|
|
10bc47402c | ||
|
|
193e42cf22 | ||
|
|
e0f58e5264 | ||
|
|
f14e48320c | ||
|
|
474fcd33a6 | ||
|
|
a2a9ffc895 | ||
|
|
b02416b32c | ||
|
|
fa52d9e89e | ||
|
|
6383aa594a | ||
|
|
54eb59c27b | ||
|
|
3877f8309b | ||
|
|
d8b0681831 | ||
|
|
cd8303dbea | ||
|
|
ab51d6e931 | ||
|
|
b7e402dca2 | ||
|
|
842040a8b3 | ||
|
|
1205da5d34 | ||
|
|
f40824fedd | ||
|
|
67fa0cbc61 | ||
|
|
e029c77f76 | ||
|
|
2b23ae4e67 | ||
|
|
c8ecc23c9c | ||
|
|
94f8959879 | ||
|
|
2cdb8eb44d | ||
|
|
ebc1d1ecb3 | ||
|
|
d9e99334d2 | ||
|
|
a06776bbbd | ||
|
|
aecd725a71 | ||
|
|
ad8e9364e1 | ||
|
|
b812fef1c3 | ||
|
|
56371214b0 | ||
|
|
3669a86f41 | ||
|
|
4095bf63ef | ||
|
|
d75321ca8a | ||
|
|
c931e1cdd7 | ||
|
|
ea8cda72c7 | ||
|
|
f0bcd7888a | ||
|
|
6a08a66221 | ||
|
|
5e58edf598 | ||
|
|
1e79772ec1 | ||
|
|
3461ce053f | ||
|
|
9c84ce30e8 | ||
|
|
93ca63e133 | ||
|
|
e367c86fce | ||
|
|
34dc12994a | ||
|
|
df4de5ce4b | ||
|
|
ea630480b2 | ||
|
|
db1159cd0d | ||
|
|
ccf1dc0585 | ||
|
|
1cb96cf057 | ||
|
|
2bc8a114c1 | ||
|
|
7f183b9edc | ||
|
|
f86be17834 | ||
|
|
6854e3729a | ||
|
|
5f48b1e16f | ||
|
|
aea92be8d3 | ||
|
|
b4c5d6c626 | ||
|
|
0e2845082b | ||
|
|
705a40d035 | ||
|
|
f85a072708 | ||
|
|
e990387660 | ||
|
|
feb76f504c | ||
|
|
6a33173a49 | ||
|
|
5e8e9765fa | ||
|
|
2a6cdb9b14 | ||
|
|
642c0fa216 | ||
|
|
9d5e79725c | ||
|
|
db301d2635 | ||
|
|
d7283d17e2 | ||
|
|
bd7b58ad43 | ||
|
|
27c0e5f8a6 | ||
|
|
7a439a3e07 | ||
|
|
fff6e6717f | ||
|
|
2671764e99 | ||
|
|
4f12e2affb | ||
|
|
e1faab524b | ||
|
|
6c5585d059 | ||
|
|
dc678dd510 | ||
|
|
7b70c5a745 | ||
|
|
2e8190ce29 | ||
|
|
14b09b91df | ||
|
|
b77d7eaadc | ||
|
|
c68e0bfbc4 | ||
|
|
8a26e99dfc | ||
|
|
9e2dd11617 | ||
|
|
19f01007f4 | ||
|
|
2c4bbbbbfb | ||
|
|
71536c50a2 | ||
|
|
1d118a9ca3 | ||
|
|
94f6c45291 | ||
|
|
fd4a64f6a7 | ||
|
|
d8ba15ab98 | ||
|
|
a0a2e1359e | ||
|
|
34b32da1e6 | ||
|
|
f9af688bea | ||
|
|
8829bc3a65 | ||
|
|
59d8e5a853 | ||
|
|
fddd77e87c | ||
|
|
eed3fb1ed9 | ||
|
|
fbaf9272a8 | ||
|
|
97df7c75b1 | ||
|
|
358892869b | ||
|
|
7cca371c22 | ||
|
|
8b1c8b36ce | ||
|
|
86154adc92 | ||
|
|
bfc0b57f62 | ||
|
|
d67110e771 | ||
|
|
2d7c382c2f | ||
|
|
4536cd13ef | ||
|
|
627db97b30 | ||
|
|
bbdce9536b | ||
|
|
5f0644d924 | ||
|
|
0b5dffc5a5 | ||
|
|
33c85e9ed6 | ||
|
|
c04164e47a | ||
|
|
06e7545f33 | ||
|
|
0399d96fd4 | ||
|
|
347688ac8c | ||
|
|
37ce64acb7 | ||
|
|
4b8bc4e7ea | ||
|
|
d9ba83217f | ||
|
|
1d658ca1ac | ||
|
|
65f94ff465 | ||
|
|
1b010fbd07 | ||
|
|
a1bce42387 | ||
|
|
4808f18ca0 | ||
|
|
b41baf19b4 | ||
|
|
9d78fa8825 | ||
|
|
2a6a424ce0 | ||
|
|
c05c040241 | ||
|
|
3d3f2b535b | ||
|
|
49bf1f675a | ||
|
|
50f3ac493c | ||
|
|
690302f2b3 | ||
|
|
af7b387f94 | ||
|
|
1c38c47e4a | ||
|
|
74525efc37 | ||
|
|
985db74216 | ||
|
|
6d4a34d971 | ||
|
|
43b17cce0e | ||
|
|
0fc11de8bf | ||
|
|
39f5b1dbc2 | ||
|
|
f3596856ba | ||
|
|
fc7e8b4deb | ||
|
|
4178b4a61e | ||
|
|
05d5fdde53 | ||
|
|
3be1e97863 | ||
|
|
0cffb8202d | ||
|
|
657ce1cbbf | ||
|
|
f8ee80be19 | ||
|
|
70de8b8719 | ||
|
|
f972f0b8e4 | ||
|
|
8c025758c9 | ||
|
|
2713bd9bfc | ||
|
|
cdc067c4b7 | ||
|
|
a18ab16d9d | ||
|
|
7bdde3ec68 | ||
|
|
2c89f988a1 | ||
|
|
ead7c7403a | ||
|
|
5ce67e5f5c | ||
|
|
b34b2d8e2a | ||
|
|
64d1dec3d8 | ||
|
|
7f14bbcc22 | ||
|
|
9396cdef4a | ||
|
|
b49f759ef9 | ||
|
|
3087e0cbdb | ||
|
|
9992eaad2b | ||
|
|
33f5e74e45 | ||
|
|
9a068f02da | ||
|
|
b71a4f1d0b | ||
|
|
42950550cc | ||
|
|
86842bbb02 | ||
|
|
4ca0277e63 | ||
|
|
671ac52201 | ||
|
|
78c71babda | ||
|
|
19196a2ee4 | ||
|
|
843c5f0687 | ||
|
|
260160ade1 | ||
|
|
06525ff690 | ||
|
|
ac2ccc2d03 | ||
|
|
41eba0c873 | ||
|
|
f6672496d1 | ||
|
|
004b51f3cd | ||
|
|
1046716304 | ||
|
|
187b7a8c39 | ||
|
|
27177e3ef4 | ||
|
|
367eaae47f | ||
|
|
dcee433547 | ||
|
|
ea74e24024 | ||
|
|
6353ddf58a | ||
|
|
911d83cfd8 | ||
|
|
49fc1c4f7e | ||
|
|
097eb07fcc | ||
|
|
00f992259b | ||
|
|
ce655173f8 | ||
|
|
ec1c146362 | ||
|
|
4098a9b70f | ||
|
|
b617d5cab0 | ||
|
|
4b29fda924 | ||
|
|
37d07c9bd8 | ||
|
|
0cf964073d | ||
|
|
298e161658 | ||
|
|
997b2e84da | ||
|
|
c135866f0d | ||
|
|
509f125e3a | ||
|
|
90c51a2d51 | ||
|
|
2b58539588 | ||
|
|
8b3bf26edf | ||
|
|
d4190496fb | ||
|
|
a8ceb40953 | ||
|
|
e47b6a32de | ||
|
|
77ed31d975 | ||
|
|
2d4a24554f | ||
|
|
4fcade37c2 | ||
|
|
71978841b1 | ||
|
|
6aab88abce | ||
|
|
7bc5ad35cb | ||
|
|
751020a589 | ||
|
|
fdfd6e80fb | ||
|
|
ee3b6e471b | ||
|
|
b818d6a1c1 | ||
|
|
221cd9826d | ||
|
|
5afda1dc2f | ||
|
|
108ca38f10 | ||
|
|
f104c7acdc | ||
|
|
a62a7d4da4 | ||
|
|
2f0fa38f3d | ||
|
|
2243a4e8eb | ||
|
|
4e15cb6618 | ||
|
|
2e103a2d69 | ||
|
|
22c7609e0d | ||
|
|
04504cdb2e | ||
|
|
294211bf84 | ||
|
|
0f7e0a4c87 | ||
|
|
351e9ab929 | ||
|
|
6d16eae210 | ||
|
|
f0d0345fcd | ||
|
|
1c8cb1a617 | ||
|
|
bc065d4b31 | ||
|
|
71c9d90b39 | ||
|
|
dd7d4923be | ||
|
|
f348405d46 | ||
|
|
7e37e7298e | ||
|
|
ed56599260 | ||
|
|
457ce15a4c | ||
|
|
892d200cd2 | ||
|
|
0b763b70f4 | ||
|
|
9310771832 | ||
|
|
a590c0487e | ||
|
|
46fcd84ecb | ||
|
|
f8f492efb2 | ||
|
|
ebe29d3a5a | ||
|
|
92b8ea6dcd | ||
|
|
1654dee62e | ||
|
|
2316b9e76a | ||
|
|
b095b361cb | ||
|
|
fe7c72beac | ||
|
|
9930209980 | ||
|
|
48553f4ad9 | ||
|
|
a7bb600bf5 | ||
|
|
57938b19a8 | ||
|
|
8d485f4f74 | ||
|
|
8db6d1f4f7 | ||
|
|
6198f7ace3 | ||
|
|
9472fb2a4c | ||
|
|
8fdc49a0af | ||
|
|
639cede6c4 | ||
|
|
65fd49e377 | ||
|
|
4e141bf764 | ||
|
|
2db40e841b | ||
|
|
1f728ff4da | ||
|
|
4c43b28820 | ||
|
|
e6610357e3 | ||
|
|
8ad2f8d633 | ||
|
|
127d354972 | ||
|
|
a099981b3c | ||
|
|
857230def3 | ||
|
|
693c23e562 | ||
|
|
5c6e31200e | ||
|
|
074318d797 | ||
|
|
838b377d4b | ||
|
|
f1061d7190 | ||
|
|
948e40f51a | ||
|
|
fb6746e694 | ||
|
|
e68632e840 | ||
|
|
049319c499 | ||
|
|
958c2ee356 | ||
|
|
6b569df751 | ||
|
|
7587a58dc3 | ||
|
|
7ea3930975 | ||
|
|
cccaf5fe54 | ||
|
|
932dbd74ee | ||
|
|
4cbb2300df | ||
|
|
ecb4efa352 | ||
|
|
03d7fe0407 | ||
|
|
3f6dff1543 | ||
|
|
9857869799 | ||
|
|
8879c5d8c7 | ||
|
|
7d1edd41c7 | ||
|
|
970542eb7f | ||
|
|
5270929b57 | ||
|
|
6543ead625 | ||
|
|
817d618d08 | ||
|
|
a0833393a8 | ||
|
|
811a2db58b | ||
|
|
8b9af97012 | ||
|
|
36b874d8a0 | ||
|
|
5680bc05b5 | ||
|
|
90b8751874 | ||
|
|
51712af78d | ||
|
|
f5681cb746 | ||
|
|
85d52a4d68 | ||
|
|
cec7f88d2b | ||
|
|
4043bcab61 | ||
|
|
1efa50aeab | ||
|
|
400fa08ebc | ||
|
|
eda1a1de24 | ||
|
|
31d8cd09dd | ||
|
|
91b3ddb489 | ||
|
|
2392d575c2 | ||
|
|
8b47748df9 | ||
|
|
7a9899f747 | ||
|
|
2e5e784f4c | ||
|
|
5b9d14e966 | ||
|
|
5fb0b1b5ff | ||
|
|
a08d95a742 | ||
|
|
64380cbd4d | ||
|
|
837d98deaf | ||
|
|
a63ac294db | ||
|
|
d20a0d8455 | ||
|
|
cd710e99c0 | ||
|
|
1cedbc9423 | ||
|
|
2ce5915e70 | ||
|
|
e602d3fef0 | ||
|
|
c6e1e5c1cf | ||
|
|
837bff58e7 | ||
|
|
01d50adce7 | ||
|
|
05cbe1c6f3 | ||
|
|
939bd8d9ab | ||
|
|
204a689848 | ||
|
|
a852c7e5ca | ||
|
|
b3ad45f2cc | ||
|
|
9f988bb464 | ||
|
|
ce0ad0a3ea | ||
|
|
c5daf892c6 | ||
|
|
bd484cbe41 | ||
|
|
cbfb0a7310 | ||
|
|
45027da057 | ||
|
|
fb53f2ed0e | ||
|
|
37e0e8942e | ||
|
|
ef86508bbb | ||
|
|
f6459e20f9 | ||
|
|
aeff5edaeb | ||
|
|
d274d05336 | ||
|
|
207eb0990c | ||
|
|
1cbe8297aa | ||
|
|
5b8fcebabd | ||
|
|
820f17ce74 | ||
|
|
753ecd7244 | ||
|
|
df3ea385ee | ||
|
|
eb041e9e65 | ||
|
|
96eb2496e4 | ||
|
|
cbe89c8c40 | ||
|
|
4d893c4da1 | ||
|
|
9dd0b135b9 | ||
|
|
5b0439ddb5 | ||
|
|
c3cb82a2de | ||
|
|
516f13bf48 | ||
|
|
c3bf18cf5a | ||
|
|
ece2d2943e | ||
|
|
610dbd4dcf | ||
|
|
fe3c043d61 | ||
|
|
83d8135722 | ||
|
|
6caf3f2252 | ||
|
|
b8331a3a4a | ||
|
|
c271349c30 | ||
|
|
329a6e0768 | ||
|
|
fae2dca9dc | ||
|
|
7112a42c96 | ||
|
|
d76618cbad | ||
|
|
d8db37786c | ||
|
|
79bc1065f3 | ||
|
|
b107afc13c | ||
|
|
fcfbf0a733 | ||
|
|
8460a7a87d | ||
|
|
2940dd71ab | ||
|
|
135ebaa251 | ||
|
|
f94b3eb383 | ||
|
|
a3db496f31 | ||
|
|
dd78c05d59 | ||
|
|
d9c4326a6b | ||
|
|
4ec90dbcfe | ||
|
|
b8c6800b37 | ||
|
|
a82a33996c | ||
|
|
9a27f19e2e | ||
|
|
c56c6e3e05 | ||
|
|
0c0fb37b33 | ||
|
|
e865d81dad | ||
|
|
cc86d67c26 | ||
|
|
3287a18cac | ||
|
|
135ea0f120 | ||
|
|
468928a2e6 | ||
|
|
57cafe78f8 | ||
|
|
8d27ef7a37 | ||
|
|
5acad994b6 | ||
|
|
6ebdf9ba4e | ||
|
|
f8067f11e1 | ||
|
|
4a0d23d652 | ||
|
|
42be930dfc | ||
|
|
47bc500f40 | ||
|
|
d536b6e43a | ||
|
|
5ef91076f9 | ||
|
|
5966e39406 | ||
|
|
044686b564 | ||
|
|
19018e4854 | ||
|
|
518e506df4 | ||
|
|
26ebf30da7 | ||
|
|
587fed282d | ||
|
|
98cabddcdc | ||
|
|
419bd01818 | ||
|
|
5a1ab0c168 | ||
|
|
74514b487d | ||
|
|
8e7bd453f4 | ||
|
|
348450b9d9 | ||
|
|
a50a481bb5 | ||
|
|
342e08e8fe | ||
|
|
65da6f39dc | ||
|
|
5774b99891 | ||
|
|
02ce970aea | ||
|
|
f8c24bf86a | ||
|
|
bbb486f7b2 | ||
|
|
bdbc7cf713 | ||
|
|
a44d4f0872 | ||
|
|
16636c34b5 | ||
|
|
ce2e379f30 | ||
|
|
4c4f5eef3e | ||
|
|
6a70a1412b | ||
|
|
a9025e2aba | ||
|
|
b1e26e3a48 | ||
|
|
7fc88f2641 | ||
|
|
68388a6011 | ||
|
|
481b060caf | ||
|
|
3cb8dae374 | ||
|
|
55999b5b65 | ||
|
|
1e126cd633 | ||
|
|
b6f7ff7038 | ||
|
|
8885bdd5f0 | ||
|
|
35b986e2be | ||
|
|
a80625d5f7 | ||
|
|
d2bb1a83a1 | ||
|
|
9f829c6990 | ||
|
|
6d52e06f66 | ||
|
|
1c8fcdbe6e | ||
|
|
b207814ffd | ||
|
|
f700e178a1 | ||
|
|
e75c7078ce | ||
|
|
74f7549b7f | ||
|
|
d9dc4ac68c | ||
|
|
134f170726 | ||
|
|
14fab0293e | ||
|
|
5318a7a9da | ||
|
|
46d655b328 | ||
|
|
099d0176d6 | ||
|
|
9aaee86120 | ||
|
|
e3d3927cfd | ||
|
|
c6d0f3da92 | ||
|
|
156fd4ee6f | ||
|
|
f192758f25 | ||
|
|
62f3507a32 | ||
|
|
25800a7883 | ||
|
|
15855bf6bf | ||
|
|
8dd683029b | ||
|
|
d10302ad9d | ||
|
|
06e74cf44f | ||
|
|
1b46b4b13b | ||
|
|
39d8c93444 | ||
|
|
6629f5578c | ||
|
|
5d4502e971 | ||
|
|
d070372117 | ||
|
|
330871cfbf | ||
|
|
d6dce1f0fe | ||
|
|
947c816591 | ||
|
|
c525d893d3 | ||
|
|
9ebdfc96a4 | ||
|
|
1ed0895803 | ||
|
|
eb4b8479f3 | ||
|
|
48fa6b755e | ||
|
|
4cef7c4c2d | ||
|
|
d072172ff5 | ||
|
|
6a90a76fb1 | ||
|
|
0ca083f5e9 | ||
|
|
f8e7ff86ab | ||
|
|
690832b7d7 | ||
|
|
78a42f29b1 | ||
|
|
d8be1dbb86 | ||
|
|
2995327a15 | ||
|
|
2cc48a0f25 | ||
|
|
511d436947 | ||
|
|
78a3a5f762 | ||
|
|
c98473d118 | ||
|
|
5b2e6591bc | ||
|
|
63ca5f8054 | ||
|
|
b1a2eb4de5 | ||
|
|
a1ca3523a3 | ||
|
|
ce5eb39f37 | ||
|
|
98438644c5 | ||
|
|
18d98a6384 | ||
|
|
ea49c9ef15 | ||
|
|
ddebd0c974 | ||
|
|
6e9c1dc08e | ||
|
|
7b433940bf | ||
|
|
dd2fffcfd5 | ||
|
|
679b5f144d | ||
|
|
f45601435c | ||
|
|
301cf2f1ba | ||
|
|
ba9a3a7980 | ||
|
|
80a002dadc | ||
|
|
206ba319af | ||
|
|
bdb217bed7 | ||
|
|
0805725a6f | ||
|
|
df177ac43f | ||
|
|
4ff76bab5c | ||
|
|
86c4c552db | ||
|
|
bc9582bcc1 | ||
|
|
f383c45e6b | ||
|
|
3bf5ffeb99 | ||
|
|
875879caed | ||
|
|
5f28bc82e0 | ||
|
|
0327f5c30f | ||
|
|
774c084708 | ||
|
|
325a06162e | ||
|
|
e3dbf56ef5 | ||
|
|
919f54d0d2 | ||
|
|
4563441a3a | ||
|
|
323c7da201 | ||
|
|
d667da4851 | ||
|
|
6be6b2ebde | ||
|
|
8f03a3aabb | ||
|
|
5f1489438c | ||
|
|
695843d7ec | ||
|
|
b9e4787b1c | ||
|
|
f2e0b436c6 | ||
|
|
e78dbeb056 | ||
|
|
cdbf740d38 | ||
|
|
bbbf7c5391 | ||
|
|
ad535b2e3f | ||
|
|
4f30ed6537 | ||
|
|
c6567d7830 | ||
|
|
a72ef287e3 | ||
|
|
86297a08bd | ||
|
|
0a81e51072 | ||
|
|
d33eb3b455 | ||
|
|
c6b1a163af | ||
|
|
8cca8f642c | ||
|
|
e8f49b8ecc | ||
|
|
4f1680810b | ||
|
|
7710578a3c | ||
|
|
02aae0d351 | ||
|
|
16512f3507 | ||
|
|
91976b2a2b | ||
|
|
fc44143587 | ||
|
|
70b160373d | ||
|
|
bafc648c9d | ||
|
|
e8e9e599f8 | ||
|
|
9051635bf5 | ||
|
|
dfb582ecc5 | ||
|
|
6ea1d6a237 | ||
|
|
a758a235dc | ||
|
|
5c20087f19 | ||
|
|
9b942ba05a | ||
|
|
96b6c89da9 | ||
|
|
3585d15353 | ||
|
|
d89d99da78 | ||
|
|
8a7468ef67 | ||
|
|
a76d4a79d1 | ||
|
|
882e75b7a3 | ||
|
|
222e00619e | ||
|
|
1f7edc5bb9 | ||
|
|
6a4712ec37 | ||
|
|
96376669ef | ||
|
|
a94cdbc633 | ||
|
|
3cd25f3c10 | ||
|
|
7281a813e0 | ||
|
|
3c1f141339 | ||
|
|
aa51d5be1d | ||
|
|
bb60488bf3 | ||
|
|
4fbabd9f35 | ||
|
|
f8750fe0b6 | ||
|
|
420fb1c393 | ||
|
|
7628b5a08d | ||
|
|
9038587f67 | ||
|
|
824dad6fab | ||
|
|
dbd2960841 | ||
|
|
b02fa6c358 | ||
|
|
bc1c51894c | ||
|
|
6136ece5dd | ||
|
|
bb204fa868 | ||
|
|
ce84ad8774 | ||
|
|
597a236e05 | ||
|
|
180606f721 | ||
|
|
dcfa718a9b | ||
|
|
6858f87926 | ||
|
|
743e808404 | ||
|
|
0ffb257f08 | ||
|
|
d670d5feab | ||
|
|
f34859f51b | ||
|
|
42ae43e81e | ||
|
|
41cad79a21 | ||
|
|
92c7525d0a | ||
|
|
86e496040c | ||
|
|
21c2ecfd1d | ||
|
|
720bd46683 | ||
|
|
b83967809d | ||
|
|
385d4e8ab2 | ||
|
|
96d52f47d1 | ||
|
|
e4353189dc | ||
|
|
dbe8dc67f2 | ||
|
|
4998c30d20 | ||
|
|
20e84b9c9a | ||
|
|
efdaa6a64e | ||
|
|
1a96622366 | ||
|
|
d5af189125 | ||
|
|
5ebaf8264a | ||
|
|
40bfde9697 | ||
|
|
a52805b35e | ||
|
|
0312258db7 | ||
|
|
2ddd9e2477 | ||
|
|
be44d2c601 | ||
|
|
ee44ba67cf | ||
|
|
c462216be7 | ||
|
|
e43ebd4d40 | ||
|
|
0ea0c416ec | ||
|
|
3f42eb86d2 | ||
|
|
b003e5065a | ||
|
|
cc25c40406 | ||
|
|
8e3c632a7c | ||
|
|
ab94208bb5 | ||
|
|
0562ed3eb9 | ||
|
|
3ceef052a3 | ||
|
|
d5b5251587 | ||
|
|
0ae73fa6b3 | ||
|
|
38b6fd213f | ||
|
|
39193ae92f | ||
|
|
c8c18497cc | ||
|
|
bdb0b3d6dc | ||
|
|
7312c8f396 | ||
|
|
831272a137 | ||
|
|
d50231b888 | ||
|
|
6160bb0953 | ||
|
|
1e013e6cd7 | ||
|
|
96955d9305 | ||
|
|
fdfa38a209 | ||
|
|
daa4b57af1 | ||
|
|
de3be6ba52 | ||
|
|
81c2e425ef | ||
|
|
4bf6b6fb96 | ||
|
|
4ed8497bd7 | ||
|
|
738280bbe5 | ||
|
|
ad9384aeac | ||
|
|
67f5416858 | ||
|
|
51567ff5c4 | ||
|
|
6f6a94c9b0 | ||
|
|
08bd3ecc91 | ||
|
|
6ebc0f4e81 | ||
|
|
bf39798263 | ||
|
|
92521acfa3 | ||
|
|
f8d43a19c1 | ||
|
|
842ddc2a26 | ||
|
|
bafed078e5 | ||
|
|
8999fb84de | ||
|
|
3b162924c5 | ||
|
|
242c61205d | ||
|
|
139727dd33 | ||
|
|
a52341e29e | ||
|
|
c2358f60fb | ||
|
|
c8ea108be3 | ||
|
|
8fdd0abc53 | ||
|
|
f396b2f476 | ||
|
|
6e9413eada | ||
|
|
30a5467b82 | ||
|
|
3ffa3ca5e5 | ||
|
|
f0351b8bec | ||
|
|
d8093fa1f0 | ||
|
|
2080f33fdb | ||
|
|
6a2925c546 | ||
|
|
26960c96d9 | ||
|
|
58a080fe6b | ||
|
|
3c3b7527a1 | ||
|
|
0d7028416a | ||
|
|
aa6dca4b4c | ||
|
|
3d7a7bd609 | ||
|
|
35854af1e9 | ||
|
|
4450652c32 | ||
|
|
d34e09f8d5 | ||
|
|
610ba5bf6a | ||
|
|
65debc6a27 | ||
|
|
61868d5fde | ||
|
|
998f4c6dff | ||
|
|
a2a8393775 | ||
|
|
d9ec9d3af9 | ||
|
|
7104b24633 | ||
|
|
9cd81c9148 | ||
|
|
56a60d1651 | ||
|
|
1cdf02bc31 | ||
|
|
5b469496d6 | ||
|
|
2a1d3a1ce1 | ||
|
|
9b89492d83 | ||
|
|
45b1eee8f3 | ||
|
|
e9dda2079a | ||
|
|
c279acbab8 | ||
|
|
082cd9b087 | ||
|
|
7d2e5d674a | ||
|
|
b2d95c545d | ||
|
|
127c3125ad | ||
|
|
63f1e0d504 | ||
|
|
1ad10b6954 | ||
|
|
820cf98087 | ||
|
|
15bd6beebd | ||
|
|
31871f192c | ||
|
|
7acfa4a2ac | ||
|
|
e212d68d60 | ||
|
|
6aeee89ee4 | ||
|
|
623572a652 | ||
|
|
f7c587c08b | ||
|
|
da1f346b1a | ||
|
|
056b56ed78 | ||
|
|
791581b850 | ||
|
|
08f426ba09 | ||
|
|
54371cffff | ||
|
|
c7946c0edf | ||
|
|
ae578c64a0 | ||
|
|
08bc1898cc | ||
|
|
dc626b1b3e | ||
|
|
7283e06750 | ||
|
|
c76368509e | ||
|
|
b54eb97f6b | ||
|
|
2d997fb046 | ||
|
|
e2cf769b20 | ||
|
|
281786b3b9 | ||
|
|
b06b8608d0 | ||
|
|
22dc39eb85 | ||
|
|
4a894958f0 | ||
|
|
38273a786a | ||
|
|
6ba0a5d942 | ||
|
|
2ad731f4e0 | ||
|
|
a491fb5471 | ||
|
|
33cfb940b4 | ||
|
|
46b79334e4 | ||
|
|
a7e841bcba | ||
|
|
f2400b35b0 | ||
|
|
31058336ce | ||
|
|
ac2cbef7f8 | ||
|
|
d7187ff998 | ||
|
|
2cc79f44ea | ||
|
|
067a67c14e | ||
|
|
054addfa9b | ||
|
|
6b7cf875de | ||
|
|
581e31499b | ||
|
|
a5883a8429 | ||
|
|
95c2d91a5b | ||
|
|
9f487d57fb | ||
|
|
e0d278e7ea | ||
|
|
36725c3574 | ||
|
|
8c911215b1 | ||
|
|
2b57a976c2 | ||
|
|
4817ce282a | ||
|
|
2da6666587 | ||
|
|
07f1bc050a | ||
|
|
9135e15b12 | ||
|
|
5c32a86257 | ||
|
|
d5bcac4d14 | ||
|
|
73bcadbbfe | ||
|
|
d3091d3dd8 | ||
|
|
f9ab78e393 | ||
|
|
7a94077906 | ||
|
|
6b438c3a46 | ||
|
|
5383ed22ea | ||
|
|
dbd09daa33 | ||
|
|
25ace13a3d | ||
|
|
8fc6112e32 | ||
|
|
ac2eb99d63 | ||
|
|
43707d8074 | ||
|
|
b0b3c18e99 | ||
|
|
349f2801c5 | ||
|
|
658e7beb2b | ||
|
|
e777eb6c99 | ||
|
|
8a6ce43ad3 | ||
|
|
72d182032b | ||
|
|
9b8189a3e4 | ||
|
|
14eb9ca3f8 | ||
|
|
cc2cba8c70 | ||
|
|
17660220fe | ||
|
|
d5538a79da | ||
|
|
fc5cd1c219 | ||
|
|
e10e63a87f | ||
|
|
93b05de15e | ||
|
|
a6a94060c6 | ||
|
|
a12ec0ffdc | ||
|
|
ae5d484309 | ||
|
|
2cdcc6f5ad | ||
|
|
3699e0199b | ||
|
|
5ec417d50c | ||
|
|
240edd6812 | ||
|
|
faa1b87926 | ||
|
|
08a9764465 | ||
|
|
e3a73ce7d1 | ||
|
|
d75a5e78a5 | ||
|
|
6a179215d6 | ||
|
|
c2dc2f0712 |
6
.github/ISSUE_TEMPLATE.txt
vendored
6
.github/ISSUE_TEMPLATE.txt
vendored
@@ -1,11 +1,11 @@
|
||||
The issue tracker is for reporting product deficiencies. "How do I?" questions should be posted to the discussion forum at https://groups.google.com/group/google-apps-manager. When in doubt, start at the discussion forum and return here only when instructed to do so.
|
||||
|
||||
Please confirm the following:
|
||||
* I have upgraded to the latest GAM release from https://git.io/gamreleases and I still have this issue.
|
||||
* I am typing the command as described in the GAM Wiki at https://github.com/jay0lee/gam/wiki
|
||||
* I have upgraded to the latest GAM release from https://github.com/GAM-team/GAM/releases and I still have this issue.
|
||||
* I am typing the command as described in the GAM Wiki at https://github.com/GAM-team/GAM/wiki
|
||||
|
||||
Full steps to reproduce the issue:
|
||||
1.
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/aa-question.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/aa-question.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: Question about using GAM
|
||||
about: Help with using GAM or running it for the first time
|
||||
title: Please use the GAM discussion group
|
||||
labels: invalid
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
If you need help with GAM, please do not file an issue here, it will be closed and ignored.
|
||||
|
||||
Please post your question to the GAM discussion group where other admins are ready and willing to help:
|
||||
|
||||
https://groups.google.com/g/google-apps-manager
|
||||
23
.github/ISSUE_TEMPLATE/za-bug-report.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/za-bug-report.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: jay0lee
|
||||
|
||||
---
|
||||
|
||||
The issue tracker is for reporting product deficiencies. "How do I?" questions should be posted to the discussion forum at https://groups.google.com/group/google-apps-manager. When in doubt, start at the discussion forum and return here only when instructed to do so.
|
||||
|
||||
Please confirm the following:
|
||||
* I have upgraded to the latest GAM release from https://github.com/GAM-team/GAM/releases and I still have this issue.
|
||||
* I am typing the command as described in the GAM Wiki at https://github.com/jay0lee/gam/wiki
|
||||
|
||||
Full steps to reproduce the issue:
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
Expected outcome (what are you trying to do?):
|
||||
|
||||
Actual outcome (what errors or bad behavior do you see instead?):
|
||||
20
.github/ISSUE_TEMPLATE/zz-feature-request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/zz-feature-request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for GAM
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: jay0lee
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
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**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
BIN
.github/actions/creds.tar.xz.gpg
vendored
Normal file
BIN
.github/actions/creds.tar.xz.gpg
vendored
Normal file
Binary file not shown.
38
.github/actions/decrypt.sh
vendored
Executable file
38
.github/actions/decrypt.sh
vendored
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
credspath="$3"
|
||||
if [ ! -d "$credspath" ]; then
|
||||
echo "creating ${credspath}"
|
||||
mkdir -p "$credspath"
|
||||
fi
|
||||
gpgfile="$1"
|
||||
if [ -f "$gpgfile" ]; then
|
||||
echo "source file is ${gpgfile}"
|
||||
else
|
||||
echo "ERROR: ${gpgfile} does not exist"
|
||||
exit 1
|
||||
fi
|
||||
credsfile="$2"
|
||||
echo "target file is ${credsfile}"
|
||||
if [ -z ${PASSCODE+x} ]; then
|
||||
echo "ERROR: PASSCODE is unset";
|
||||
exit 2
|
||||
else
|
||||
echo "PASSCODE is set";
|
||||
fi
|
||||
|
||||
gpg --batch \
|
||||
--yes \
|
||||
--decrypt \
|
||||
--passphrase="$PASSCODE" \
|
||||
--output "$credsfile" \
|
||||
"$gpgfile"
|
||||
|
||||
if [[ "$RUNNER_OS" == "macOS" ]]; then
|
||||
tar="gtar"
|
||||
else
|
||||
tar="tar"
|
||||
fi
|
||||
|
||||
"$tar" xlvvf "$credsfile" --directory "$credspath"
|
||||
rm -rvf "$gpgfile"
|
||||
rm -rvf "$credsfile"
|
||||
13
.github/actions/entitlements.plist
vendored
Normal file
13
.github/actions/entitlements.plist
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- These are required for binaries built by PyInstaller -->
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
6
.github/actions/package_exclusions.txt
vendored
Normal file
6
.github/actions/package_exclusions.txt
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
oauth2.txt
|
||||
nobrowser.txt
|
||||
enabledasa.txt
|
||||
lastupdatecheck.txt
|
||||
*.lck
|
||||
*.csv
|
||||
61
.github/stale.yml
vendored
Normal file
61
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 90
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: 7
|
||||
|
||||
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels: []
|
||||
|
||||
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- enhancement
|
||||
- help wanted
|
||||
- security
|
||||
|
||||
# Set to true to ignore issues in a project (defaults to false)
|
||||
exemptProjects: false
|
||||
|
||||
# Set to true to ignore issues in a milestone (defaults to false)
|
||||
exemptMilestones: false
|
||||
|
||||
# Set to true to ignore issues with an assignee (defaults to false)
|
||||
exemptAssignees: false
|
||||
|
||||
# Label to use when marking as stale
|
||||
staleLabel: wontfix
|
||||
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs.
|
||||
|
||||
# Comment to post when removing the stale label.
|
||||
# unmarkComment: >
|
||||
# Your comment here.
|
||||
|
||||
# Comment to post when closing a stale Issue or Pull Request.
|
||||
# closeComment: >
|
||||
# Your comment here.
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 30
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
# only: issues
|
||||
|
||||
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
|
||||
# pulls:
|
||||
# daysUntilStale: 30
|
||||
# markComment: >
|
||||
# This pull request has been automatically marked as stale because it has not had
|
||||
# recent activity. It will be closed if no further activity occurs. Thank you
|
||||
# for your contributions.
|
||||
|
||||
# issues:
|
||||
# exemptLabels:
|
||||
# - confirmed
|
||||
1050
.github/workflows/build.yml
vendored
Normal file
1050
.github/workflows/build.yml
vendored
Normal file
File diff suppressed because it is too large
Load Diff
70
.github/workflows/codeql-analysis.yml
vendored
Normal file
70
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '25 10 * * 1'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
36
.github/workflows/get-cacerts.yml
vendored
Normal file
36
.github/workflows/get-cacerts.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Check for Google Root CA Updates
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '23 23 * * *'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: src
|
||||
|
||||
jobs:
|
||||
check-apis:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
with:
|
||||
persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal token
|
||||
fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
|
||||
|
||||
- name: Check for updates
|
||||
run: curl -o ./cacerts.pem -vvvv https://pki.goog/roots.pem
|
||||
|
||||
- name: Commit file
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add cacerts.pem
|
||||
git diff --quiet && git diff --staged --quiet || git commit -am '[ci skip] Updated cacerts.pem'
|
||||
|
||||
- name: Push changes
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
32
.pre-commit-config.yaml
Normal file
32
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
# See https://pre-commit.com for more information
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: double-quote-string-fixer
|
||||
- id: check-yaml
|
||||
- id: check-docstring-first
|
||||
- id: name-tests-test
|
||||
- id: requirements-txt-fixer
|
||||
- id: check-merge-conflict
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-yapf
|
||||
rev: v0.30.0
|
||||
hooks:
|
||||
- id: yapf
|
||||
args: [--style=google, --in-place]
|
||||
|
||||
- repo: https://github.com/PyCQA/pylint
|
||||
rev: pylint-2.5.0
|
||||
hooks:
|
||||
- id: pylint
|
||||
args: [--output-format=colorized]
|
||||
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.31.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus]
|
||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
50
README.md
50
README.md
@@ -1,25 +1,41 @@
|
||||
GAM
|
||||
============================
|
||||
GAM is a command line tool for Google G Suite Administrators to manage domain and user settings quickly and easily.
|
||||
GAM is a command line tool for Google Workspace admins to manage domain and user settings quickly and easily.
|
||||
|
||||
Downloads
|
||||
---------
|
||||
You can download the current GAM release from the [GitHub Releases] page.
|
||||
[](https://github.com/GAM-team/GAM/actions/workflows/build.yml)
|
||||
|
||||
# Quick Start
|
||||
|
||||
## Linux / MacOS
|
||||
|
||||
Open a terminal and run:
|
||||
|
||||
```sh
|
||||
bash <(curl -s -S -L https://gam-shortn.appspot.com/gam-install)
|
||||
```
|
||||
|
||||
this will download GAM, install it and start setup.
|
||||
|
||||
## Windows
|
||||
|
||||
Download the MSI Installer from the [GitHub Releases] page. Install the MSI and you'll be prompted to setup GAM.
|
||||
|
||||
# Documentation
|
||||
|
||||
Documentation
|
||||
------------------
|
||||
The GAM documentation is hosted in the [GitHub Wiki]
|
||||
|
||||
Mailing List / Discussion group
|
||||
-------------------------------
|
||||
# Mailing List / Discussion group
|
||||
|
||||
The GAM mailing list / discussion group is hosted on [Google Groups]. You can join the list and interact via email, or just post from the web itself.
|
||||
|
||||
Author
|
||||
------
|
||||
GAM is maintained by <a href="mailto:jay0lee@gmail.com">Jay Lee</a>.
|
||||
# Chat Room
|
||||
|
||||
[GAM release]: https://git.io/gamreleases
|
||||
[GitHub Releases]: https://github.com/jay0lee/GAM/releases
|
||||
[GitHub]: https://github.com/jay0lee/GAM/tree/master
|
||||
[GitHub Wiki]: https://github.com/jay0lee/GAM/wiki/
|
||||
There is a public chat room hosted in Google Chat. [Instructions to join](https://github.com/GAM-team/GAM/wiki/GAM-Public-Chat-Room).
|
||||
|
||||
# Author
|
||||
|
||||
GAM is maintained by [Jay (James) Lee](mailto:jay0lee@gmail.com) and [Ross Scroggs](mailto:ross.scroggs@gmail.com). Please direct "how do I?" questions to [Google Groups].
|
||||
|
||||
[GAM release]: https://github.com/GAM-team/GAM/releases
|
||||
[GitHub Releases]: https://github.com/GAM-team/GAM/releases
|
||||
[GitHub]: https://github.com/GAM-team/GAM/tree/master
|
||||
[GitHub Wiki]: https://github.com/GAM-team/GAM/wiki/
|
||||
[Google Groups]: http://groups.google.com/group/google-apps-manager
|
||||
|
||||
2
src/.gitignore
vendored
2
src/.gitignore
vendored
@@ -64,7 +64,7 @@ nobrowser.txt
|
||||
nocache.txt
|
||||
noverifyssl.txt
|
||||
gamcache/
|
||||
gam/
|
||||
dist/
|
||||
gam-64/
|
||||
*.zip
|
||||
*.msi
|
||||
|
||||
8888
src/GamCommands.txt
8888
src/GamCommands.txt
File diff suppressed because it is too large
Load Diff
18751
src/GamUpdate.txt
Normal file
18751
src/GamUpdate.txt
Normal file
File diff suppressed because it is too large
Load Diff
547
src/LICENSE
547
src/LICENSE
@@ -1,547 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
|
||||
APACHE HTTP SERVER SUBCOMPONENTS:
|
||||
|
||||
The Apache HTTP Server includes a number of subcomponents with
|
||||
separate copyright notices and license terms. Your use of the source
|
||||
code for the these subcomponents is subject to the terms and
|
||||
conditions of the following licenses.
|
||||
|
||||
For the mod_mime_magic component:
|
||||
|
||||
/*
|
||||
* mod_mime_magic: MIME type lookup via file magic numbers
|
||||
* Copyright (c) 1996-1997 Cisco Systems, Inc.
|
||||
*
|
||||
* This software was submitted by Cisco Systems to the Apache Group in July
|
||||
* 1997. Future revisions and derivatives of this source code must
|
||||
* acknowledge Cisco Systems as the original contributor of this module.
|
||||
* All other licensing and usage conditions are those of the Apache Group.
|
||||
*
|
||||
* Some of this code is derived from the free version of the file command
|
||||
* originally posted to comp.sources.unix. Copyright info for that program
|
||||
* is included below as required.
|
||||
* ---------------------------------------------------------------------------
|
||||
* - Copyright (c) Ian F. Darwin, 1987. Written by Ian F. Darwin.
|
||||
*
|
||||
* This software is not subject to any license of the American Telephone and
|
||||
* Telegraph Company or of the Regents of the University of California.
|
||||
*
|
||||
* Permission is granted to anyone to use this software for any purpose on any
|
||||
* computer system, and to alter it and redistribute it freely, subject to
|
||||
* the following restrictions:
|
||||
*
|
||||
* 1. The author is not responsible for the consequences of use of this
|
||||
* software, no matter how awful, even if they arise from flaws in it.
|
||||
*
|
||||
* 2. The origin of this software must not be misrepresented, either by
|
||||
* explicit claim or by omission. Since few users ever read sources, credits
|
||||
* must appear in the documentation.
|
||||
*
|
||||
* 3. Altered versions must be plainly marked as such, and must not be
|
||||
* misrepresented as being the original software. Since few users ever read
|
||||
* sources, credits must appear in the documentation.
|
||||
*
|
||||
* 4. This notice may not be removed or altered.
|
||||
* -------------------------------------------------------------------------
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
For the modules\mappers\mod_imagemap.c component:
|
||||
|
||||
"macmartinized" polygon code copyright 1992 by Eric Haines, erich@eye.com
|
||||
|
||||
For the server\util_md5.c component:
|
||||
|
||||
/************************************************************************
|
||||
* NCSA HTTPd Server
|
||||
* Software Development Group
|
||||
* National Center for Supercomputing Applications
|
||||
* University of Illinois at Urbana-Champaign
|
||||
* 605 E. Springfield, Champaign, IL 61820
|
||||
* httpd@ncsa.uiuc.edu
|
||||
*
|
||||
* Copyright (C) 1995, Board of Trustees of the University of Illinois
|
||||
*
|
||||
************************************************************************
|
||||
*
|
||||
* md5.c: NCSA HTTPd code which uses the md5c.c RSA Code
|
||||
*
|
||||
* Original Code Copyright (C) 1994, Jeff Hostetler, Spyglass, Inc.
|
||||
* Portions of Content-MD5 code Copyright (C) 1993, 1994 by Carnegie Mellon
|
||||
* University (see Copyright below).
|
||||
* Portions of Content-MD5 code Copyright (C) 1991 Bell Communications
|
||||
* Research, Inc. (Bellcore) (see Copyright below).
|
||||
* Portions extracted from mpack, John G. Myers - jgm+@cmu.edu
|
||||
* Content-MD5 Code contributed by Martin Hamilton (martin@net.lut.ac.uk)
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
/* these portions extracted from mpack, John G. Myers - jgm+@cmu.edu */
|
||||
/* (C) Copyright 1993,1994 by Carnegie Mellon University
|
||||
* All Rights Reserved.
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software
|
||||
* and its documentation for any purpose is hereby granted without
|
||||
* fee, provided that the above copyright notice appear in all copies
|
||||
* and that both that copyright notice and this permission notice
|
||||
* appear in supporting documentation, and that the name of Carnegie
|
||||
* Mellon University not be used in advertising or publicity
|
||||
* pertaining to distribution of the software without specific,
|
||||
* written prior permission. Carnegie Mellon University makes no
|
||||
* representations about the suitability of this software for any
|
||||
* purpose. It is provided "as is" without express or implied
|
||||
* warranty.
|
||||
*
|
||||
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
|
||||
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
||||
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (c) 1991 Bell Communications Research, Inc. (Bellcore)
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this material
|
||||
* for any purpose and without fee is hereby granted, provided
|
||||
* that the above copyright notice and this permission notice
|
||||
* appear in all copies, and that the name of Bellcore not be
|
||||
* used in advertising or publicity pertaining to this
|
||||
* material without the specific, prior written permission
|
||||
* of an authorized representative of Bellcore. BELLCORE
|
||||
* MAKES NO REPRESENTATIONS ABOUT THE ACCURACY OR SUITABILITY
|
||||
* OF THIS MATERIAL FOR ANY PURPOSE. IT IS PROVIDED "AS IS",
|
||||
* WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
|
||||
*/
|
||||
|
||||
For the srclib\apr\include\apr_md5.h component:
|
||||
/*
|
||||
* This is work is derived from material Copyright RSA Data Security, Inc.
|
||||
*
|
||||
* The RSA copyright statement and Licence for that original material is
|
||||
* included below. This is followed by the Apache copyright statement and
|
||||
* licence for the modifications made to that material.
|
||||
*/
|
||||
|
||||
/* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
|
||||
rights reserved.
|
||||
|
||||
License to copy and use this software is granted provided that it
|
||||
is identified as the "RSA Data Security, Inc. MD5 Message-Digest
|
||||
Algorithm" in all material mentioning or referencing this software
|
||||
or this function.
|
||||
|
||||
License is also granted to make and use derivative works provided
|
||||
that such works are identified as "derived from the RSA Data
|
||||
Security, Inc. MD5 Message-Digest Algorithm" in all material
|
||||
mentioning or referencing the derived work.
|
||||
|
||||
RSA Data Security, Inc. makes no representations concerning either
|
||||
the merchantability of this software or the suitability of this
|
||||
software for any particular purpose. It is provided "as is"
|
||||
without express or implied warranty of any kind.
|
||||
|
||||
These notices must be retained in any copies of any part of this
|
||||
documentation and/or software.
|
||||
*/
|
||||
|
||||
For the srclib\apr\passwd\apr_md5.c component:
|
||||
|
||||
/*
|
||||
* This is work is derived from material Copyright RSA Data Security, Inc.
|
||||
*
|
||||
* The RSA copyright statement and Licence for that original material is
|
||||
* included below. This is followed by the Apache copyright statement and
|
||||
* licence for the modifications made to that material.
|
||||
*/
|
||||
|
||||
/* MD5C.C - RSA Data Security, Inc., MD5 message-digest algorithm
|
||||
*/
|
||||
|
||||
/* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
|
||||
rights reserved.
|
||||
|
||||
License to copy and use this software is granted provided that it
|
||||
is identified as the "RSA Data Security, Inc. MD5 Message-Digest
|
||||
Algorithm" in all material mentioning or referencing this software
|
||||
or this function.
|
||||
|
||||
License is also granted to make and use derivative works provided
|
||||
that such works are identified as "derived from the RSA Data
|
||||
Security, Inc. MD5 Message-Digest Algorithm" in all material
|
||||
mentioning or referencing the derived work.
|
||||
|
||||
RSA Data Security, Inc. makes no representations concerning either
|
||||
the merchantability of this software or the suitability of this
|
||||
software for any particular purpose. It is provided "as is"
|
||||
without express or implied warranty of any kind.
|
||||
|
||||
These notices must be retained in any copies of any part of this
|
||||
documentation and/or software.
|
||||
*/
|
||||
/*
|
||||
* The apr_md5_encode() routine uses much code obtained from the FreeBSD 3.0
|
||||
* MD5 crypt() function, which is licenced as follows:
|
||||
* ----------------------------------------------------------------------------
|
||||
* "THE BEER-WARE LICENSE" (Revision 42):
|
||||
* <phk@login.dknet.dk> wrote this file. As long as you retain this notice you
|
||||
* can do whatever you want with this stuff. If we meet some day, and you think
|
||||
* this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
For the srclib\apr-util\crypto\apr_md4.c component:
|
||||
|
||||
* This is derived from material copyright RSA Data Security, Inc.
|
||||
* Their notice is reproduced below in its entirety.
|
||||
*
|
||||
* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
|
||||
* rights reserved.
|
||||
*
|
||||
* License to copy and use this software is granted provided that it
|
||||
* is identified as the "RSA Data Security, Inc. MD4 Message-Digest
|
||||
* Algorithm" in all material mentioning or referencing this software
|
||||
* or this function.
|
||||
*
|
||||
* License is also granted to make and use derivative works provided
|
||||
* that such works are identified as "derived from the RSA Data
|
||||
* Security, Inc. MD4 Message-Digest Algorithm" in all material
|
||||
* mentioning or referencing the derived work.
|
||||
*
|
||||
* RSA Data Security, Inc. makes no representations concerning either
|
||||
* the merchantability of this software or the suitability of this
|
||||
* software for any particular purpose. It is provided "as is"
|
||||
* without express or implied warranty of any kind.
|
||||
*
|
||||
* These notices must be retained in any copies of any part of this
|
||||
* documentation and/or software.
|
||||
*/
|
||||
|
||||
For the srclib\apr-util\include\apr_md4.h component:
|
||||
|
||||
*
|
||||
* This is derived from material copyright RSA Data Security, Inc.
|
||||
* Their notice is reproduced below in its entirety.
|
||||
*
|
||||
* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
|
||||
* rights reserved.
|
||||
*
|
||||
* License to copy and use this software is granted provided that it
|
||||
* is identified as the "RSA Data Security, Inc. MD4 Message-Digest
|
||||
* Algorithm" in all material mentioning or referencing this software
|
||||
* or this function.
|
||||
*
|
||||
* License is also granted to make and use derivative works provided
|
||||
* that such works are identified as "derived from the RSA Data
|
||||
* Security, Inc. MD4 Message-Digest Algorithm" in all material
|
||||
* mentioning or referencing the derived work.
|
||||
*
|
||||
* RSA Data Security, Inc. makes no representations concerning either
|
||||
* the merchantability of this software or the suitability of this
|
||||
* software for any particular purpose. It is provided "as is"
|
||||
* without express or implied warranty of any kind.
|
||||
*
|
||||
* These notices must be retained in any copies of any part of this
|
||||
* documentation and/or software.
|
||||
*/
|
||||
|
||||
|
||||
For the srclib\apr-util\test\testmd4.c component:
|
||||
|
||||
*
|
||||
* This is derived from material copyright RSA Data Security, Inc.
|
||||
* Their notice is reproduced below in its entirety.
|
||||
*
|
||||
* Copyright (C) 1990-2, RSA Data Security, Inc. Created 1990. All
|
||||
* rights reserved.
|
||||
*
|
||||
* RSA Data Security, Inc. makes no representations concerning either
|
||||
* the merchantability of this software or the suitability of this
|
||||
* software for any particular purpose. It is provided "as is"
|
||||
* without express or implied warranty of any kind.
|
||||
*
|
||||
* These notices must be retained in any copies of any part of this
|
||||
* documentation and/or software.
|
||||
*/
|
||||
|
||||
For the srclib\apr-util\xml\expat\conftools\install-sh component:
|
||||
|
||||
#
|
||||
# install - install a program, script, or datafile
|
||||
# This comes from X11R5 (mit/util/scripts/install.sh).
|
||||
#
|
||||
# Copyright 1991 by the Massachusetts Institute of Technology
|
||||
#
|
||||
# Permission to use, copy, modify, distribute, and sell this software and its
|
||||
# documentation for any purpose is hereby granted without fee, provided that
|
||||
# the above copyright notice appear in all copies and that both that
|
||||
# copyright notice and this permission notice appear in supporting
|
||||
# documentation, and that the name of M.I.T. not be used in advertising or
|
||||
# publicity pertaining to distribution of the software without specific,
|
||||
# written prior permission. M.I.T. makes no representations about the
|
||||
# suitability of this software for any purpose. It is provided "as is"
|
||||
# without express or implied warranty.
|
||||
#
|
||||
|
||||
For the test\zb.c component:
|
||||
|
||||
/* ZeusBench V1.01
|
||||
===============
|
||||
|
||||
This program is Copyright (C) Zeus Technology Limited 1996.
|
||||
|
||||
This program may be used and copied freely providing this copyright notice
|
||||
is not removed.
|
||||
|
||||
This software is provided "as is" and any express or implied waranties,
|
||||
including but not limited to, the implied warranties of merchantability and
|
||||
fitness for a particular purpose are disclaimed. In no event shall
|
||||
Zeus Technology Ltd. be liable for any direct, indirect, incidental, special,
|
||||
exemplary, or consequential damaged (including, but not limited to,
|
||||
procurement of substitute good or services; loss of use, data, or profits;
|
||||
or business interruption) however caused and on theory of liability. Whether
|
||||
in contract, strict liability or tort (including negligence or otherwise)
|
||||
arising in any way out of the use of this software, even if advised of the
|
||||
possibility of such damage.
|
||||
|
||||
Written by Adam Twiss (adam@zeus.co.uk). March 1996
|
||||
|
||||
Thanks to the following people for their input:
|
||||
Mike Belshe (mbelshe@netscape.com)
|
||||
Michael Campanella (campanella@stevms.enet.dec.com)
|
||||
|
||||
*/
|
||||
|
||||
For the expat xml parser component:
|
||||
|
||||
Copyright (c) 1998, 1999, 2000 Thai Open Source Software Center Ltd
|
||||
and Clark Cooper
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
====================================================================
|
||||
1
src/LICENSE
Symbolic link
1
src/LICENSE
Symbolic link
@@ -0,0 +1 @@
|
||||
../LICENSE
|
||||
1128
src/cacerts.pem
Normal file
1128
src/cacerts.pem
Normal file
File diff suppressed because it is too large
Load Diff
21
src/callgam.py
Executable file
21
src/callgam.py
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Sample Python script to call GAM"""
|
||||
|
||||
import multiprocessing
|
||||
import platform
|
||||
|
||||
from gam import initializeLogging, CallGAMCommand
|
||||
|
||||
if __name__ == '__main__':
|
||||
# One time initialization
|
||||
if platform.system() != 'Linux':
|
||||
multiprocessing.freeze_support()
|
||||
multiprocessing.set_start_method('spawn')
|
||||
initializeLogging()
|
||||
#
|
||||
CallGAMCommand(['gam', 'version'])
|
||||
# Issue command, output goes to stdout/stderr
|
||||
rc = CallGAMCommand(['gam', 'info', 'domain'])
|
||||
# Issue command, redirect stdout/stderr
|
||||
rc = CallGAMCommand(['gam', 'redirect', 'stdout', 'domain.txt', 'redirect', 'stderr', 'stdout', 'info', 'domain'])
|
||||
@@ -1,486 +0,0 @@
|
||||
{
|
||||
"kind": "discovery#restDescription",
|
||||
"discoveryVersion": "v1",
|
||||
"id": "cloudprint:v2",
|
||||
"name": "cloudprint",
|
||||
"version": "v2",
|
||||
"revision": "20150605",
|
||||
"title": "Cloud Print API",
|
||||
"description": "Lets you access Cloud Print Printers",
|
||||
"ownerDomain": "google.com",
|
||||
"ownerName": "Google",
|
||||
"icons": {
|
||||
"x16": "http://www.google.com/images/icons/product/search-16.gif",
|
||||
"x32": "http://www.google.com/images/icons/product/search-32.gif"
|
||||
},
|
||||
"documentationLink": "https://developers.google.com/cloud-print",
|
||||
"protocol": "rest",
|
||||
"baseUrl": "https://www.google.com/",
|
||||
"basePath": "/cloudprint/",
|
||||
"rootUrl": "https://www.google.com/",
|
||||
"servicePath": "/cloudprint/",
|
||||
"parameters": {
|
||||
"prettyPrint": {
|
||||
"type": "boolean",
|
||||
"description": "Returns response with indentations and line breaks.",
|
||||
"default": "true",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"oauth2": {
|
||||
"scopes": {
|
||||
"https://www.googleapis.com/auth/cloudprint": {
|
||||
"description": "Manage Cloud Print"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"Job": {
|
||||
"id": "Job",
|
||||
"type": "object",
|
||||
"description": "Job Object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Job Title"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jobs": {
|
||||
"id": "Jobs",
|
||||
"type": "object",
|
||||
"description": "List of Jobs.",
|
||||
"properties": {
|
||||
"jobs": {
|
||||
"type": "array",
|
||||
"description": "List of job objects.",
|
||||
"items": {
|
||||
"$ref": "Job"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Printer": {
|
||||
"id": "Printer",
|
||||
"type": "object",
|
||||
"description": "Printer Object",
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Display Name"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Printers": {
|
||||
"id": "Printers",
|
||||
"type": "object",
|
||||
"description": "List of Printers.",
|
||||
"properties": {
|
||||
"printers": {
|
||||
"type": "array",
|
||||
"description": "List of printer objects.",
|
||||
"items": {
|
||||
"$ref": "Printer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"jobs": {
|
||||
"methods": {
|
||||
"delete": {
|
||||
"id": "cloudprint.jobs.delete",
|
||||
"path": "deletejob",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"jobid": {
|
||||
"type": "string",
|
||||
"location": "query",
|
||||
"required": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fetch": {
|
||||
"id": "cloudprint.jobs.fetch",
|
||||
"path": "fetch",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Jobs"
|
||||
}
|
||||
},
|
||||
"getticket": {
|
||||
"id": "cloudprint.jobs.getticket",
|
||||
"path": "ticket",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"jobid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"use_cjt": {
|
||||
"type": "boolean",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"id": "cloudprint.jobs.list",
|
||||
"path": "jobs",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"q": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"offset": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"limit": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"sortorder": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Jobs"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"id": "cloudprint.jobs.update",
|
||||
"path": "control",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"jobid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"semantic_state_diff": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Jobs"
|
||||
}
|
||||
},
|
||||
"resubmit": {
|
||||
"id": "cloudprint.jobs.resubmit",
|
||||
"path": "resubmit",
|
||||
"httpMethod": "POST",
|
||||
"description": "resubmit a job to new printer.",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"jobid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"ticket": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Job"
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"id": "cloudprint.jobs.submit",
|
||||
"path": "submit",
|
||||
"httpMethod": "POST",
|
||||
"description": "Send a print job to cloud print.",
|
||||
"request": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"ticket": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"contentType": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Job"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"printers": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "cloudprint.printers.get",
|
||||
"path": "printer",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"extra_fields": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Printer"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"id": "cloudprint.printers.list",
|
||||
"path": "search",
|
||||
"httpMethod": "GET",
|
||||
"description": "List all printers",
|
||||
"parameters": {
|
||||
"q": {
|
||||
"type": "string",
|
||||
"description": "Query list of printers",
|
||||
"location": "query"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "limit results to printers of type",
|
||||
"location": "query"
|
||||
},
|
||||
"connection_status": {
|
||||
"type": "string",
|
||||
"description": "limit results to printers with this status",
|
||||
"location": "query"
|
||||
},
|
||||
"extra_fields": {
|
||||
"type": "string",
|
||||
"description": "include extra fields",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Printers"
|
||||
}
|
||||
},
|
||||
"share": {
|
||||
"id": "cloudprint.printers.share",
|
||||
"path": "share",
|
||||
"httpMethod": "GET",
|
||||
"description": "Share printer with user, group or domain",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"skip_notification": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"unshare": {
|
||||
"id": "cloudprint.printers.unshare",
|
||||
"path": "unshare",
|
||||
"httpMethod": "GET",
|
||||
"description": "unshare printer with user, group or domain",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"public": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"id": "cloudprint.printers.delete",
|
||||
"path": "delete",
|
||||
"httpMethod": "GET",
|
||||
"description": "delete a printer",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"id": "cloudprint.printers.update",
|
||||
"path": "update",
|
||||
"httpMethod": "GET",
|
||||
"description": "update a printer",
|
||||
"parameters": {
|
||||
"isTosAccepted": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
},
|
||||
"gcpVersion": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"setupUrl": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"supportUrl": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"firmware": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"currentQuota": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"proxy": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"manufacturer": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"defaultDisplayName": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"updateUrl": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"ownerId": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"quotaEnabled": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
},
|
||||
"dailyQuota": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
{
|
||||
"kind": "discovery#restDescription",
|
||||
"discoveryVersion": "v1",
|
||||
"id": "email-settings:v2",
|
||||
"name": "email-settings",
|
||||
"version": "v2",
|
||||
"revision": "20161013",
|
||||
"title": "Email Settings API",
|
||||
"description": "Lets you manage Google Apps Email Settings",
|
||||
"ownerDomain": "google.com",
|
||||
"ownerName": "Google",
|
||||
"icons": {
|
||||
"x16": "http://www.google.com/images/icons/product/search-16.gif",
|
||||
"x32": "http://www.google.com/images/icons/product/search-32.gif"
|
||||
},
|
||||
"documentationLink": "https://developers.google.com/admin-sdk/email-settings",
|
||||
"protocol": "rest",
|
||||
"baseUrl": "https://apps-apis.google.com/",
|
||||
"rootUrl": "https://apps-apis.google.com/",
|
||||
"servicePath": "/a/feeds/emailsettings/2.0/",
|
||||
"parameters": {
|
||||
"v": {
|
||||
"type": "string",
|
||||
"description": "GData Version",
|
||||
"default": "2.0",
|
||||
"enum": [
|
||||
"2.0"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"GData 2.0"
|
||||
],
|
||||
"location": "query"
|
||||
},
|
||||
"alt": {
|
||||
"type": "string",
|
||||
"description": "Data format for the response.",
|
||||
"default": "json",
|
||||
"enum": [
|
||||
"json"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Responses with Content-Type of application/json"
|
||||
],
|
||||
"location": "query"
|
||||
},
|
||||
"quotaUser": {
|
||||
"type": "string",
|
||||
"description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.",
|
||||
"location": "query"
|
||||
},
|
||||
"prettyPrint": {
|
||||
"type": "boolean",
|
||||
"description": "Returns response with indentations and line breaks.",
|
||||
"default": "true",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"oauth2": {
|
||||
"scopes": {
|
||||
"https://apps-apis.google.com/a/feeds/emailsettings/2.0/": {
|
||||
"description": "Manage email settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"Delegate": {
|
||||
"id": "Delegate",
|
||||
"type": "object",
|
||||
"description": "a delegate.",
|
||||
"properties": {
|
||||
"apps$property": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "property name"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "organization name value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Delegates": {
|
||||
"id": "feed",
|
||||
"type": "object",
|
||||
"description": "List of delegates.",
|
||||
"properties": {
|
||||
"entry": {
|
||||
"type": "object",
|
||||
"description": "list of delegates",
|
||||
"items": {
|
||||
"$ref": "Delegate"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"delegates": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "email-settings.delegates.get",
|
||||
"path": "{domainName}/{delegator}/delegation",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
},
|
||||
"delegator": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Delegates"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"id": "email-settings.delegates.delete",
|
||||
"path": "{domainName}/{delegator}/delegation/{delegate}",
|
||||
"httpMethod": "DELETE",
|
||||
"parameters": {
|
||||
"domainName": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
},
|
||||
"delegator": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
},
|
||||
"delegate": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "path"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,47 +8,61 @@ GAM installation script.
|
||||
OPTIONS:
|
||||
-h show help.
|
||||
-d Directory where gam folder will be installed. Default is \$HOME/bin/
|
||||
-a Architecture to install (i386, x86_64, arm). Default is to detect your arch with "uname -m".
|
||||
-a Architecture to install (i386, x86_64, x86_64_legacy, arm, arm64). Default is to detect your arch with "uname -m".
|
||||
-o OS we are running (linux, macos). Default is to detect your OS with "uname -s".
|
||||
-b OS version. Default is to detect on MacOS and Linux.
|
||||
-l Just upgrade GAM to latest version. Skips project creation and auth.
|
||||
-p Profile update (true, false). Should script add gam command to environment. Default is true.
|
||||
-u Admin user email address to use with GAM. Default is to prompt.
|
||||
-r Regular user email address. Used to test service account access to user data. Default is to prompt.
|
||||
-v Version to install (latest, prerelease, draft, 3.8, etc). Default is latest.
|
||||
-s Strip gam component from extracted files, files will be downloaded directly to $target_dir
|
||||
EOF
|
||||
}
|
||||
|
||||
target_dir="$HOME/bin"
|
||||
target_gam="gam7/gam"
|
||||
gamarch=$(uname -m)
|
||||
gamos=$(uname -s)
|
||||
osversion=""
|
||||
update_profile=true
|
||||
upgrade_only=false
|
||||
gamversion="latest"
|
||||
adminuser=""
|
||||
regularuser=""
|
||||
while getopts "hd:a:o:p:u:r:v:" OPTION
|
||||
strip_gam="--strip-components 0"
|
||||
|
||||
while getopts "hd:a:o:b:lp:u:r:v:s" OPTION
|
||||
do
|
||||
case $OPTION in
|
||||
h) usage; exit;;
|
||||
d) target_dir=$OPTARG;;
|
||||
a) gamarch=$OPTARG;;
|
||||
o) gamos=$OPTARG;;
|
||||
p) update_profile=$OPTARG;;
|
||||
u) adminuser=$OPTARG;;
|
||||
r) regularuser=$OPTARG;;
|
||||
v) gamversion=$OPTARG;;
|
||||
d) target_dir="$OPTARG";;
|
||||
a) gamarch="$OPTARG";;
|
||||
o) gamos="$OPTARG";;
|
||||
b) osversion="$OPTARG";;
|
||||
l) upgrade_only=true;;
|
||||
p) update_profile="$OPTARG";;
|
||||
u) adminuser="$OPTARG";;
|
||||
r) regularuser="$OPTARG";;
|
||||
v) gamversion="$OPTARG";;
|
||||
s) strip_gam="--strip-components 1"; target_gam="gam";;
|
||||
?) usage; exit;;
|
||||
esac
|
||||
done
|
||||
|
||||
update_profile() {
|
||||
[ -f "$1" ] || return 1
|
||||
# remove possible / from end of target_dir
|
||||
target_dir=${target_dir%/}
|
||||
|
||||
grep -F "$alias_line" "$1" > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
update_profile() {
|
||||
[ "$2" -eq 1 ] || [ -f "$1" ] || return 1
|
||||
|
||||
grep -F "$alias_line" "$1" > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo_yellow "Adding gam alias to profile file $1."
|
||||
echo -e "\n$alias_line" >> "$1"
|
||||
echo -e "\n$alias_line" >> "$1"
|
||||
else
|
||||
echo_yellow "gam alias already exists in profile file $1. Skipping add."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
echo_red()
|
||||
@@ -69,43 +83,50 @@ echo -e "\x1B[1;33m$1"
|
||||
echo -e '\x1B[0m'
|
||||
}
|
||||
|
||||
case $gamos in
|
||||
[lL]inux)
|
||||
gamos="linux"
|
||||
case $gamarch in
|
||||
x86_64) gamfile="linux-x86_64.tar.xz";;
|
||||
i?86) gamfile="linux-i686.tar.xz";;
|
||||
arm*) gamfile="linux-armv7l.tar.xz";;
|
||||
*)
|
||||
echo_red "ERROR: this installer currently only supports i386, x86_64 and arm Linux. Looks like you're running on $gamarch. Exiting."
|
||||
exit
|
||||
esac
|
||||
;;
|
||||
[Mm]ac[Oo][sS]|[Dd]arwin)
|
||||
osver=$(sw_vers -productVersion | awk -F'.' '{print $2}')
|
||||
if (( $osver < 10 )); then
|
||||
echo_red "ERROR: GAM currently requires MacOS 10.10 or newer. You are running MacOS 10.$osver. Please upgrade."
|
||||
exit
|
||||
else
|
||||
echo_green "Good, you're running MacOS 10.$osver..."
|
||||
fi
|
||||
gamos="macos"
|
||||
gamfile="macos.tar.xz"
|
||||
;;
|
||||
*)
|
||||
echo_red "Sorry, this installer currently only supports Linux and MacOS. Looks like you're runnning on $gamos. Exiting."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$gamversion" == "latest" -o "$gamversion" == "prerelease" -o "$gamversion" == "draft" ]; then
|
||||
release_url="https://api.github.com/repos/jay0lee/GAM/releases"
|
||||
version_gt()
|
||||
{
|
||||
# MacOS < 10.13 doesn't support sort -V
|
||||
echo "" | sort -V > /dev/null 2>&1
|
||||
vsort_failed=$?
|
||||
if [ "${1}" = "${2}" ]; then
|
||||
true
|
||||
elif (( $vsort_failed != 0 )); then
|
||||
false
|
||||
else
|
||||
release_url="https://api.github.com/repos/jay0lee/GAM/releases/tags/v$gamversion"
|
||||
test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
reverse() {
|
||||
for (( i = ${#*}; i > 0; i-- ))
|
||||
{
|
||||
echo ${!i}
|
||||
}
|
||||
}
|
||||
|
||||
if [ "$gamversion" == "latest" ]; then
|
||||
release_url="https://api.github.com/repos/GAM-team/GAM/releases/latest"
|
||||
elif [ "$gamversion" == "prerelease" -o "$gamversion" == "draft" ]; then
|
||||
release_url="https://api.github.com/repos/GAM-team/GAM/releases"
|
||||
else
|
||||
release_url="https://api.github.com/repos/GAM-team/GAM/releases/tags/v$gamversion"
|
||||
fi
|
||||
|
||||
echo_yellow "Checking GitHub URL $release_url for $gamversion GAM release..."
|
||||
release_json=$(curl -s $release_url 2>&1 /dev/null)
|
||||
if [ -z ${GHCLIENT+x} ]; then
|
||||
check_type="unauthenticated"
|
||||
curl_opts=( )
|
||||
else
|
||||
check_type="authenticated"
|
||||
curl_opts=( "$GHCLIENT" )
|
||||
fi
|
||||
echo_yellow "Checking GitHub URL $release_url for $gamversion GAM release ($check_type)..."
|
||||
release_json=$(curl \
|
||||
--silent \
|
||||
"${curl_opts[@]}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"$release_url" \
|
||||
2>&1 /dev/null)
|
||||
|
||||
echo_yellow "Getting file and download URL..."
|
||||
# Python is sadly the nearest to universal way to safely handle JSON with Bash
|
||||
@@ -127,23 +148,191 @@ if type(release) is list:
|
||||
continue
|
||||
release = a_release
|
||||
break
|
||||
for asset in release['assets']:
|
||||
if asset[sys.argv[1]].endswith('$gamfile'):
|
||||
print asset[sys.argv[1]]
|
||||
break"
|
||||
browser_download_url=$(echo "$release_json" | python -c "$pycode" browser_download_url $gamversion)
|
||||
name=$(echo "$release_json" | python -c "$pycode" name $gamversion)
|
||||
try:
|
||||
for asset in release['assets']:
|
||||
print(asset[attrib])
|
||||
#else:
|
||||
# print('ERROR: Attribute: {0} for version {1} not found'.format(attrib, gamversion))
|
||||
except KeyError:
|
||||
print('ERROR: assets value not found in JSON value of:\n\n%s' % release)"
|
||||
|
||||
pycmd="python3"
|
||||
$pycmd -V >/dev/null 2>&1
|
||||
rc=$?
|
||||
if (( $rc != 0 )); then
|
||||
pycmd="python"
|
||||
fi
|
||||
$pycmd -V >/dev/null 2>&1
|
||||
rc=$?
|
||||
if (( $rc != 0 )); then
|
||||
pycmd="/usr/bin/python3"
|
||||
fi
|
||||
$pycmd -V >/dev/null 2>&1
|
||||
rc=$?
|
||||
if (( $rc != 0 )); then
|
||||
pycmd="python2"
|
||||
fi
|
||||
$pycmd -V >/dev/null 2>&1
|
||||
rc=$?
|
||||
if (( $rc != 0 )); then
|
||||
echo_red "ERROR: No version of python installed."
|
||||
exit
|
||||
fi
|
||||
# also sort the URLs once so we're evaluating newest OS version first
|
||||
download_urls=$(echo "$release_json" | \
|
||||
$pycmd -c "$pycode" browser_download_url "$gamversion" | \
|
||||
sort --version-sort --reverse)
|
||||
if [[ ${download_urls:0:5} = "ERROR" ]]; then
|
||||
echo_red "${download_urls}"
|
||||
exit
|
||||
fi
|
||||
|
||||
case $gamos in
|
||||
[lL]inux)
|
||||
gamos="linux"
|
||||
download_urls=$(echo -e "$download_urls" | grep "\-linux-")
|
||||
if [ "$osversion" == "" ]; then
|
||||
this_glibc_ver=$(ldd --version | awk '/ldd/{print $NF}')
|
||||
else
|
||||
this_glibc_ver=$osversion
|
||||
fi
|
||||
echo "This Linux distribution uses glibc $this_glibc_ver"
|
||||
case $gamarch in
|
||||
x86_64)
|
||||
download_urls=$(echo -e "$download_urls" | grep "\-x86_64-")
|
||||
gam_x86_64_glibc_vers=$(echo -e "$download_urls" | \
|
||||
grep --only-matching 'glibc[0-9\.]*\.tar\.xz$' \
|
||||
| cut -c 6-9 )
|
||||
useglibc="legacy"
|
||||
for gam_glibc_ver in $gam_x86_64_glibc_vers; do
|
||||
if version_gt $this_glibc_ver $gam_glibc_ver; then
|
||||
useglibc="glibc$gam_glibc_ver"
|
||||
echo_green "Using GAM compiled against $useglibc"
|
||||
break
|
||||
fi
|
||||
done
|
||||
download_url=$(echo -e "$download_urls" | grep "$useglibc")
|
||||
;;
|
||||
arm|arm64|aarch64)
|
||||
download_urls=$(echo -e "$download_urls" | grep "\-aarch64-")
|
||||
gam_arm64_glibc_vers=$(echo -e "$download_urls" | \
|
||||
grep --only-matching 'glibc[0-9\.]*\.tar\.xz$' | \
|
||||
cut -c 6-9)
|
||||
useglibc="legacy"
|
||||
for gam_glibc_ver in $gam_arm64_glibc_vers; do
|
||||
if version_gt $this_glibc_ver $gam_glibc_ver; then
|
||||
useglibc="glibc$gam_glibc_ver"
|
||||
echo_green "Using GAM compiled against $useglibc"
|
||||
break
|
||||
fi
|
||||
done
|
||||
download_url=$(echo -e "$download_urls" | grep "$useglibc")
|
||||
;;
|
||||
*)
|
||||
echo_red "ERROR: this installer currently only supports x86_64 and arm64 Linux. Looks like you're running on $gamarch. Exiting."
|
||||
exit
|
||||
esac
|
||||
;;
|
||||
[Mm]ac[Oo][sS]|[Dd]arwin)
|
||||
gamos="macos"
|
||||
currentversion=$(sw_vers -productVersion | awk -F '.' '{print $1 "." $2}')
|
||||
# override osversion only if it wasn't set by cli arguments
|
||||
osversion=${osversion:-${currentversion}}
|
||||
# override osversion only if it wasn't set by cli arguments
|
||||
download_urls=$(echo -e "$download_urls" | grep "\-macos")
|
||||
case $gamarch in
|
||||
x86_64)
|
||||
archgrep="\-x86_64"
|
||||
;;
|
||||
arm|arm64|aarch64)
|
||||
archgrep="\-aarch64"
|
||||
;;
|
||||
*)
|
||||
echo_red "ERROR: this installer currently only supports x86_64 and arm64 MacOS. Looks like you're running on ${gamarch}. Exiting."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
gam_macos_urls=$(echo -e "$download_urls" | \
|
||||
grep "$archgrep")
|
||||
versionless_urls=$(echo -e "$gam_macos_urls" | \
|
||||
grep "\-macos-")
|
||||
if [ "$versionless_urls" == "" ]; then
|
||||
# versions after 7.00.38 include MacOS version info
|
||||
gam_macos_vers=$(echo -e "$gam_macos_urls" | \
|
||||
grep --only-matching '\-macos[0-9\.]*' | \
|
||||
cut -c 7-10)
|
||||
for gam_mac_ver in $gam_macos_vers; do
|
||||
if version_gt $currentversion $gam_mac_ver; then
|
||||
download_url=$(echo -e "$gam_macos_urls" | grep "$gam_mac_ver")
|
||||
echo_green "You are running MacOS ${currentversion} Using GAM compiled against ${gam_mac_ver}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z ${download_url+x} ]; then
|
||||
echo_red "Sorry, you are running MacOS ${osversion} but GAM on ${gamarch} requires MacOS ${gam_mac_ver} or newer. Exiting."
|
||||
exit
|
||||
fi
|
||||
else
|
||||
# versions 7.00.38 and older don't include version info
|
||||
case $gamarch in
|
||||
x86_64)
|
||||
minimum_version=13
|
||||
download_url=$(echo -e "$download_urls" | grep "\-x86_64")
|
||||
;;
|
||||
arm|arm64|aarch64)
|
||||
download_url=$(echo -e "$download_urls" | grep "\-aarch64")
|
||||
minimum_version=14
|
||||
;;
|
||||
esac
|
||||
if version_gt "$osversion" "$minimum_version"; then
|
||||
echo_green "You are running MacOS ${osversion}, good. Downloading GAM from ${download_url}."
|
||||
else
|
||||
echo_red "Sorry, you are running MacOS ${osversion} but GAM on ${gamarch} requires MacOS ${minimum_version}. Exiting."
|
||||
exit
|
||||
fi
|
||||
if [ -z ${download_url+x} ]; then
|
||||
echo_red "Sorry, you are running MacOS ${currentversion} but GAM on ${gamarch} requires MacOS ${minimum_version}. Exiting."
|
||||
exit
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
MINGW64_NT*)
|
||||
gamos="windows"
|
||||
echo "You are running Windows"
|
||||
download_url=$(echo -e "$download_urls" | \
|
||||
grep "\-windows-" | \
|
||||
grep ".zip")
|
||||
;;
|
||||
*)
|
||||
echo_red "Sorry, this installer currently only supports Linux and MacOS. Looks like you're running on ${gamos}. Exiting."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
# Temp dir for archive
|
||||
temp_archive_dir=$(mktemp -d)
|
||||
temp_archive_dir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
|
||||
|
||||
echo_yellow "Downloading file $name from $browser_download_url to $temp_archive_dir."
|
||||
# Clean up after ourselves even if we are killed with CTRL-C
|
||||
trap "rm -rf $temp_archive_dir" EXIT
|
||||
|
||||
# hack to grab the end of the URL which should be the filename.
|
||||
name=$(echo -e "$download_url" | rev | cut -f1 -d "/" | rev)
|
||||
|
||||
echo_yellow "Downloading ${download_url} to $temp_archive_dir ($check_type)..."
|
||||
# Save archive to temp w/o losing our path
|
||||
(cd $temp_archive_dir && curl -O -L $browser_download_url)
|
||||
(cd "$temp_archive_dir" && curl -O -L -s "${curl_opts[@]}" "$download_url")
|
||||
|
||||
mkdir -p $target_dir
|
||||
mkdir -p "$target_dir"
|
||||
|
||||
echo_yellow "Extracting archive to $target_dir"
|
||||
tar xf $temp_archive_dir/$name -C $target_dir
|
||||
if [[ "$name" =~ tar.xz|tar.gz|tar ]]; then
|
||||
tar $strip_gam -xf "$temp_archive_dir"/"$name" -C "$target_dir"
|
||||
elif [[ "$name" == *.zip ]]; then
|
||||
unzip -o "${temp_archive_dir}/${name}" -d "${target_dir}"
|
||||
else
|
||||
echo "I don't know what to do with files like ${name}. Giving up."
|
||||
exit 1
|
||||
fi
|
||||
rc=$?
|
||||
if (( $rc != 0 )); then
|
||||
echo_red "ERROR: extracting the GAM archive with tar failed with error $rc. Exiting."
|
||||
@@ -154,16 +343,34 @@ fi
|
||||
|
||||
# Update profile to add gam command
|
||||
if [ "$update_profile" = true ]; then
|
||||
alias_line="alias gam=\"$target_dir/gam/gam\""
|
||||
alias_line="alias gam=\"${target_dir// /\\ }/$target_gam\""
|
||||
if [ "$gamos" == "linux" ]; then
|
||||
update_profile "$HOME/.bashrc" || update_profile "$HOME/.bash_profile"
|
||||
update_profile "$HOME/.bash_aliases" 0 || update_profile "$HOME/.bash_profile" 0 || update_profile "$HOME/.bashrc" 0
|
||||
update_profile "$HOME/.zshrc" 0
|
||||
elif [ "$gamos" == "macos" ]; then
|
||||
update_profile "$HOME/.profile" || update_profile "$HOME/.bash_profile"
|
||||
update_profile "$HOME/.bash_aliases" 0 || update_profile "$HOME/.bash_profile" 0 || update_profile "$HOME/.bashrc" 0 || update_profile "$HOME/.profile" 1
|
||||
update_profile "$HOME/.zshrc" 1
|
||||
fi
|
||||
else
|
||||
echo_yellow "skipping profile update."
|
||||
fi
|
||||
|
||||
if [ "$upgrade_only" = true ]; then
|
||||
echo_green "Here's information about your GAM upgrade:"
|
||||
"$target_dir/$target_gam" version extended
|
||||
rc=$?
|
||||
if (( $rc != 0 )); then
|
||||
echo_red "ERROR: Failed running GAM for the first time with return code $rc. Please report this error to GAM mailing list. Exiting."
|
||||
exit
|
||||
fi
|
||||
|
||||
echo_green "GAM upgrade complete!"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Set config command
|
||||
#config_cmd="config no_browser false"
|
||||
|
||||
while true; do
|
||||
read -p "Can you run a full browser on this machine? (usually Y for MacOS, N for Linux if you SSH into this machine) " yn
|
||||
case $yn in
|
||||
@@ -171,7 +378,8 @@ while true; do
|
||||
break
|
||||
;;
|
||||
[Nn]*)
|
||||
touch $target_dir/gam/nobrowser.txt > /dev/null 2>&1
|
||||
# config_cmd="config no_browser true"
|
||||
touch "$target_dir/gam/nobrowser.txt" > /dev/null 2>&1
|
||||
break
|
||||
;;
|
||||
*)
|
||||
@@ -187,16 +395,17 @@ while true; do
|
||||
case $yn in
|
||||
[Yy]*)
|
||||
if [ "$adminuser" == "" ]; then
|
||||
read -p "Please enter your G Suite admin email address: " adminuser
|
||||
read -p "Please enter your Google Workspace admin email address: " adminuser
|
||||
fi
|
||||
$target_dir/gam/gam create project $adminuser
|
||||
# "$target_dir/$target_gam" $config_cmd create project $adminuser
|
||||
"$target_dir/$target_gam" create project $adminuser
|
||||
rc=$?
|
||||
if (( $rc == 0 )); then
|
||||
echo_green "Project creation complete."
|
||||
project_created=true
|
||||
break
|
||||
else
|
||||
echo_red "Projection creation failed. Trying again. Say N to skip projection creation."
|
||||
echo_red "Project creation failed. Trying again. Say N to skip project creation."
|
||||
fi
|
||||
;;
|
||||
[Nn]*)
|
||||
@@ -211,10 +420,11 @@ done
|
||||
|
||||
admin_authorized=false
|
||||
while $project_created; do
|
||||
read -p "Are you ready to authorize GAM to perform G Suite management operations as your admin account? (yes or no) " yn
|
||||
read -p "Are you ready to authorize GAM to perform Google Workspace management operations as your admin account? (yes or no) " yn
|
||||
case $yn in
|
||||
[Yy]*)
|
||||
$target_dir/gam/gam oauth create $adminuser
|
||||
# "$target_dir/$target_gam" $config_cmd oauth create $adminuser
|
||||
"$target_dir/$target_gam" oauth create $adminuser
|
||||
rc=$?
|
||||
if (( $rc == 0 )); then
|
||||
echo_green "Admin authorization complete."
|
||||
@@ -235,15 +445,16 @@ while $project_created; do
|
||||
done
|
||||
|
||||
service_account_authorized=false
|
||||
while $project_created; do
|
||||
read -p "Are you ready to authorize GAM to manage G Suite user data and settings? (yes or no) " yn
|
||||
while $admin_authorized; do
|
||||
read -p "Are you ready to authorize GAM to manage Google Workspace user data and settings? (yes or no) " yn
|
||||
case $yn in
|
||||
[Yy]*)
|
||||
if [ "$regularuser" == "" ]; then
|
||||
read -p "Please enter the email address of a regular G Suite user: " regularuser
|
||||
read -p "Please enter the email address of a regular Google Workspace user: " regularuser
|
||||
fi
|
||||
echo_yellow "Great! Checking service account scopes.This will fail the first time. Follow the steps to authorize and retry. It can take a few minutes for scopes to PASS after they've been authorized in the admin console."
|
||||
$target_dir/gam/gam user $adminuser check serviceaccount
|
||||
# "$target_dir/$target_gam" $config_cmd user $regularuser check serviceaccount
|
||||
"$target_dir/$target_gam" user $regularuser check serviceaccount
|
||||
rc=$?
|
||||
if (( $rc == 0 )); then
|
||||
echo_green "Service account authorization complete."
|
||||
@@ -254,7 +465,7 @@ while $project_created; do
|
||||
fi
|
||||
;;
|
||||
[Nn]*)
|
||||
echo -e "\nYou can authorize a service account later by running:\n\ngam check serviceaccount\n"
|
||||
echo -e "\nYou can authorize a service account later by running:\n\ngam user $adminuser check serviceaccount\n"
|
||||
break
|
||||
;;
|
||||
*)
|
||||
@@ -264,7 +475,8 @@ while $project_created; do
|
||||
done
|
||||
|
||||
echo_green "Here's information about your new GAM installation:"
|
||||
$target_dir/gam/gam version
|
||||
#"$target_dir/$target_gam" $config_cmd save version extended
|
||||
"$target_dir/$target_gam" version extended
|
||||
rc=$?
|
||||
if (( $rc != 0 )); then
|
||||
echo_red "ERROR: Failed running GAM for the first time with $rc. Please report this error to GAM mailing list. Exiting."
|
||||
@@ -276,5 +488,3 @@ if [ "$update_profile" = true ]; then
|
||||
echo_green "Please restart your terminal shell or to get started right away run:\n\n$alias_line"
|
||||
fi
|
||||
|
||||
# Clean up after ourselves even if we are killed with CTRL-C
|
||||
trap "rm -rf $temp_archive_dir" EXIT
|
||||
|
||||
@@ -1,71 +1,84 @@
|
||||
@echo(
|
||||
@set /p adminemail= "Please enter your G Suite admin email address: "
|
||||
:neworupgrade
|
||||
@echo.
|
||||
@set /p nu= "If you have installed any version of GAM on any computer for your domain, enter u to upgrade, otherwise enter n? [u or n] "
|
||||
@if /I "%nu%"=="u" (
|
||||
@ echo GAM installation and setup complete!
|
||||
@ goto alldone
|
||||
)
|
||||
@if /I not "%nu%"=="n" (
|
||||
@ echo.
|
||||
@ echo Please answer n or u.
|
||||
@ goto neworupgrade
|
||||
)
|
||||
|
||||
:createproject
|
||||
@echo(
|
||||
@echo.
|
||||
@set /p yn= "Are you ready to set up a Google API project for GAM? [y or n] "
|
||||
@if /I "%yn%"=="n" (
|
||||
@ echo(
|
||||
@ echo.
|
||||
@ echo You can create an API project later by running:
|
||||
@ echo(
|
||||
@ echo.
|
||||
@ echo gam create project
|
||||
@ goto alldone
|
||||
)
|
||||
@if /I not "%yn%"=="y" (
|
||||
@ echo(
|
||||
@ echo.
|
||||
@ echo Please answer y or n.
|
||||
@ goto createproject
|
||||
)
|
||||
|
||||
@set /p adminemail= "Please enter your Google Workspace admin email address: "
|
||||
|
||||
@gam create project %adminemail%
|
||||
@if not ERRORLEVEL 1 goto projectdone
|
||||
@echo(
|
||||
@echo Projection creation failed. Trying again. Say n to skip projection creation.
|
||||
@echo.
|
||||
@echo Project creation failed. Trying again. Say n to skip project creation.
|
||||
@goto createproject
|
||||
:projectdone
|
||||
|
||||
:adminauth
|
||||
@echo(
|
||||
@set /p yn= "Are you ready to authorize GAM to perform G Suite management operations as your admin account? [y or n] "
|
||||
@echo.
|
||||
@set /p yn= "Are you ready to authorize GAM to perform Google Workspace management operations as your admin account? [y or n] "
|
||||
@if /I "%yn%"=="n" (
|
||||
@ echo(
|
||||
@ echo.
|
||||
@ echo You can authorize an admin later by running:
|
||||
@ echo(
|
||||
@ echo.
|
||||
@ echo gam oauth create %adminemail%
|
||||
@ goto admindone
|
||||
)
|
||||
@if /I not "%yn%"=="y" (
|
||||
@ echo(
|
||||
@ echo.
|
||||
@ echo Please answer y or n.
|
||||
@ goto adminauth
|
||||
)
|
||||
@gam oauth create %adminemail%
|
||||
@if not ERRORLEVEL 1 goto admindone
|
||||
@echo(
|
||||
@echo.
|
||||
@echo Admin authorization failed. Trying again. Say n to skip admin authorization.
|
||||
@goto adminauth
|
||||
:admindone
|
||||
|
||||
:saauth
|
||||
@echo(
|
||||
@set /p yn= "Are you ready to authorize GAM to manage G Suite user data and settings? [y or n] "
|
||||
@echo.
|
||||
@set /p yn= "Are you ready to authorize GAM to manage Google Workspace user data and settings? [y or n] "
|
||||
@if /I "%yn%"=="n" (
|
||||
@ echo(
|
||||
@ echo.
|
||||
@ echo You can authorize a service account later by running:
|
||||
@ echo(
|
||||
@ echo.
|
||||
@ echo gam user %adminemail% check serviceaccount
|
||||
@ goto sadone
|
||||
)
|
||||
@if /I not "%yn%"=="y" (
|
||||
@ echo(
|
||||
@ echo.
|
||||
@ echo Please answer y or n.
|
||||
@ goto saauth
|
||||
)
|
||||
@echo(
|
||||
@set /p regularuser= "Please enter the email address of a regular G Suite user: "
|
||||
@echo.
|
||||
@set /p regularuser= "Please enter the email address of a regular Google Workspace user: "
|
||||
@echo Great! Checking service account scopes. This will fail the first time. Follow the steps to authorize and retry. It can take a few minutes for scopes to PASS after they've been authorized in the admin console.
|
||||
@gam user %regularuser% check serviceaccount
|
||||
@if not ERRORLEVEL 1 goto sadone
|
||||
@echo(
|
||||
@echo.
|
||||
@echo Service account authorization failed. Confirm you entered the scopes correctly in the admin console. It can take a few minutes for scopes to PASS after they are entered in the admin console so if you're sure you entered them correctly, go grab a coffee and then hit Y to try again. Say N to skip admin authorization.
|
||||
@goto saauth
|
||||
:sadone
|
||||
|
||||
11
src/gam.exe.manifest
Normal file
11
src/gam.exe.manifest
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 8.1 / Server 2012 R2 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||
<!-- Windows 10+ / Server 2016+ -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
10882
src/gam.py
10882
src/gam.py
File diff suppressed because it is too large
Load Diff
151
src/gam.spec
Normal file
151
src/gam.spec
Normal file
@@ -0,0 +1,151 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
from os import getenv
|
||||
import re
|
||||
from sys import platform
|
||||
|
||||
from PyInstaller.utils.hooks import copy_metadata
|
||||
|
||||
from gam.gamlib.glverlibs import GAM_VER_LIBS
|
||||
|
||||
|
||||
with open("gam/__init__.py") as f:
|
||||
version_file = f.read()
|
||||
version = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M).group(1)
|
||||
version_list = [int(i) for i in version.split('.')]
|
||||
while len(version_list) < 4:
|
||||
version_list.append(0)
|
||||
version_tuple = tuple(version_list)
|
||||
version_str = str(version_tuple)
|
||||
with open("version_info.txt.in") as f:
|
||||
version_info = f.read()
|
||||
version_info = version_info.replace("{VERSION}", version).replace(
|
||||
"{VERSION_TUPLE}", version_str
|
||||
)
|
||||
with open("version_info.txt", "w") as f:
|
||||
f.write(version_info)
|
||||
print(version_info)
|
||||
|
||||
datas = []
|
||||
for pkg in GAM_VER_LIBS:
|
||||
datas += copy_metadata(pkg, recursive=True)
|
||||
datas += [('gam/cbcm-v1.1beta1.json', '.')]
|
||||
datas += [('gam/contactdelegation-v1.json', '.')]
|
||||
datas += [('gam/datastudio-v1.json', '.')]
|
||||
datas += [('gam/serviceaccountlookup-v1.json', '.')]
|
||||
datas += [('cacerts.pem', '.')]
|
||||
hiddenimports = [
|
||||
'gam.gamlib.yubikey',
|
||||
]
|
||||
|
||||
runtime_hooks = []
|
||||
a = Analysis(
|
||||
['gam/__main__.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=runtime_hooks,
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=None,
|
||||
noarchive=False,
|
||||
)
|
||||
#print(f"datas from analysis:\n{a.datas}")
|
||||
for d in a.datas:
|
||||
if 'pyconfig' in d[0]:
|
||||
a.datas.remove(d)
|
||||
break
|
||||
#print(f"datas after pyconfig cleanup:\n{a.datas}")
|
||||
pyz = PYZ(a.pure,
|
||||
a.zipped_data,
|
||||
cipher=None)
|
||||
# requires Python 3.10+ but no one should be compiling
|
||||
# GAM with older versions anyway
|
||||
target_arch = None
|
||||
codesign_identity = None
|
||||
entitlements_file = None
|
||||
manifest = None
|
||||
version = 'version_info.txt'
|
||||
match platform:
|
||||
case "darwin":
|
||||
if getenv('arch') == 'universal2':
|
||||
target_arch = "universal2"
|
||||
|
||||
codesign_identity = getenv('codesign_identity')
|
||||
if codesign_identity:
|
||||
entitlements_file = '../.github/actions/entitlements.plist'
|
||||
strip = True
|
||||
case "win32":
|
||||
target_arch = None
|
||||
strip = False
|
||||
manifest = 'gam.exe.manifest'
|
||||
case _:
|
||||
target_arch = None
|
||||
strip = True
|
||||
name = 'gam'
|
||||
debug = False
|
||||
bootloader_ignore_signals = False
|
||||
upx = False
|
||||
console = True
|
||||
disable_windowed_traceback = False
|
||||
argv_emulation = False
|
||||
if getenv('PYINSTALLER_BUILD_ONEDIR') == 'yes':
|
||||
# Build one directory
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name=name,
|
||||
debug=debug,
|
||||
bootloader_ignore_signals=bootloader_ignore_signals,
|
||||
strip=strip,
|
||||
manifest=manifest,
|
||||
upx=upx,
|
||||
console=console,
|
||||
# put most everyting under a lib/ subfolder
|
||||
contents_directory='lib',
|
||||
disable_windowed_traceback=disable_windowed_traceback,
|
||||
argv_emulation=argv_emulation,
|
||||
target_arch=target_arch,
|
||||
codesign_identity=codesign_identity,
|
||||
entitlements_file=entitlements_file,
|
||||
version=version,
|
||||
)
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=strip,
|
||||
upx=upx,
|
||||
upx_exclude=[],
|
||||
name=name,
|
||||
)
|
||||
else:
|
||||
# Build one file
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name=name,
|
||||
debug=debug,
|
||||
bootloader_ignore_signals=bootloader_ignore_signals,
|
||||
manifest=manifest,
|
||||
strip=strip,
|
||||
upx=upx,
|
||||
console=console,
|
||||
disable_windowed_traceback=disable_windowed_traceback,
|
||||
argv_emulation=argv_emulation,
|
||||
target_arch=target_arch,
|
||||
codesign_identity=codesign_identity,
|
||||
entitlements_file=entitlements_file,
|
||||
version=version,
|
||||
)
|
||||
|
||||
44
src/gam.wxs
44
src/gam.wxs
@@ -2,11 +2,11 @@
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" >
|
||||
<Product
|
||||
Id="*"
|
||||
Name="GAM"
|
||||
Name="GAM7"
|
||||
Language="1033"
|
||||
Version="$(env.GAMVERSION)"
|
||||
Manufacturer="Jay Lee - jay0lee@gmail.com"
|
||||
UpgradeCode="15C5FD61-B04C-4E04-A26D-CD8424C19D9F">
|
||||
Manufacturer="GAM Team - google-apps-manager@googlegroups.com"
|
||||
UpgradeCode="D86B52B2-EFE9-4F9D-8BA3-9D84B9B2D319">
|
||||
<Package
|
||||
InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
|
||||
|
||||
@@ -22,17 +22,21 @@
|
||||
|
||||
<Feature
|
||||
Id="gam"
|
||||
Title="GAM"
|
||||
Title="GAM7"
|
||||
Level="1">
|
||||
<ComponentGroupRef Id="ProductComponents" />
|
||||
</Feature>
|
||||
</Product>
|
||||
|
||||
<Fragment>
|
||||
<SetDirectory Id="WINDOWSVOLUME" Value="[WindowsVolume]"/>
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="ROOTDRIVE">
|
||||
<Directory Id="INSTALLFOLDER" Name="GAM" />
|
||||
</Directory>
|
||||
<Directory Id="WINDOWSVOLUME">
|
||||
<Directory Id="INSTALLFOLDER" Name="GAM7">
|
||||
<Directory Id="lib" Name="lib">
|
||||
</Directory>
|
||||
</Directory>
|
||||
</Directory>
|
||||
</Directory>
|
||||
</Fragment>
|
||||
|
||||
@@ -41,23 +45,27 @@
|
||||
<ComponentGroup
|
||||
Id="ProductComponents"
|
||||
Directory="INSTALLFOLDER"
|
||||
Source="gam-64">
|
||||
<Component Id="gam_exe" Guid="886abc07-73c5-4acc-9f71-58daf62aabc1">
|
||||
Source="dist/gam/gam7">
|
||||
<Component Id="gam_exe" Guid="d046ea24-c9f8-40ca-84db-70b0119933ff">
|
||||
<File Name="gam.exe" KeyPath="yes" />
|
||||
<Environment Id="PATH" Name="PATH" Value="[INSTALLFOLDER]" Permanent="yes" Part="last" Action="set" System="yes" />
|
||||
</Component>
|
||||
<Component Id="license" Guid="7a15de2e-fb91-4d0a-b8bf-c8b19c68f569">
|
||||
<Component Id="license" Guid="c76864c5-d005-44d5-bb7c-a27e5923792d">
|
||||
<File Name="LICENSE" KeyPath="yes" />
|
||||
</Component>
|
||||
<Component Id="whatsnew_txt" Guid="6aa9863c-90d9-412f-9b73-fda82549a950">
|
||||
<File Name="whatsnew.txt" KeyPath="yes" />
|
||||
</Component>
|
||||
<Component Id="gam_setup_bat" Guid="ef01f93a-4b50-488a-9c04-ec5e13e66218">
|
||||
<Component Id="gam_setup_bat" Guid="5e6bbacb-d86f-4d80-a10b-89b81ee63fcb">
|
||||
<File Name="gam-setup.bat" KeyPath="yes" />
|
||||
</Component>
|
||||
<Component Id="gamcommands_txt" Guid="58ff9c45-a7c9-4e22-8845-a9a92610c1f3">
|
||||
<File Name="gamcommands.txt" KeyPath="yes" />
|
||||
<Component Id="GamCommands_txt" Guid="a2dca862-b222-469e-a637-95ea2a1c53e7">
|
||||
<File Name="GamCommands.txt" KeyPath="yes" />
|
||||
</Component>
|
||||
<Component Id="GamUpdate_txt" Guid="1b7cdd48-0fff-4943-a219-102fcd14c755">
|
||||
<File Name="GamUpdate.txt" KeyPath="yes" />
|
||||
</Component>
|
||||
<Component Id="cacerts_pem" Guid="61fe2b2d-1646-4bed-b844-193965e97727">
|
||||
<File Name="cacerts.pem" KeyPath="yes" />
|
||||
</Component>
|
||||
<ComponentGroupRef Id="Lib" />
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
|
||||
@@ -66,9 +74,5 @@
|
||||
<ExecuteAction />
|
||||
<Show Dialog="WelcomeDlg" Before="ProgressDlg" />
|
||||
</InstallUISequence>
|
||||
<CustomAction Id="setup_gam" ExeCommand="[INSTALLFOLDER]gam-setup.bat" Directory="INSTALLFOLDER" Execute="commit" Impersonate="yes" Return="asyncWait"/>
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action="setup_gam" After="InstallFiles" >NOT Installed AND NOT UPGRADINGPRODUCTCODE AND NOT WIX_UPGRADE_DETECTED</Custom>
|
||||
</InstallExecuteSequence>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
|
||||
77241
src/gam/__init__.py
Executable file
77241
src/gam/__init__.py
Executable file
File diff suppressed because it is too large
Load Diff
40
src/gam/__main__.py
Normal file
40
src/gam/__main__.py
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# GAM
|
||||
#
|
||||
# Copyright 2023, All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import multiprocessing
|
||||
import platform
|
||||
import sys
|
||||
|
||||
import gam
|
||||
|
||||
def main():
|
||||
gam.initializeLogging()
|
||||
rc = gam.ProcessGAMCommand(sys.argv)
|
||||
try:
|
||||
sys.stdout.flush()
|
||||
except (IOError, ValueError):
|
||||
pass
|
||||
sys.exit(rc)
|
||||
|
||||
# Run from command line
|
||||
if __name__ == '__main__':
|
||||
if platform.system() != 'Linux':
|
||||
multiprocessing.freeze_support()
|
||||
multiprocessing.set_start_method('spawn')
|
||||
main()
|
||||
1460
src/gam/atom/__init__.py
Normal file
1460
src/gam/atom/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
41
src/gam/atom/auth.py
Normal file
41
src/gam/atom/auth.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2009 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License 2.0;
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
# This module is used for version 2 of the Google Data APIs.
|
||||
|
||||
|
||||
# __author__ = 'j.s@google.com (Jeff Scudder)'
|
||||
|
||||
import base64
|
||||
|
||||
|
||||
class BasicAuth(object):
|
||||
"""Sets the Authorization header as defined in RFC1945"""
|
||||
|
||||
def __init__(self, user_id, password):
|
||||
self.basic_cookie = base64.encodestring(
|
||||
'%s:%s' % (user_id, password)).strip()
|
||||
|
||||
def modify_request(self, http_request):
|
||||
http_request.headers['Authorization'] = 'Basic %s' % self.basic_cookie
|
||||
|
||||
ModifyRequest = modify_request
|
||||
|
||||
|
||||
class NoAuth(object):
|
||||
def modify_request(self, http_request):
|
||||
pass
|
||||
214
src/gam/atom/client.py
Normal file
214
src/gam/atom/client.py
Normal file
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2009 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License 2.0;
|
||||
|
||||
|
||||
|
||||
"""AtomPubClient provides CRUD ops. in line with the Atom Publishing Protocol.
|
||||
|
||||
"""
|
||||
|
||||
# __author__ = 'j.s@google.com (Jeff Scudder)'
|
||||
|
||||
import atom.http_core
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MissingHost(Error):
|
||||
pass
|
||||
|
||||
|
||||
class AtomPubClient(object):
|
||||
host = None
|
||||
auth_token = None
|
||||
ssl = False # Whether to force all requests over https
|
||||
xoauth_requestor_id = None
|
||||
|
||||
def __init__(self, http_client=None, host=None, auth_token=None, source=None,
|
||||
xoauth_requestor_id=None, **kwargs):
|
||||
"""Creates a new AtomPubClient instance.
|
||||
|
||||
Args:
|
||||
source: The name of your application.
|
||||
http_client: An object capable of performing HTTP requests through a
|
||||
request method. This object is used to perform the request
|
||||
when the AtomPubClient's request method is called. Used to
|
||||
allow HTTP requests to be directed to a mock server, or use
|
||||
an alternate library instead of the default of httplib to
|
||||
make HTTP requests.
|
||||
host: str The default host name to use if a host is not specified in the
|
||||
requested URI.
|
||||
auth_token: An object which sets the HTTP Authorization header when its
|
||||
modify_request method is called.
|
||||
"""
|
||||
self.http_client = http_client or atom.http_core.ProxiedHttpClient()
|
||||
if host is not None:
|
||||
self.host = host
|
||||
if auth_token is not None:
|
||||
self.auth_token = auth_token
|
||||
self.xoauth_requestor_id = xoauth_requestor_id
|
||||
self.source = source
|
||||
|
||||
def request(self, method=None, uri=None, auth_token=None,
|
||||
http_request=None, **kwargs):
|
||||
"""Performs an HTTP request to the server indicated.
|
||||
|
||||
Uses the http_client instance to make the request.
|
||||
|
||||
Args:
|
||||
method: The HTTP method as a string, usually one of 'GET', 'POST',
|
||||
'PUT', or 'DELETE'
|
||||
uri: The URI desired as a string or atom.http_core.Uri.
|
||||
http_request:
|
||||
auth_token: An authorization token object whose modify_request method
|
||||
sets the HTTP Authorization header.
|
||||
|
||||
Returns:
|
||||
The results of calling self.http_client.request. With the default
|
||||
http_client, this is an HTTP response object.
|
||||
"""
|
||||
# Modify the request based on the AtomPubClient settings and parameters
|
||||
# passed in to the request.
|
||||
http_request = self.modify_request(http_request)
|
||||
if isinstance(uri, str):
|
||||
uri = atom.http_core.Uri.parse_uri(uri)
|
||||
if uri is not None:
|
||||
uri.modify_request(http_request)
|
||||
if isinstance(method, str):
|
||||
http_request.method = method
|
||||
# Any unrecognized arguments are assumed to be capable of modifying the
|
||||
# HTTP request.
|
||||
for name, value in kwargs.items():
|
||||
if value is not None:
|
||||
if hasattr(value, 'modify_request'):
|
||||
value.modify_request(http_request)
|
||||
else:
|
||||
http_request.uri.query[name] = str(value)
|
||||
# Default to an http request if the protocol scheme is not set.
|
||||
if http_request.uri.scheme is None:
|
||||
http_request.uri.scheme = 'http'
|
||||
# Override scheme. Force requests over https.
|
||||
if self.ssl:
|
||||
http_request.uri.scheme = 'https'
|
||||
if http_request.uri.path is None:
|
||||
http_request.uri.path = '/'
|
||||
# Add the Authorization header at the very end. The Authorization header
|
||||
# value may need to be calculated using information in the request.
|
||||
if auth_token:
|
||||
auth_token.modify_request(http_request)
|
||||
elif self.auth_token:
|
||||
self.auth_token.modify_request(http_request)
|
||||
# Check to make sure there is a host in the http_request.
|
||||
if http_request.uri.host is None:
|
||||
raise MissingHost('No host provided in request %s %s' % (
|
||||
http_request.method, str(http_request.uri)))
|
||||
# Perform the fully specified request using the http_client instance.
|
||||
# Sends the request to the server and returns the server's response.
|
||||
return self.http_client.request(http_request)
|
||||
|
||||
Request = request
|
||||
|
||||
def get(self, uri=None, auth_token=None, http_request=None, **kwargs):
|
||||
"""Performs a request using the GET method, returns an HTTP response."""
|
||||
return self.request(method='GET', uri=uri, auth_token=auth_token,
|
||||
http_request=http_request, **kwargs)
|
||||
|
||||
Get = get
|
||||
|
||||
def post(self, uri=None, data=None, auth_token=None, http_request=None,
|
||||
**kwargs):
|
||||
"""Sends data using the POST method, returns an HTTP response."""
|
||||
return self.request(method='POST', uri=uri, auth_token=auth_token,
|
||||
http_request=http_request, data=data, **kwargs)
|
||||
|
||||
Post = post
|
||||
|
||||
def put(self, uri=None, data=None, auth_token=None, http_request=None,
|
||||
**kwargs):
|
||||
"""Sends data using the PUT method, returns an HTTP response."""
|
||||
return self.request(method='PUT', uri=uri, auth_token=auth_token,
|
||||
http_request=http_request, data=data, **kwargs)
|
||||
|
||||
Put = put
|
||||
|
||||
def delete(self, uri=None, auth_token=None, http_request=None, **kwargs):
|
||||
"""Performs a request using the DELETE method, returns an HTTP response."""
|
||||
return self.request(method='DELETE', uri=uri, auth_token=auth_token,
|
||||
http_request=http_request, **kwargs)
|
||||
|
||||
Delete = delete
|
||||
|
||||
def modify_request(self, http_request):
|
||||
"""Changes the HTTP request before sending it to the server.
|
||||
|
||||
Sets the User-Agent HTTP header and fills in the HTTP host portion
|
||||
of the URL if one was not included in the request (for this it uses
|
||||
the self.host member if one is set). This method is called in
|
||||
self.request.
|
||||
|
||||
Args:
|
||||
http_request: An atom.http_core.HttpRequest() (optional) If one is
|
||||
not provided, a new HttpRequest is instantiated.
|
||||
|
||||
Returns:
|
||||
An atom.http_core.HttpRequest() with the User-Agent header set and
|
||||
if this client has a value in its host member, the host in the request
|
||||
URL is set.
|
||||
"""
|
||||
if http_request is None:
|
||||
http_request = atom.http_core.HttpRequest()
|
||||
|
||||
if self.host is not None and http_request.uri.host is None:
|
||||
http_request.uri.host = self.host
|
||||
|
||||
if self.xoauth_requestor_id is not None:
|
||||
http_request.uri.query['xoauth_requestor_id'] = self.xoauth_requestor_id
|
||||
|
||||
# Set the user agent header for logging purposes.
|
||||
if self.source:
|
||||
http_request.headers['User-Agent'] = '%s gdata-py/2.0.18' % self.source
|
||||
else:
|
||||
http_request.headers['User-Agent'] = 'gdata-py/2.0.17'
|
||||
|
||||
return http_request
|
||||
|
||||
ModifyRequest = modify_request
|
||||
|
||||
|
||||
class CustomHeaders(object):
|
||||
"""Add custom headers to an http_request.
|
||||
|
||||
Usage:
|
||||
>>> custom_headers = atom.client.CustomHeaders(header1='value1',
|
||||
header2='value2')
|
||||
>>> client.get(uri, custom_headers=custom_headers)
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Creates a CustomHeaders instance.
|
||||
|
||||
Initialize the headers dictionary with the arguments list.
|
||||
"""
|
||||
self.headers = kwargs
|
||||
|
||||
def modify_request(self, http_request):
|
||||
"""Changes the HTTP request before sending it to the server.
|
||||
|
||||
Adds the custom headers to the HTTP request.
|
||||
|
||||
Args:
|
||||
http_request: An atom.http_core.HttpRequest().
|
||||
|
||||
Returns:
|
||||
An atom.http_core.HttpRequest() with the added custom headers.
|
||||
"""
|
||||
|
||||
for name, value in self.headers.items():
|
||||
if value is not None:
|
||||
http_request.headers[name] = value
|
||||
return http_request
|
||||
535
src/gam/atom/core.py
Normal file
535
src/gam/atom/core.py
Normal file
@@ -0,0 +1,535 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License 2.0;
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
# This module is used for version 2 of the Google Data APIs.
|
||||
|
||||
|
||||
# __author__ = 'j.s@google.com (Jeff Scudder)'
|
||||
|
||||
import inspect
|
||||
|
||||
import lxml.etree as ElementTree
|
||||
|
||||
try:
|
||||
from xml.dom.minidom import parseString as xmlString
|
||||
except ImportError:
|
||||
xmlString = None
|
||||
|
||||
STRING_ENCODING = 'utf-8'
|
||||
|
||||
|
||||
class XmlElement(object):
|
||||
"""Represents an element node in an XML document.
|
||||
|
||||
The text member is a UTF-8 encoded str or unicode.
|
||||
"""
|
||||
_qname = None
|
||||
_other_elements = None
|
||||
_other_attributes = None
|
||||
# The rule set contains mappings for XML qnames to child members and the
|
||||
# appropriate member classes.
|
||||
_rule_set = None
|
||||
_members = None
|
||||
text = None
|
||||
|
||||
def __init__(self, text=None, *args, **kwargs):
|
||||
if ('_members' not in self.__class__.__dict__
|
||||
or self.__class__._members is None):
|
||||
self.__class__._members = tuple(self.__class__._list_xml_members())
|
||||
for member_name, member_type in self.__class__._members:
|
||||
if member_name in kwargs:
|
||||
setattr(self, member_name, kwargs[member_name])
|
||||
else:
|
||||
if isinstance(member_type, list):
|
||||
setattr(self, member_name, [])
|
||||
else:
|
||||
setattr(self, member_name, None)
|
||||
self._other_elements = []
|
||||
self._other_attributes = {}
|
||||
if text is not None:
|
||||
self.text = text
|
||||
|
||||
def _list_xml_members(cls):
|
||||
"""Generator listing all members which are XML elements or attributes.
|
||||
|
||||
The following members would be considered XML members:
|
||||
foo = 'abc' - indicates an XML attribute with the qname abc
|
||||
foo = SomeElement - indicates an XML child element
|
||||
foo = [AnElement] - indicates a repeating XML child element, each instance
|
||||
will be stored in a list in this member
|
||||
foo = ('att1', '{http://example.com/namespace}att2') - indicates an XML
|
||||
attribute which has different parsing rules in different versions of
|
||||
the protocol. Version 1 of the XML parsing rules will look for an
|
||||
attribute with the qname 'att1' but verion 2 of the parsing rules will
|
||||
look for a namespaced attribute with the local name of 'att2' and an
|
||||
XML namespace of 'http://example.com/namespace'.
|
||||
"""
|
||||
members = []
|
||||
for pair in inspect.getmembers(cls):
|
||||
if not pair[0].startswith('_') and pair[0] != 'text':
|
||||
member_type = pair[1]
|
||||
if (isinstance(member_type, tuple) or isinstance(member_type, list)
|
||||
or isinstance(member_type, str)
|
||||
or (inspect.isclass(member_type)
|
||||
and issubclass(member_type, XmlElement))):
|
||||
members.append(pair)
|
||||
return members
|
||||
|
||||
_list_xml_members = classmethod(_list_xml_members)
|
||||
|
||||
def _get_rules(cls, version):
|
||||
"""Initializes the _rule_set for the class which is used when parsing XML.
|
||||
|
||||
This method is used internally for parsing and generating XML for an
|
||||
XmlElement. It is not recommended that you call this method directly.
|
||||
|
||||
Returns:
|
||||
A tuple containing the XML parsing rules for the appropriate version.
|
||||
|
||||
The tuple looks like:
|
||||
(qname, {sub_element_qname: (member_name, member_class, repeating), ..},
|
||||
{attribute_qname: member_name})
|
||||
|
||||
To give a couple of concrete example, the atom.data.Control _get_rules
|
||||
with version of 2 will return:
|
||||
('{http://www.w3.org/2007/app}control',
|
||||
{'{http://www.w3.org/2007/app}draft': ('draft',
|
||||
<class 'atom.data.Draft'>,
|
||||
False)},
|
||||
{})
|
||||
Calling _get_rules with version 1 on gdata.data.FeedLink will produce:
|
||||
('{http://schemas.google.com/g/2005}feedLink',
|
||||
{'{http://www.w3.org/2005/Atom}feed': ('feed',
|
||||
<class 'gdata.data.GDFeed'>,
|
||||
False)},
|
||||
{'href': 'href', 'readOnly': 'read_only', 'countHint': 'count_hint',
|
||||
'rel': 'rel'})
|
||||
"""
|
||||
# Initialize the _rule_set to make sure there is a slot available to store
|
||||
# the parsing rules for this version of the XML schema.
|
||||
# Look for rule set in the class __dict__ proxy so that only the
|
||||
# _rule_set for this class will be found. By using the dict proxy
|
||||
# we avoid finding rule_sets defined in superclasses.
|
||||
# The four lines below provide support for any number of versions, but it
|
||||
# runs a bit slower then hard coding slots for two versions, so I'm using
|
||||
# the below two lines.
|
||||
# if '_rule_set' not in cls.__dict__ or cls._rule_set is None:
|
||||
# cls._rule_set = []
|
||||
# while len(cls.__dict__['_rule_set']) < version:
|
||||
# cls._rule_set.append(None)
|
||||
# If there is no rule set cache in the class, provide slots for two XML
|
||||
# versions. If and when there is a version 3, this list will need to be
|
||||
# expanded.
|
||||
if '_rule_set' not in cls.__dict__ or cls._rule_set is None:
|
||||
cls._rule_set = [None, None]
|
||||
# If a version higher than 2 is requested, fall back to version 2 because
|
||||
# 2 is currently the highest supported version.
|
||||
if version > 2:
|
||||
return cls._get_rules(2)
|
||||
# Check the dict proxy for the rule set to avoid finding any rule sets
|
||||
# which belong to the superclass. We only want rule sets for this class.
|
||||
if cls._rule_set[version - 1] is None:
|
||||
# The rule set for each version consists of the qname for this element
|
||||
# ('{namespace}tag'), a dictionary (elements) for looking up the
|
||||
# corresponding class member when given a child element's qname, and a
|
||||
# dictionary (attributes) for looking up the corresponding class member
|
||||
# when given an XML attribute's qname.
|
||||
elements = {}
|
||||
attributes = {}
|
||||
if ('_members' not in cls.__dict__ or cls._members is None):
|
||||
cls._members = tuple(cls._list_xml_members())
|
||||
for member_name, target in cls._members:
|
||||
if isinstance(target, list):
|
||||
# This member points to a repeating element.
|
||||
elements[_get_qname(target[0], version)] = (member_name, target[0],
|
||||
True)
|
||||
elif isinstance(target, tuple):
|
||||
# This member points to a versioned XML attribute.
|
||||
if version <= len(target):
|
||||
attributes[target[version - 1]] = member_name
|
||||
else:
|
||||
attributes[target[-1]] = member_name
|
||||
elif isinstance(target, str):
|
||||
# This member points to an XML attribute.
|
||||
attributes[target] = member_name
|
||||
elif issubclass(target, XmlElement):
|
||||
# This member points to a single occurance element.
|
||||
elements[_get_qname(target, version)] = (member_name, target, False)
|
||||
version_rules = (_get_qname(cls, version), elements, attributes)
|
||||
cls._rule_set[version - 1] = version_rules
|
||||
return version_rules
|
||||
else:
|
||||
return cls._rule_set[version - 1]
|
||||
|
||||
_get_rules = classmethod(_get_rules)
|
||||
|
||||
def get_elements(self, tag=None, namespace=None, version=1):
|
||||
"""Find all sub elements which match the tag and namespace.
|
||||
|
||||
To find all elements in this object, call get_elements with the tag and
|
||||
namespace both set to None (the default). This method searches through
|
||||
the object's members and the elements stored in _other_elements which
|
||||
did not match any of the XML parsing rules for this class.
|
||||
|
||||
Args:
|
||||
tag: str
|
||||
namespace: str
|
||||
version: int Specifies the version of the XML rules to be used when
|
||||
searching for matching elements.
|
||||
|
||||
Returns:
|
||||
A list of the matching XmlElements.
|
||||
"""
|
||||
matches = []
|
||||
ignored1, elements, ignored2 = self.__class__._get_rules(version)
|
||||
if elements:
|
||||
for qname, element_def in elements.items():
|
||||
member = getattr(self, element_def[0])
|
||||
if member:
|
||||
if _qname_matches(tag, namespace, qname):
|
||||
if element_def[2]:
|
||||
# If this is a repeating element, copy all instances into the
|
||||
# result list.
|
||||
matches.extend(member)
|
||||
else:
|
||||
matches.append(member)
|
||||
for element in self._other_elements:
|
||||
if _qname_matches(tag, namespace, element._qname):
|
||||
matches.append(element)
|
||||
return matches
|
||||
|
||||
GetElements = get_elements
|
||||
# FindExtensions and FindChildren are provided for backwards compatibility
|
||||
# to the atom.AtomBase class.
|
||||
# However, FindExtensions may return more results than the v1 atom.AtomBase
|
||||
# method does, because get_elements searches both the expected children
|
||||
# and the unexpected "other elements". The old AtomBase.FindExtensions
|
||||
# method searched only "other elements" AKA extension_elements.
|
||||
FindExtensions = get_elements
|
||||
FindChildren = get_elements
|
||||
|
||||
def get_attributes(self, tag=None, namespace=None, version=1):
|
||||
"""Find all attributes which match the tag and namespace.
|
||||
|
||||
To find all attributes in this object, call get_attributes with the tag
|
||||
and namespace both set to None (the default). This method searches
|
||||
through the object's members and the attributes stored in
|
||||
_other_attributes which did not fit any of the XML parsing rules for this
|
||||
class.
|
||||
|
||||
Args:
|
||||
tag: str
|
||||
namespace: str
|
||||
version: int Specifies the version of the XML rules to be used when
|
||||
searching for matching attributes.
|
||||
|
||||
Returns:
|
||||
A list of XmlAttribute objects for the matching attributes.
|
||||
"""
|
||||
matches = []
|
||||
ignored1, ignored2, attributes = self.__class__._get_rules(version)
|
||||
if attributes:
|
||||
for qname, attribute_def in attributes.items():
|
||||
if isinstance(attribute_def, (list, tuple)):
|
||||
attribute_def = attribute_def[0]
|
||||
member = getattr(self, attribute_def)
|
||||
# TODO: ensure this hasn't broken existing behavior.
|
||||
# member = getattr(self, attribute_def[0])
|
||||
if member:
|
||||
if _qname_matches(tag, namespace, qname):
|
||||
matches.append(XmlAttribute(qname, member))
|
||||
for qname, value in self._other_attributes.items():
|
||||
if _qname_matches(tag, namespace, qname):
|
||||
matches.append(XmlAttribute(qname, value))
|
||||
return matches
|
||||
|
||||
GetAttributes = get_attributes
|
||||
|
||||
def _harvest_tree(self, tree, version=1):
|
||||
"""Populates object members from the data in the tree Element."""
|
||||
qname, elements, attributes = self.__class__._get_rules(version)
|
||||
for element in tree:
|
||||
if elements and element.tag in elements:
|
||||
definition = elements[element.tag]
|
||||
# If this is a repeating element, make sure the member is set to a
|
||||
# list.
|
||||
if definition[2]:
|
||||
if getattr(self, definition[0]) is None:
|
||||
setattr(self, definition[0], [])
|
||||
getattr(self, definition[0]).append(_xml_element_from_tree(element,
|
||||
definition[1], version))
|
||||
else:
|
||||
setattr(self, definition[0], _xml_element_from_tree(element,
|
||||
definition[1], version))
|
||||
else:
|
||||
self._other_elements.append(_xml_element_from_tree(element, XmlElement,
|
||||
version))
|
||||
for attrib, value in tree.attrib.items():
|
||||
if attributes and attrib in attributes:
|
||||
setattr(self, attributes[attrib], value)
|
||||
else:
|
||||
self._other_attributes[attrib] = value
|
||||
if tree.text:
|
||||
self.text = tree.text
|
||||
|
||||
def _to_tree(self, version=1):
|
||||
new_tree = ElementTree.Element(_get_qname(self, version))
|
||||
self._attach_members(new_tree, version)
|
||||
return new_tree
|
||||
|
||||
def _attach_members(self, tree, version=1):
|
||||
"""Convert members to XML elements/attributes and add them to the tree.
|
||||
|
||||
Args:
|
||||
tree: An ElementTree.Element which will be modified. The members of
|
||||
this object will be added as child elements or attributes
|
||||
according to the rules described in _expected_elements and
|
||||
_expected_attributes. The elements and attributes stored in
|
||||
other_attributes and other_elements are also added a children
|
||||
of this tree.
|
||||
version: int Ingnored in this method but used by VersionedElement.
|
||||
encoding: str (optional)
|
||||
"""
|
||||
qname, elements, attributes = self.__class__._get_rules(version)
|
||||
encoding = STRING_ENCODING
|
||||
# Add the expected elements and attributes to the tree.
|
||||
if elements:
|
||||
for tag, element_def in elements.items():
|
||||
member = getattr(self, element_def[0])
|
||||
# If this is a repeating element and there are members in the list.
|
||||
if member and element_def[2]:
|
||||
for instance in member:
|
||||
instance._become_child(tree, version)
|
||||
elif member:
|
||||
member._become_child(tree, version)
|
||||
if attributes:
|
||||
for attribute_tag, member_name in attributes.items():
|
||||
value = getattr(self, member_name)
|
||||
if value:
|
||||
tree.attrib[attribute_tag] = value
|
||||
# Add the unexpected (other) elements and attributes to the tree.
|
||||
for element in self._other_elements:
|
||||
element._become_child(tree, version)
|
||||
for key, value in self._other_attributes.items():
|
||||
# I'm not sure if unicode can be used in the attribute name, so for now
|
||||
# we assume the encoding is correct for the attribute name.
|
||||
if not isinstance(value, str):
|
||||
value = value.decode(encoding)
|
||||
tree.attrib[key] = value
|
||||
if self.text:
|
||||
if isinstance(self.text, str):
|
||||
tree.text = self.text
|
||||
else:
|
||||
tree.text = self.text.decode(encoding)
|
||||
|
||||
def to_string(self, version=1, encoding=None, pretty_print=None):
|
||||
"""Converts this object to XML."""
|
||||
|
||||
tree_string = ElementTree.tostring(self._to_tree(version))
|
||||
|
||||
if pretty_print and xmlString is not None:
|
||||
return xmlString(tree_string).toprettyxml()
|
||||
|
||||
return tree_string
|
||||
|
||||
ToString = to_string
|
||||
|
||||
def __str__(self):
|
||||
return self.to_string()
|
||||
|
||||
def _become_child(self, tree, version=1):
|
||||
"""Adds a child element to tree with the XML data in self."""
|
||||
new_child = ElementTree.Element(_get_qname(self, version))
|
||||
tree.append(new_child)
|
||||
new_child.tag = _get_qname(self, version)
|
||||
self._attach_members(new_child, version)
|
||||
|
||||
def __get_extension_elements(self):
|
||||
return self._other_elements
|
||||
|
||||
def __set_extension_elements(self, elements):
|
||||
self._other_elements = elements
|
||||
|
||||
extension_elements = property(__get_extension_elements,
|
||||
__set_extension_elements,
|
||||
"""Provides backwards compatibility for v1 atom.AtomBase classes.""")
|
||||
|
||||
def __get_extension_attributes(self):
|
||||
return self._other_attributes
|
||||
|
||||
def __set_extension_attributes(self, attributes):
|
||||
self._other_attributes = attributes
|
||||
|
||||
extension_attributes = property(__get_extension_attributes,
|
||||
__set_extension_attributes,
|
||||
"""Provides backwards compatibility for v1 atom.AtomBase classes.""")
|
||||
|
||||
def _get_tag(self, version=1):
|
||||
qname = _get_qname(self, version)
|
||||
if qname:
|
||||
return qname[qname.find('}') + 1:]
|
||||
return None
|
||||
|
||||
def _get_namespace(self, version=1):
|
||||
qname = _get_qname(self, version)
|
||||
if qname.startswith('{'):
|
||||
return qname[1:qname.find('}')]
|
||||
else:
|
||||
return None
|
||||
|
||||
def _set_tag(self, tag):
|
||||
if isinstance(self._qname, tuple):
|
||||
self._qname = self._qname.copy()
|
||||
if self._qname[0].startswith('{'):
|
||||
self._qname[0] = '{%s}%s' % (self._get_namespace(1), tag)
|
||||
else:
|
||||
self._qname[0] = tag
|
||||
else:
|
||||
if self._qname is not None and self._qname.startswith('{'):
|
||||
self._qname = '{%s}%s' % (self._get_namespace(), tag)
|
||||
else:
|
||||
self._qname = tag
|
||||
|
||||
def _set_namespace(self, namespace):
|
||||
tag = self._get_tag(1)
|
||||
if tag is None:
|
||||
tag = ''
|
||||
if isinstance(self._qname, tuple):
|
||||
self._qname = self._qname.copy()
|
||||
if namespace:
|
||||
self._qname[0] = '{%s}%s' % (namespace, tag)
|
||||
else:
|
||||
self._qname[0] = tag
|
||||
else:
|
||||
if namespace:
|
||||
self._qname = '{%s}%s' % (namespace, tag)
|
||||
else:
|
||||
self._qname = tag
|
||||
|
||||
tag = property(_get_tag, _set_tag,
|
||||
"""Provides backwards compatibility for v1 atom.AtomBase classes.""")
|
||||
|
||||
namespace = property(_get_namespace, _set_namespace,
|
||||
"""Provides backwards compatibility for v1 atom.AtomBase classes.""")
|
||||
|
||||
# Provided for backwards compatibility to atom.ExtensionElement
|
||||
children = extension_elements
|
||||
attributes = extension_attributes
|
||||
|
||||
|
||||
def _get_qname(element, version):
|
||||
if isinstance(element._qname, tuple):
|
||||
if version <= len(element._qname):
|
||||
return element._qname[version - 1]
|
||||
else:
|
||||
return element._qname[-1]
|
||||
else:
|
||||
return element._qname
|
||||
|
||||
|
||||
def _qname_matches(tag, namespace, qname):
|
||||
"""Logic determines if a QName matches the desired local tag and namespace.
|
||||
|
||||
This is used in XmlElement.get_elements and XmlElement.get_attributes to
|
||||
find matches in the element's members (among all expected-and-unexpected
|
||||
elements-and-attributes).
|
||||
|
||||
Args:
|
||||
expected_tag: string
|
||||
expected_namespace: string
|
||||
qname: string in the form '{xml_namespace}localtag' or 'tag' if there is
|
||||
no namespace.
|
||||
|
||||
Returns:
|
||||
boolean True if the member's tag and namespace fit the expected tag and
|
||||
namespace.
|
||||
"""
|
||||
# If there is no expected namespace or tag, then everything will match.
|
||||
if qname is None:
|
||||
member_tag = None
|
||||
member_namespace = None
|
||||
else:
|
||||
if qname.startswith('{'):
|
||||
member_namespace = qname[1:qname.index('}')]
|
||||
member_tag = qname[qname.index('}') + 1:]
|
||||
else:
|
||||
member_namespace = None
|
||||
member_tag = qname
|
||||
return ((tag is None and namespace is None)
|
||||
# If there is a tag, but no namespace, see if the local tag matches.
|
||||
or (namespace is None and member_tag == tag)
|
||||
# There was no tag, but there was a namespace so see if the namespaces
|
||||
# match.
|
||||
or (tag is None and member_namespace == namespace)
|
||||
# There was no tag, and the desired elements have no namespace, so check
|
||||
# to see that the member's namespace is None.
|
||||
or (tag is None and namespace == ''
|
||||
and member_namespace is None)
|
||||
# The tag and the namespace both match.
|
||||
or (tag == member_tag
|
||||
and namespace == member_namespace)
|
||||
# The tag matches, and the expected namespace is the empty namespace,
|
||||
# check to make sure the member's namespace is None.
|
||||
or (tag == member_tag and namespace == ''
|
||||
and member_namespace is None))
|
||||
|
||||
|
||||
def parse(xml_string, target_class=None, version=1):
|
||||
"""Parses the XML string according to the rules for the target_class.
|
||||
|
||||
Args:
|
||||
xml_string: bytes
|
||||
target_class: XmlElement or a subclass. If None is specified, the
|
||||
XmlElement class is used.
|
||||
version: int (optional) The version of the schema which should be used when
|
||||
converting the XML into an object. The default is 1.
|
||||
encoding: str (optional) The character encoding of the bytes in the
|
||||
xml_string. Default is 'UTF-8'.
|
||||
"""
|
||||
if target_class is None:
|
||||
target_class = XmlElement
|
||||
if not isinstance(xml_string, bytes):
|
||||
raise Exception("This function only accepts bytes")
|
||||
tree = ElementTree.fromstring(xml_string)
|
||||
return _xml_element_from_tree(tree, target_class, version)
|
||||
|
||||
|
||||
Parse = parse
|
||||
xml_element_from_string = parse
|
||||
XmlElementFromString = xml_element_from_string
|
||||
|
||||
|
||||
def _xml_element_from_tree(tree, target_class, version=1):
|
||||
if target_class._qname is None:
|
||||
instance = target_class()
|
||||
instance._qname = tree.tag
|
||||
instance._harvest_tree(tree, version)
|
||||
return instance
|
||||
# TODO handle the namespace-only case
|
||||
# Namespace only will be used with Google Spreadsheets rows and
|
||||
# Google Base item attributes.
|
||||
elif tree.tag == _get_qname(target_class, version):
|
||||
instance = target_class()
|
||||
instance._harvest_tree(tree, version)
|
||||
return instance
|
||||
return None
|
||||
|
||||
|
||||
class XmlAttribute(object):
|
||||
def __init__(self, qname, value):
|
||||
self._qname = qname
|
||||
self.value = value
|
||||
327
src/gam/atom/data.py
Normal file
327
src/gam/atom/data.py
Normal file
@@ -0,0 +1,327 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2009 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License 2.0;
|
||||
|
||||
|
||||
|
||||
# This module is used for version 2 of the Google Data APIs.
|
||||
|
||||
|
||||
# __author__ = 'j.s@google.com (Jeff Scudder)'
|
||||
|
||||
import atom.core
|
||||
|
||||
XML_TEMPLATE = '{http://www.w3.org/XML/1998/namespace}%s'
|
||||
ATOM_TEMPLATE = '{http://www.w3.org/2005/Atom}%s'
|
||||
APP_TEMPLATE_V1 = '{http://purl.org/atom/app#}%s'
|
||||
APP_TEMPLATE_V2 = '{http://www.w3.org/2007/app}%s'
|
||||
|
||||
|
||||
class Name(atom.core.XmlElement):
|
||||
"""The atom:name element."""
|
||||
_qname = ATOM_TEMPLATE % 'name'
|
||||
|
||||
|
||||
class Email(atom.core.XmlElement):
|
||||
"""The atom:email element."""
|
||||
_qname = ATOM_TEMPLATE % 'email'
|
||||
|
||||
|
||||
class Uri(atom.core.XmlElement):
|
||||
"""The atom:uri element."""
|
||||
_qname = ATOM_TEMPLATE % 'uri'
|
||||
|
||||
|
||||
class Person(atom.core.XmlElement):
|
||||
"""A foundation class which atom:author and atom:contributor extend.
|
||||
|
||||
A person contains information like name, email address, and web page URI for
|
||||
an author or contributor to an Atom feed.
|
||||
"""
|
||||
name = Name
|
||||
email = Email
|
||||
uri = Uri
|
||||
|
||||
|
||||
class Author(Person):
|
||||
"""The atom:author element.
|
||||
|
||||
An author is a required element in Feed unless each Entry contains an Author.
|
||||
"""
|
||||
_qname = ATOM_TEMPLATE % 'author'
|
||||
|
||||
|
||||
class Contributor(Person):
|
||||
"""The atom:contributor element."""
|
||||
_qname = ATOM_TEMPLATE % 'contributor'
|
||||
|
||||
|
||||
class Link(atom.core.XmlElement):
|
||||
"""The atom:link element."""
|
||||
_qname = ATOM_TEMPLATE % 'link'
|
||||
href = 'href'
|
||||
rel = 'rel'
|
||||
type = 'type'
|
||||
hreflang = 'hreflang'
|
||||
title = 'title'
|
||||
length = 'length'
|
||||
|
||||
|
||||
class Generator(atom.core.XmlElement):
|
||||
"""The atom:generator element."""
|
||||
_qname = ATOM_TEMPLATE % 'generator'
|
||||
uri = 'uri'
|
||||
version = 'version'
|
||||
|
||||
|
||||
class Text(atom.core.XmlElement):
|
||||
"""A foundation class from which atom:title, summary, etc. extend.
|
||||
|
||||
This class should never be instantiated.
|
||||
"""
|
||||
type = 'type'
|
||||
|
||||
|
||||
class Title(Text):
|
||||
"""The atom:title element."""
|
||||
_qname = ATOM_TEMPLATE % 'title'
|
||||
|
||||
|
||||
class Subtitle(Text):
|
||||
"""The atom:subtitle element."""
|
||||
_qname = ATOM_TEMPLATE % 'subtitle'
|
||||
|
||||
|
||||
class Rights(Text):
|
||||
"""The atom:rights element."""
|
||||
_qname = ATOM_TEMPLATE % 'rights'
|
||||
|
||||
|
||||
class Summary(Text):
|
||||
"""The atom:summary element."""
|
||||
_qname = ATOM_TEMPLATE % 'summary'
|
||||
|
||||
|
||||
class Content(Text):
|
||||
"""The atom:content element."""
|
||||
_qname = ATOM_TEMPLATE % 'content'
|
||||
src = 'src'
|
||||
|
||||
|
||||
class Category(atom.core.XmlElement):
|
||||
"""The atom:category element."""
|
||||
_qname = ATOM_TEMPLATE % 'category'
|
||||
term = 'term'
|
||||
scheme = 'scheme'
|
||||
label = 'label'
|
||||
|
||||
|
||||
class Id(atom.core.XmlElement):
|
||||
"""The atom:id element."""
|
||||
_qname = ATOM_TEMPLATE % 'id'
|
||||
|
||||
|
||||
class Icon(atom.core.XmlElement):
|
||||
"""The atom:icon element."""
|
||||
_qname = ATOM_TEMPLATE % 'icon'
|
||||
|
||||
|
||||
class Logo(atom.core.XmlElement):
|
||||
"""The atom:logo element."""
|
||||
_qname = ATOM_TEMPLATE % 'logo'
|
||||
|
||||
|
||||
class Draft(atom.core.XmlElement):
|
||||
"""The app:draft element which indicates if this entry should be public."""
|
||||
_qname = (APP_TEMPLATE_V1 % 'draft', APP_TEMPLATE_V2 % 'draft')
|
||||
|
||||
|
||||
class Control(atom.core.XmlElement):
|
||||
"""The app:control element indicating restrictions on publication.
|
||||
|
||||
The APP control element may contain a draft element indicating whether or
|
||||
not this entry should be publicly available.
|
||||
"""
|
||||
_qname = (APP_TEMPLATE_V1 % 'control', APP_TEMPLATE_V2 % 'control')
|
||||
draft = Draft
|
||||
|
||||
|
||||
class Date(atom.core.XmlElement):
|
||||
"""A parent class for atom:updated, published, etc."""
|
||||
|
||||
|
||||
class Updated(Date):
|
||||
"""The atom:updated element."""
|
||||
_qname = ATOM_TEMPLATE % 'updated'
|
||||
|
||||
|
||||
class Published(Date):
|
||||
"""The atom:published element."""
|
||||
_qname = ATOM_TEMPLATE % 'published'
|
||||
|
||||
|
||||
class LinkFinder(object):
|
||||
"""An "interface" providing methods to find link elements
|
||||
|
||||
Entry elements often contain multiple links which differ in the rel
|
||||
attribute or content type. Often, developers are interested in a specific
|
||||
type of link so this class provides methods to find specific classes of
|
||||
links.
|
||||
|
||||
This class is used as a mixin in Atom entries and feeds.
|
||||
"""
|
||||
|
||||
def find_url(self, rel):
|
||||
"""Returns the URL (as a string) in a link with the desired rel value."""
|
||||
for link in self.link:
|
||||
if link.rel == rel and link.href:
|
||||
return link.href
|
||||
return None
|
||||
|
||||
FindUrl = find_url
|
||||
|
||||
def get_link(self, rel):
|
||||
"""Returns a link object which has the desired rel value.
|
||||
|
||||
If you are interested in the URL instead of the link object,
|
||||
consider using find_url instead.
|
||||
"""
|
||||
for link in self.link:
|
||||
if link.rel == rel and link.href:
|
||||
return link
|
||||
return None
|
||||
|
||||
GetLink = get_link
|
||||
|
||||
def find_self_link(self):
|
||||
"""Find the first link with rel set to 'self'
|
||||
|
||||
Returns:
|
||||
A str containing the link's href or None if none of the links had rel
|
||||
equal to 'self'
|
||||
"""
|
||||
return self.find_url('self')
|
||||
|
||||
FindSelfLink = find_self_link
|
||||
|
||||
def get_self_link(self):
|
||||
return self.get_link('self')
|
||||
|
||||
GetSelfLink = get_self_link
|
||||
|
||||
def find_edit_link(self):
|
||||
return self.find_url('edit')
|
||||
|
||||
FindEditLink = find_edit_link
|
||||
|
||||
def get_edit_link(self):
|
||||
return self.get_link('edit')
|
||||
|
||||
GetEditLink = get_edit_link
|
||||
|
||||
def find_edit_media_link(self):
|
||||
link = self.find_url('edit-media')
|
||||
# Search for media-edit as well since Picasa API used media-edit instead.
|
||||
if link is None:
|
||||
return self.find_url('media-edit')
|
||||
return link
|
||||
|
||||
FindEditMediaLink = find_edit_media_link
|
||||
|
||||
def get_edit_media_link(self):
|
||||
link = self.get_link('edit-media')
|
||||
if link is None:
|
||||
return self.get_link('media-edit')
|
||||
return link
|
||||
|
||||
GetEditMediaLink = get_edit_media_link
|
||||
|
||||
def find_next_link(self):
|
||||
return self.find_url('next')
|
||||
|
||||
FindNextLink = find_next_link
|
||||
|
||||
def get_next_link(self):
|
||||
return self.get_link('next')
|
||||
|
||||
GetNextLink = get_next_link
|
||||
|
||||
def find_license_link(self):
|
||||
return self.find_url('license')
|
||||
|
||||
FindLicenseLink = find_license_link
|
||||
|
||||
def get_license_link(self):
|
||||
return self.get_link('license')
|
||||
|
||||
GetLicenseLink = get_license_link
|
||||
|
||||
def find_alternate_link(self):
|
||||
return self.find_url('alternate')
|
||||
|
||||
FindAlternateLink = find_alternate_link
|
||||
|
||||
def get_alternate_link(self):
|
||||
return self.get_link('alternate')
|
||||
|
||||
GetAlternateLink = get_alternate_link
|
||||
|
||||
|
||||
class FeedEntryParent(atom.core.XmlElement, LinkFinder):
|
||||
"""A super class for atom:feed and entry, contains shared attributes"""
|
||||
author = [Author]
|
||||
category = [Category]
|
||||
contributor = [Contributor]
|
||||
id = Id
|
||||
link = [Link]
|
||||
rights = Rights
|
||||
title = Title
|
||||
updated = Updated
|
||||
|
||||
def __init__(self, atom_id=None, text=None, *args, **kwargs):
|
||||
if atom_id is not None:
|
||||
self.id = atom_id
|
||||
atom.core.XmlElement.__init__(self, text=text, *args, **kwargs)
|
||||
|
||||
|
||||
class Source(FeedEntryParent):
|
||||
"""The atom:source element."""
|
||||
_qname = ATOM_TEMPLATE % 'source'
|
||||
generator = Generator
|
||||
icon = Icon
|
||||
logo = Logo
|
||||
subtitle = Subtitle
|
||||
|
||||
|
||||
class Entry(FeedEntryParent):
|
||||
"""The atom:entry element."""
|
||||
_qname = ATOM_TEMPLATE % 'entry'
|
||||
content = Content
|
||||
published = Published
|
||||
source = Source
|
||||
summary = Summary
|
||||
control = Control
|
||||
|
||||
|
||||
class Feed(Source):
|
||||
"""The atom:feed element which contains entries."""
|
||||
_qname = ATOM_TEMPLATE % 'feed'
|
||||
entry = [Entry]
|
||||
|
||||
|
||||
class ExtensionElement(atom.core.XmlElement):
|
||||
"""Provided for backwards compatibility to the v1 atom.ExtensionElement."""
|
||||
|
||||
def __init__(self, tag=None, namespace=None, attributes=None,
|
||||
children=None, text=None, *args, **kwargs):
|
||||
if namespace:
|
||||
self._qname = '{%s}%s' % (namespace, tag)
|
||||
else:
|
||||
self._qname = tag
|
||||
self.children = children or []
|
||||
self.attributes = attributes or {}
|
||||
self.text = text
|
||||
|
||||
_BecomeChildElement = atom.core.XmlElement._become_child
|
||||
354
src/gam/atom/http.py
Normal file
354
src/gam/atom/http.py
Normal file
@@ -0,0 +1,354 @@
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License 2.0;
|
||||
|
||||
|
||||
|
||||
"""HttpClients in this module use httplib to make HTTP requests.
|
||||
|
||||
This module make HTTP requests based on httplib, but there are environments
|
||||
in which an httplib based approach will not work (if running in Google App
|
||||
Engine for example). In those cases, higher level classes (like AtomService
|
||||
and GDataService) can swap out the HttpClient to transparently use a
|
||||
different mechanism for making HTTP requests.
|
||||
|
||||
HttpClient: Contains a request method which performs an HTTP call to the
|
||||
server.
|
||||
|
||||
ProxiedHttpClient: Contains a request method which connects to a proxy using
|
||||
settings stored in operating system environment variables then
|
||||
performs an HTTP call to the endpoint server.
|
||||
"""
|
||||
|
||||
# __author__ = 'api.jscudder (Jeff Scudder)'
|
||||
|
||||
import base64
|
||||
import http.client
|
||||
import os
|
||||
import socket
|
||||
|
||||
import atom.http_core
|
||||
import atom.http_interface
|
||||
import atom.url
|
||||
|
||||
ssl_imported = False
|
||||
ssl = None
|
||||
try:
|
||||
import ssl
|
||||
|
||||
ssl_imported = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class ProxyError(atom.http_interface.Error):
|
||||
pass
|
||||
|
||||
|
||||
class TestConfigurationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
DEFAULT_CONTENT_TYPE = 'application/atom+xml'
|
||||
|
||||
|
||||
class HttpClient(atom.http_interface.GenericHttpClient):
|
||||
# Added to allow old v1 HttpClient objects to use the new
|
||||
# http_code.HttpClient. Used in unit tests to inject a mock client.
|
||||
v2_http_client = None
|
||||
|
||||
def __init__(self, headers=None):
|
||||
self.debug = False
|
||||
self.headers = headers or {}
|
||||
|
||||
def request(self, operation, url, data=None, headers=None):
|
||||
"""Performs an HTTP call to the server, supports GET, POST, PUT, and
|
||||
DELETE.
|
||||
|
||||
Usage example, perform and HTTP GET on http://www.google.com/:
|
||||
import atom.http
|
||||
client = atom.http.HttpClient()
|
||||
http_response = client.request('GET', 'http://www.google.com/')
|
||||
|
||||
Args:
|
||||
operation: str The HTTP operation to be performed. This is usually one
|
||||
of 'GET', 'POST', 'PUT', or 'DELETE'
|
||||
data: filestream, list of parts, or other object which can be converted
|
||||
to a string. Should be set to None when performing a GET or DELETE.
|
||||
If data is a file-like object which can be read, this method will
|
||||
read a chunk of 100K bytes at a time and send them.
|
||||
If the data is a list of parts to be sent, each part will be
|
||||
evaluated and sent.
|
||||
url: The full URL to which the request should be sent. Can be a string
|
||||
or atom.url.Url.
|
||||
headers: dict of strings. HTTP headers which should be sent
|
||||
in the request.
|
||||
"""
|
||||
all_headers = self.headers.copy()
|
||||
if headers:
|
||||
all_headers.update(headers)
|
||||
|
||||
# If the list of headers does not include a Content-Length, attempt to
|
||||
# calculate it based on the data object.
|
||||
if data and 'Content-Length' not in all_headers:
|
||||
if isinstance(data, (str,)):
|
||||
all_headers['Content-Length'] = str(len(data))
|
||||
else:
|
||||
raise atom.http_interface.ContentLengthRequired('Unable to calculate '
|
||||
'the length of the data parameter. Specify a value for '
|
||||
'Content-Length')
|
||||
|
||||
# Set the content type to the default value if none was set.
|
||||
if 'Content-Type' not in all_headers:
|
||||
all_headers['Content-Type'] = DEFAULT_CONTENT_TYPE
|
||||
|
||||
if self.v2_http_client is not None:
|
||||
http_request = atom.http_core.HttpRequest(method=operation)
|
||||
atom.http_core.Uri.parse_uri(str(url)).modify_request(http_request)
|
||||
http_request.headers = all_headers
|
||||
if data:
|
||||
http_request._body_parts.append(data)
|
||||
return self.v2_http_client.request(http_request=http_request)
|
||||
|
||||
if not isinstance(url, atom.url.Url):
|
||||
if isinstance(url, str):
|
||||
url = atom.url.parse_url(url)
|
||||
else:
|
||||
raise atom.http_interface.UnparsableUrlObject('Unable to parse url parameter because it was not a string or atom.url.Url')
|
||||
|
||||
connection = self._prepare_connection(url, all_headers)
|
||||
|
||||
if self.debug:
|
||||
connection.debuglevel = 1
|
||||
|
||||
connection.putrequest(operation, self._get_access_url(url), skip_host=True)
|
||||
|
||||
if url.port is not None:
|
||||
connection.putheader('Host', '%s:%s' % (url.host, url.port))
|
||||
else:
|
||||
connection.putheader('Host', url.host)
|
||||
|
||||
# Overcome a bug in Python 2.4 and 2.5
|
||||
# httplib.HTTPConnection.putrequest adding
|
||||
# HTTP request header 'Host: www.google.com:443' instead of
|
||||
# 'Host: www.google.com', and thus resulting the error message
|
||||
# 'Token invalid - AuthSub token has wrong scope' in the HTTP response.
|
||||
if (url.protocol == 'https' and int(url.port or 443) == 443 and
|
||||
hasattr(connection, '_buffer') and
|
||||
isinstance(connection._buffer, list)):
|
||||
header_line = 'Host: %s:443' % url.host
|
||||
replacement_header_line = 'Host: %s' % url.host
|
||||
try:
|
||||
connection._buffer[connection._buffer.index(header_line)] = (
|
||||
replacement_header_line)
|
||||
except ValueError: # header_line missing from connection._buffer
|
||||
pass
|
||||
|
||||
# Send the HTTP headers.
|
||||
for header_name in all_headers:
|
||||
connection.putheader(header_name, all_headers[header_name])
|
||||
connection.endheaders()
|
||||
|
||||
# If there is data, send it in the request.
|
||||
if data:
|
||||
if isinstance(data, list):
|
||||
for data_part in data:
|
||||
_send_data_part(data_part, connection)
|
||||
else:
|
||||
_send_data_part(data, connection)
|
||||
|
||||
# Return the HTTP Response from the server.
|
||||
return connection.getresponse()
|
||||
|
||||
def _prepare_connection(self, url, headers):
|
||||
if not isinstance(url, atom.url.Url):
|
||||
if isinstance(url, (str,)):
|
||||
url = atom.url.parse_url(url)
|
||||
else:
|
||||
raise atom.http_interface.UnparsableUrlObject('Unable to parse url '
|
||||
'parameter because it was not a string or atom.url.Url')
|
||||
if url.protocol == 'https':
|
||||
if not url.port:
|
||||
return http.client.HTTPSConnection(url.host)
|
||||
return http.client.HTTPSConnection(url.host, int(url.port))
|
||||
else:
|
||||
if not url.port:
|
||||
return http.client.HTTPConnection(url.host)
|
||||
return http.client.HTTPConnection(url.host, int(url.port))
|
||||
|
||||
def _get_access_url(self, url):
|
||||
return url.to_string()
|
||||
|
||||
|
||||
class ProxiedHttpClient(HttpClient):
|
||||
"""Performs an HTTP request through a proxy.
|
||||
|
||||
The proxy settings are obtained from enviroment variables. The URL of the
|
||||
proxy server is assumed to be stored in the environment variables
|
||||
'https_proxy' and 'http_proxy' respectively. If the proxy server requires
|
||||
a Basic Auth authorization header, the username and password are expected to
|
||||
be in the 'proxy-username' or 'proxy_username' variable and the
|
||||
'proxy-password' or 'proxy_password' variable, or in 'http_proxy' or
|
||||
'https_proxy' as "protocol://[username:password@]host:port".
|
||||
|
||||
After connecting to the proxy server, the request is completed as in
|
||||
HttpClient.request.
|
||||
"""
|
||||
|
||||
def _prepare_connection(self, url, headers):
|
||||
proxy_settings = os.environ.get('%s_proxy' % url.protocol)
|
||||
if not proxy_settings:
|
||||
# The request was HTTP or HTTPS, but there was no appropriate proxy set.
|
||||
return HttpClient._prepare_connection(self, url, headers)
|
||||
else:
|
||||
proxy_auth = _get_proxy_auth(proxy_settings)
|
||||
proxy_netloc = _get_proxy_net_location(proxy_settings)
|
||||
if url.protocol == 'https':
|
||||
# Set any proxy auth headers
|
||||
if proxy_auth:
|
||||
proxy_auth = 'Proxy-Authorization: %s' % proxy_auth
|
||||
|
||||
# Construct the proxy connect command.
|
||||
port = url.port
|
||||
if not port:
|
||||
port = '443'
|
||||
proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (url.host, port)
|
||||
|
||||
# Set the user agent to send to the proxy
|
||||
if headers and 'User-Agent' in headers:
|
||||
user_agent = 'User-Agent: %s\r\n' % (headers['User-Agent'])
|
||||
else:
|
||||
user_agent = 'User-Agent: python\r\n'
|
||||
|
||||
proxy_pieces = '%s%s%s\r\n' % (proxy_connect, proxy_auth, user_agent)
|
||||
|
||||
# Find the proxy host and port.
|
||||
proxy_url = atom.url.parse_url(proxy_netloc)
|
||||
if not proxy_url.port:
|
||||
proxy_url.port = '80'
|
||||
|
||||
# Connect to the proxy server, very simple recv and error checking
|
||||
p_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
p_sock.connect((proxy_url.host, int(proxy_url.port)))
|
||||
#p_sock.sendall(proxy_pieces)
|
||||
p_sock.sendall(proxy_pieces.encode('utf-8'))
|
||||
response = ''
|
||||
|
||||
# Wait for the full response.
|
||||
while response.find("\r\n\r\n") == -1:
|
||||
#response += p_sock.recv(8192)
|
||||
response += p_sock.recv(8192).decode('utf-8')
|
||||
|
||||
p_status = response.split()[1]
|
||||
if p_status != str(200):
|
||||
raise ProxyError('Error status=%s' % str(p_status))
|
||||
|
||||
# Trivial setup for ssl socket.
|
||||
sslobj = None
|
||||
if ssl_imported:
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
|
||||
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||
sslobj = context.wrap_socket(p_sock, server_hostname=url.host)
|
||||
else:
|
||||
sock_ssl = socket.ssl(p_sock, None, None)
|
||||
sslobj = http.client.FakeSocket(p_sock, sock_ssl)
|
||||
|
||||
# Initalize httplib and replace with the proxy socket.
|
||||
connection = http.client.HTTPConnection(proxy_url.host)
|
||||
connection.sock = sslobj
|
||||
return connection
|
||||
else:
|
||||
# If protocol was not https.
|
||||
# Find the proxy host and port.
|
||||
proxy_url = atom.url.parse_url(proxy_netloc)
|
||||
if not proxy_url.port:
|
||||
proxy_url.port = '80'
|
||||
|
||||
if proxy_auth:
|
||||
headers['Proxy-Authorization'] = proxy_auth.strip()
|
||||
|
||||
return http.client.HTTPConnection(proxy_url.host, int(proxy_url.port))
|
||||
|
||||
def _get_access_url(self, url):
|
||||
return url.to_string()
|
||||
|
||||
|
||||
def _get_proxy_auth(proxy_settings):
|
||||
"""Returns proxy authentication string for header.
|
||||
|
||||
Will check environment variables for proxy authentication info, starting with
|
||||
proxy(_/-)username and proxy(_/-)password before checking the given
|
||||
proxy_settings for a [protocol://]username:password@host[:port] string.
|
||||
|
||||
Args:
|
||||
proxy_settings: String from http_proxy or https_proxy environment variable.
|
||||
|
||||
Returns:
|
||||
Authentication string for proxy, or empty string if no proxy username was
|
||||
found.
|
||||
"""
|
||||
proxy_username = None
|
||||
proxy_password = None
|
||||
|
||||
proxy_username = os.environ.get('proxy-username')
|
||||
if not proxy_username:
|
||||
proxy_username = os.environ.get('proxy_username')
|
||||
proxy_password = os.environ.get('proxy-password')
|
||||
if not proxy_password:
|
||||
proxy_password = os.environ.get('proxy_password')
|
||||
|
||||
if not proxy_username:
|
||||
if '@' in proxy_settings:
|
||||
protocol_and_proxy_auth = proxy_settings.split('@')[0].split(':')
|
||||
if len(protocol_and_proxy_auth) == 3:
|
||||
# 3 elements means we have [<protocol>, //<user>, <password>]
|
||||
proxy_username = protocol_and_proxy_auth[1].lstrip('/')
|
||||
proxy_password = protocol_and_proxy_auth[2]
|
||||
elif len(protocol_and_proxy_auth) == 2:
|
||||
# 2 elements means we have [<user>, <password>]
|
||||
proxy_username = protocol_and_proxy_auth[0]
|
||||
proxy_password = protocol_and_proxy_auth[1]
|
||||
if proxy_username:
|
||||
user_auth = base64.b64encode(('%s:%s' % (proxy_username, proxy_password)).encode('utf-8'))
|
||||
return 'Basic %s\r\n' % (user_auth.strip().decode('utf-8'))
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def _get_proxy_net_location(proxy_settings):
|
||||
"""Returns proxy host and port.
|
||||
|
||||
Args:
|
||||
proxy_settings: String from http_proxy or https_proxy environment variable.
|
||||
Must be in the form of protocol://[username:password@]host:port
|
||||
|
||||
Returns:
|
||||
String in the form of protocol://host:port
|
||||
"""
|
||||
if '@' in proxy_settings:
|
||||
protocol = proxy_settings.split(':')[0]
|
||||
netloc = proxy_settings.split('@')[1]
|
||||
return '%s://%s' % (protocol, netloc)
|
||||
else:
|
||||
return proxy_settings
|
||||
|
||||
|
||||
def _send_data_part(data, connection):
|
||||
if isinstance(data, (str,)):
|
||||
connection.send(data)
|
||||
return
|
||||
# Check to see if data is a file-like object that has a read method.
|
||||
elif hasattr(data, 'read'):
|
||||
# Read the file and send it a chunk at a time.
|
||||
while 1:
|
||||
binarydata = data.read(100000)
|
||||
if binarydata == b'': break
|
||||
connection.send(binarydata)
|
||||
return
|
||||
else:
|
||||
# The data object was not a file.
|
||||
# Try to convert to a string and send the data.
|
||||
#connection.send(str(data))
|
||||
connection.send(str(data).encode('utf-8'))
|
||||
return
|
||||
599
src/gam/atom/http_core.py
Normal file
599
src/gam/atom/http_core.py
Normal file
@@ -0,0 +1,599 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2009 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License 2.0;
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
# This module is used for version 2 of the Google Data APIs.
|
||||
# TODO: add proxy handling.
|
||||
|
||||
|
||||
# __author__ = 'j.s@google.com (Jeff Scudder)'
|
||||
|
||||
import http.client
|
||||
import io
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
ssl = None
|
||||
try:
|
||||
import ssl
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnknownSize(Error):
|
||||
pass
|
||||
|
||||
|
||||
class ProxyError(Error):
|
||||
pass
|
||||
|
||||
|
||||
MIME_BOUNDARY = 'END_OF_PART'
|
||||
|
||||
|
||||
def get_headers(http_response):
|
||||
"""Retrieves all HTTP headers from an HTTP response from the server.
|
||||
|
||||
This method is provided for backwards compatibility for Python2.2 and 2.3.
|
||||
The httplib.HTTPResponse object in 2.2 and 2.3 does not have a getheaders
|
||||
method so this function will use getheaders if available, but if not it
|
||||
will retrieve a few using getheader.
|
||||
"""
|
||||
if hasattr(http_response, 'getheaders'):
|
||||
return http_response.getheaders()
|
||||
else:
|
||||
headers = []
|
||||
for header in (
|
||||
'location', 'content-type', 'content-length', 'age', 'allow',
|
||||
'cache-control', 'content-location', 'content-encoding', 'date',
|
||||
'etag', 'expires', 'last-modified', 'pragma', 'server',
|
||||
'set-cookie', 'transfer-encoding', 'vary', 'via', 'warning',
|
||||
'www-authenticate', 'gdata-version'):
|
||||
value = http_response.getheader(header, None)
|
||||
if value is not None:
|
||||
headers.append((header, value))
|
||||
return headers
|
||||
|
||||
|
||||
class HttpRequest(object):
|
||||
"""Contains all of the parameters for an HTTP 1.1 request.
|
||||
|
||||
The HTTP headers are represented by a dictionary, and it is the
|
||||
responsibility of the user to ensure that duplicate field names are combined
|
||||
into one header value according to the rules in section 4.2 of RFC 2616.
|
||||
"""
|
||||
method = None
|
||||
uri = None
|
||||
|
||||
def __init__(self, uri=None, method=None, headers=None):
|
||||
"""Construct an HTTP request.
|
||||
|
||||
Args:
|
||||
uri: The full path or partial path as a Uri object or a string.
|
||||
method: The HTTP method for the request, examples include 'GET', 'POST',
|
||||
etc.
|
||||
headers: dict of strings The HTTP headers to include in the request.
|
||||
"""
|
||||
self.headers = headers or {}
|
||||
self._body_parts = []
|
||||
if method is not None:
|
||||
self.method = method
|
||||
if isinstance(uri, str):
|
||||
uri = Uri.parse_uri(uri)
|
||||
self.uri = uri or Uri()
|
||||
|
||||
def add_body_part(self, data, mime_type, size=None):
|
||||
"""Adds data to the HTTP request body.
|
||||
|
||||
If more than one part is added, this is assumed to be a mime-multipart
|
||||
request. This method is designed to create MIME 1.0 requests as specified
|
||||
in RFC 1341.
|
||||
|
||||
Args:
|
||||
data: str or a file-like object containing a part of the request body.
|
||||
mime_type: str The MIME type describing the data
|
||||
size: int Required if the data is a file like object. If the data is a
|
||||
string, the size is calculated so this parameter is ignored.
|
||||
"""
|
||||
if hasattr(data, '__len__'):
|
||||
size = len(data)
|
||||
if size is None:
|
||||
# TODO: support chunked transfer if some of the body is of unknown size.
|
||||
raise UnknownSize('Each part of the body must have a known size.')
|
||||
if 'Content-Length' in self.headers:
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
else:
|
||||
content_length = 0
|
||||
# If this is the first part added to the body, then this is not a multipart
|
||||
# request.
|
||||
if len(self._body_parts) == 0:
|
||||
self.headers['Content-Type'] = mime_type
|
||||
content_length = size
|
||||
self._body_parts.append(data)
|
||||
elif len(self._body_parts) == 1:
|
||||
# This is the first member in a mime-multipart request, so change the
|
||||
# _body_parts list to indicate a multipart payload.
|
||||
self._body_parts.insert(0, 'Media multipart posting')
|
||||
boundary_string = '\r\n--%s\r\n' % (MIME_BOUNDARY,)
|
||||
content_length += len(boundary_string) + size
|
||||
self._body_parts.insert(1, boundary_string)
|
||||
content_length += len('Media multipart posting')
|
||||
# Put the content type of the first part of the body into the multipart
|
||||
# payload.
|
||||
original_type_string = 'Content-Type: %s\r\n\r\n' % (
|
||||
self.headers['Content-Type'],)
|
||||
self._body_parts.insert(2, original_type_string)
|
||||
content_length += len(original_type_string)
|
||||
boundary_string = '\r\n--%s\r\n' % (MIME_BOUNDARY,)
|
||||
self._body_parts.append(boundary_string)
|
||||
content_length += len(boundary_string)
|
||||
# Change the headers to indicate this is now a mime multipart request.
|
||||
self.headers['Content-Type'] = 'multipart/related; boundary="%s"' % (
|
||||
MIME_BOUNDARY,)
|
||||
self.headers['MIME-version'] = '1.0'
|
||||
# Include the mime type of this part.
|
||||
type_string = 'Content-Type: %s\r\n\r\n' % (mime_type)
|
||||
self._body_parts.append(type_string)
|
||||
content_length += len(type_string)
|
||||
self._body_parts.append(data)
|
||||
ending_boundary_string = '\r\n--%s--' % (MIME_BOUNDARY,)
|
||||
self._body_parts.append(ending_boundary_string)
|
||||
content_length += len(ending_boundary_string)
|
||||
else:
|
||||
# This is a mime multipart request.
|
||||
boundary_string = '\r\n--%s\r\n' % (MIME_BOUNDARY,)
|
||||
self._body_parts.insert(-1, boundary_string)
|
||||
content_length += len(boundary_string) + size
|
||||
# Include the mime type of this part.
|
||||
type_string = 'Content-Type: %s\r\n\r\n' % (mime_type)
|
||||
self._body_parts.insert(-1, type_string)
|
||||
content_length += len(type_string)
|
||||
self._body_parts.insert(-1, data)
|
||||
self.headers['Content-Length'] = str(content_length)
|
||||
|
||||
# I could add an "append_to_body_part" method as well.
|
||||
|
||||
AddBodyPart = add_body_part
|
||||
|
||||
def add_form_inputs(self, form_data,
|
||||
mime_type='application/x-www-form-urlencoded'):
|
||||
"""Form-encodes and adds data to the request body.
|
||||
|
||||
Args:
|
||||
form_data: dict or sequnce or two member tuples which contains the
|
||||
form keys and values.
|
||||
mime_type: str The MIME type of the form data being sent. Defaults
|
||||
to 'application/x-www-form-urlencoded'.
|
||||
"""
|
||||
body = urllib.parse.urlencode(form_data)
|
||||
self.add_body_part(body, bytes(mime_type, 'ascii'))
|
||||
|
||||
AddFormInputs = add_form_inputs
|
||||
|
||||
def _copy(self):
|
||||
"""Creates a deep copy of this request."""
|
||||
copied_uri = Uri(self.uri.scheme, self.uri.host, self.uri.port,
|
||||
self.uri.path, self.uri.query.copy())
|
||||
new_request = HttpRequest(uri=copied_uri, method=self.method,
|
||||
headers=self.headers.copy())
|
||||
new_request._body_parts = self._body_parts[:]
|
||||
return new_request
|
||||
|
||||
def _dump(self):
|
||||
"""Converts to a printable string for debugging purposes.
|
||||
|
||||
In order to preserve the request, it does not read from file-like objects
|
||||
in the body.
|
||||
"""
|
||||
output = 'HTTP Request\n method: %s\n url: %s\n headers:\n' % (
|
||||
self.method, str(self.uri))
|
||||
for header, value in self.headers.items():
|
||||
output += ' %s: %s\n' % (header, value)
|
||||
output += ' body sections:\n'
|
||||
i = 0
|
||||
for part in self._body_parts:
|
||||
if isinstance(part, str):
|
||||
output += ' %s: %s\n' % (i, part)
|
||||
else:
|
||||
output += ' %s: <file like object>\n' % i
|
||||
i += 1
|
||||
return output
|
||||
|
||||
|
||||
def _apply_defaults(http_request):
|
||||
if http_request.uri.scheme is None:
|
||||
if http_request.uri.port == 443:
|
||||
http_request.uri.scheme = 'https'
|
||||
else:
|
||||
http_request.uri.scheme = 'http'
|
||||
|
||||
|
||||
class Uri(object):
|
||||
"""A URI as used in HTTP 1.1"""
|
||||
scheme = None
|
||||
host = None
|
||||
port = None
|
||||
path = None
|
||||
|
||||
def __init__(self, scheme=None, host=None, port=None, path=None, query=None):
|
||||
"""Constructor for a URI.
|
||||
|
||||
Args:
|
||||
scheme: str This is usually 'http' or 'https'.
|
||||
host: str The host name or IP address of the desired server.
|
||||
post: int The server's port number.
|
||||
path: str The path of the resource following the host. This begins with
|
||||
a /, example: '/calendar/feeds/default/allcalendars/full'
|
||||
query: dict of strings The URL query parameters. The keys and values are
|
||||
both escaped so this dict should contain the unescaped values.
|
||||
For example {'my key': 'val', 'second': '!!!'} will become
|
||||
'?my+key=val&second=%21%21%21' which is appended to the path.
|
||||
"""
|
||||
self.query = query or {}
|
||||
if scheme is not None:
|
||||
self.scheme = scheme
|
||||
if host is not None:
|
||||
self.host = host
|
||||
if port is not None:
|
||||
self.port = port
|
||||
if path:
|
||||
self.path = path
|
||||
|
||||
def _get_query_string(self):
|
||||
param_pairs = []
|
||||
for key, value in self.query.items():
|
||||
quoted_key = urllib.parse.quote_plus(str(key))
|
||||
if value is None:
|
||||
param_pairs.append(quoted_key)
|
||||
else:
|
||||
quoted_value = urllib.parse.quote_plus(str(value))
|
||||
param_pairs.append('%s=%s' % (quoted_key, quoted_value))
|
||||
return '&'.join(param_pairs)
|
||||
|
||||
def _get_relative_path(self):
|
||||
"""Returns the path with the query parameters escaped and appended."""
|
||||
param_string = self._get_query_string()
|
||||
if self.path is None:
|
||||
path = '/'
|
||||
else:
|
||||
path = self.path
|
||||
if param_string:
|
||||
return '?'.join([path, param_string])
|
||||
else:
|
||||
return path
|
||||
|
||||
def _to_string(self):
|
||||
if self.scheme is None and self.port == 443:
|
||||
scheme = 'https'
|
||||
elif self.scheme is None:
|
||||
scheme = 'http'
|
||||
else:
|
||||
scheme = self.scheme
|
||||
if self.path is None:
|
||||
path = '/'
|
||||
else:
|
||||
path = self.path
|
||||
if self.port is None:
|
||||
return '%s://%s%s' % (scheme, self.host, self._get_relative_path())
|
||||
else:
|
||||
return '%s://%s:%s%s' % (scheme, self.host, str(self.port),
|
||||
self._get_relative_path())
|
||||
|
||||
def __str__(self):
|
||||
return self._to_string()
|
||||
|
||||
def modify_request(self, http_request=None):
|
||||
"""Sets HTTP request components based on the URI."""
|
||||
if http_request is None:
|
||||
http_request = HttpRequest()
|
||||
if http_request.uri is None:
|
||||
http_request.uri = Uri()
|
||||
# Determine the correct scheme.
|
||||
if self.scheme:
|
||||
http_request.uri.scheme = self.scheme
|
||||
if self.port:
|
||||
http_request.uri.port = self.port
|
||||
if self.host:
|
||||
http_request.uri.host = self.host
|
||||
# Set the relative uri path
|
||||
if self.path:
|
||||
http_request.uri.path = self.path
|
||||
if self.query:
|
||||
http_request.uri.query = self.query.copy()
|
||||
return http_request
|
||||
|
||||
ModifyRequest = modify_request
|
||||
|
||||
def parse_uri(uri_string):
|
||||
"""Creates a Uri object which corresponds to the URI string.
|
||||
|
||||
This method can accept partial URIs, but it will leave missing
|
||||
members of the Uri unset.
|
||||
"""
|
||||
parts = urllib.parse.urlparse(uri_string)
|
||||
uri = Uri()
|
||||
if parts[0]:
|
||||
uri.scheme = parts[0]
|
||||
if parts[1]:
|
||||
host_parts = parts[1].split(':')
|
||||
if host_parts[0]:
|
||||
uri.host = host_parts[0]
|
||||
if len(host_parts) > 1:
|
||||
uri.port = int(host_parts[1])
|
||||
if parts[2]:
|
||||
uri.path = parts[2]
|
||||
if parts[4]:
|
||||
param_pairs = parts[4].split('&')
|
||||
for pair in param_pairs:
|
||||
pair_parts = pair.split('=')
|
||||
if len(pair_parts) > 1:
|
||||
uri.query[urllib.parse.unquote_plus(pair_parts[0])] = (
|
||||
urllib.parse.unquote_plus(pair_parts[1]))
|
||||
elif len(pair_parts) == 1:
|
||||
uri.query[urllib.parse.unquote_plus(pair_parts[0])] = None
|
||||
return uri
|
||||
|
||||
parse_uri = staticmethod(parse_uri)
|
||||
|
||||
ParseUri = parse_uri
|
||||
|
||||
|
||||
parse_uri = Uri.parse_uri
|
||||
|
||||
ParseUri = Uri.parse_uri
|
||||
|
||||
|
||||
class HttpResponse(object):
|
||||
status = None
|
||||
reason = None
|
||||
_body = None
|
||||
|
||||
def __init__(self, status=None, reason=None, headers=None, body=None):
|
||||
self._headers = headers or {}
|
||||
if status is not None:
|
||||
self.status = status
|
||||
if reason is not None:
|
||||
self.reason = reason
|
||||
if body is not None:
|
||||
if hasattr(body, 'read'):
|
||||
self._body = body
|
||||
else:
|
||||
self._body = io.StringIO(body)
|
||||
|
||||
def getheader(self, name, default=None):
|
||||
if name in self._headers:
|
||||
return self._headers[name]
|
||||
else:
|
||||
return default
|
||||
|
||||
def getheaders(self):
|
||||
return self._headers
|
||||
|
||||
def read(self, amt=None):
|
||||
if self._body is None:
|
||||
return None
|
||||
if not amt:
|
||||
return self._body.read()
|
||||
else:
|
||||
return self._body.read(amt)
|
||||
|
||||
|
||||
def _dump_response(http_response):
|
||||
"""Converts to a string for printing debug messages.
|
||||
|
||||
Does not read the body since that may consume the content.
|
||||
"""
|
||||
output = 'HttpResponse\n status: %s\n reason: %s\n headers:' % (
|
||||
http_response.status, http_response.reason)
|
||||
headers = get_headers(http_response)
|
||||
if isinstance(headers, dict):
|
||||
for header, value in headers.items():
|
||||
output += ' %s: %s\n' % (header, value)
|
||||
else:
|
||||
for pair in headers:
|
||||
output += ' %s: %s\n' % (pair[0], pair[1])
|
||||
return output
|
||||
|
||||
|
||||
class HttpClient(object):
|
||||
"""Performs HTTP requests using httplib."""
|
||||
debug = None
|
||||
|
||||
def request(self, http_request):
|
||||
return self._http_request(http_request.method, http_request.uri,
|
||||
http_request.headers, http_request._body_parts)
|
||||
|
||||
Request = request
|
||||
|
||||
def _get_connection(self, uri, headers=None):
|
||||
"""Opens a socket connection to the server to set up an HTTP request.
|
||||
|
||||
Args:
|
||||
uri: The full URL for the request as a Uri object.
|
||||
headers: A dict of string pairs containing the HTTP headers for the
|
||||
request.
|
||||
"""
|
||||
connection = None
|
||||
if uri.scheme == 'https':
|
||||
if not uri.port:
|
||||
connection = http.client.HTTPSConnection(uri.host)
|
||||
else:
|
||||
connection = http.client.HTTPSConnection(uri.host, int(uri.port))
|
||||
else:
|
||||
if not uri.port:
|
||||
connection = http.client.HTTPConnection(uri.host)
|
||||
else:
|
||||
connection = http.client.HTTPConnection(uri.host, int(uri.port))
|
||||
return connection
|
||||
|
||||
def _http_request(self, method, uri, headers=None, body_parts=None):
|
||||
"""Makes an HTTP request using httplib.
|
||||
|
||||
Args:
|
||||
method: str example: 'GET', 'POST', 'PUT', 'DELETE', etc.
|
||||
uri: str or atom.http_core.Uri
|
||||
headers: dict of strings mapping to strings which will be sent as HTTP
|
||||
headers in the request.
|
||||
body_parts: list of strings, objects with a read method, or objects
|
||||
which can be converted to strings using str. Each of these
|
||||
will be sent in order as the body of the HTTP request.
|
||||
"""
|
||||
if isinstance(uri, str):
|
||||
uri = Uri.parse_uri(uri)
|
||||
|
||||
connection = self._get_connection(uri, headers=headers)
|
||||
|
||||
if self.debug:
|
||||
connection.debuglevel = 1
|
||||
|
||||
if connection.host != uri.host:
|
||||
connection.putrequest(method, str(uri))
|
||||
else:
|
||||
connection.putrequest(method, uri._get_relative_path())
|
||||
|
||||
# Overcome a bug in Python 2.4 and 2.5
|
||||
# httplib.HTTPConnection.putrequest adding
|
||||
# HTTP request header 'Host: www.google.com:443' instead of
|
||||
# 'Host: www.google.com', and thus resulting the error message
|
||||
# 'Token invalid - AuthSub token has wrong scope' in the HTTP response.
|
||||
if (uri.scheme == 'https' and int(uri.port or 443) == 443 and
|
||||
hasattr(connection, '_buffer') and
|
||||
isinstance(connection._buffer, list)):
|
||||
header_line = 'Host: %s:443' % uri.host
|
||||
replacement_header_line = 'Host: %s' % uri.host
|
||||
try:
|
||||
connection._buffer[connection._buffer.index(header_line)] = (
|
||||
replacement_header_line)
|
||||
except ValueError: # header_line missing from connection._buffer
|
||||
pass
|
||||
|
||||
# Send the HTTP headers.
|
||||
for header_name, value in headers.items():
|
||||
connection.putheader(header_name, value)
|
||||
connection.endheaders()
|
||||
|
||||
# If there is data, send it in the request.
|
||||
if body_parts and [x for x in body_parts if x != '']:
|
||||
for part in body_parts:
|
||||
_send_data_part(part, connection)
|
||||
|
||||
# Return the HTTP Response from the server.
|
||||
return connection.getresponse()
|
||||
|
||||
|
||||
def _send_data_part(data, connection):
|
||||
if isinstance(data, str):
|
||||
# I might want to just allow str, not unicode.
|
||||
connection.send(data.encode())
|
||||
# Check to see if data is a file-like object that has a read method.
|
||||
elif hasattr(data, 'read'):
|
||||
# Read the file and send it a chunk at a time.
|
||||
while 1:
|
||||
binarydata = data.read(100000)
|
||||
if binarydata == '': break
|
||||
connection.send(binarydata)
|
||||
else:
|
||||
# The data object was not a file.
|
||||
# Try to convert to a string and send the data.
|
||||
connection.send(data)
|
||||
|
||||
|
||||
class ProxiedHttpClient(HttpClient):
|
||||
def _get_connection(self, uri, headers=None):
|
||||
# Check to see if there are proxy settings required for this request.
|
||||
proxy = None
|
||||
if uri.scheme == 'https':
|
||||
proxy = os.environ.get('https_proxy')
|
||||
elif uri.scheme == 'http':
|
||||
proxy = os.environ.get('http_proxy')
|
||||
if not proxy:
|
||||
return HttpClient._get_connection(self, uri, headers=headers)
|
||||
# Now we have the URL of the appropriate proxy server.
|
||||
# Get a username and password for the proxy if required.
|
||||
proxy_auth = _get_proxy_auth()
|
||||
if uri.scheme == 'https':
|
||||
import socket
|
||||
if proxy_auth:
|
||||
proxy_auth = 'Proxy-Authorization: %s' % proxy_auth
|
||||
# Construct the proxy connect command.
|
||||
port = uri.port
|
||||
if not port:
|
||||
port = 443
|
||||
proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (uri.host, port)
|
||||
# Set the user agent to send to the proxy
|
||||
user_agent = ''
|
||||
if headers and 'User-Agent' in headers:
|
||||
user_agent = 'User-Agent: %s\r\n' % (headers['User-Agent'])
|
||||
proxy_pieces = '%s%s%s\r\n' % (proxy_connect, proxy_auth, user_agent)
|
||||
# Find the proxy host and port.
|
||||
proxy_uri = Uri.parse_uri(proxy)
|
||||
if not proxy_uri.port:
|
||||
proxy_uri.port = '80'
|
||||
# Connect to the proxy server, very simple recv and error checking
|
||||
p_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
p_sock.connect((proxy_uri.host, int(proxy_uri.port)))
|
||||
#p_sock.sendall(proxy_pieces)
|
||||
p_sock.sendall(proxy_pieces.encode('utf-8'))
|
||||
response = ''
|
||||
# Wait for the full response.
|
||||
while response.find("\r\n\r\n") == -1:
|
||||
#response += p_sock.recv(8192)
|
||||
response += p_sock.recv(8192).decode('utf-8')
|
||||
p_status = response.split()[1]
|
||||
if p_status != str(200):
|
||||
raise ProxyError('Error status=%s' % str(p_status))
|
||||
# Trivial setup for ssl socket.
|
||||
sslobj = None
|
||||
if ssl is not None:
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
|
||||
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||
sslobj = context.wrap_socket(p_sock, server_hostname=uri.host)
|
||||
else:
|
||||
sock_ssl = socket.ssl(p_sock, None, None)
|
||||
sslobj = http.client.FakeSocket(p_sock, sock_ssl)
|
||||
# Initalize httplib and replace with the proxy socket.
|
||||
connection = http.client.HTTPConnection(proxy_uri.host)
|
||||
connection.sock = sslobj
|
||||
return connection
|
||||
elif uri.scheme == 'http':
|
||||
proxy_uri = Uri.parse_uri(proxy)
|
||||
if not proxy_uri.port:
|
||||
proxy_uri.port = '80'
|
||||
if proxy_auth:
|
||||
headers['Proxy-Authorization'] = proxy_auth.strip()
|
||||
return http.client.HTTPConnection(proxy_uri.host, int(proxy_uri.port))
|
||||
return None
|
||||
|
||||
|
||||
def _get_proxy_auth():
|
||||
import base64
|
||||
proxy_username = os.environ.get('proxy-username')
|
||||
if not proxy_username:
|
||||
proxy_username = os.environ.get('proxy_username')
|
||||
proxy_password = os.environ.get('proxy-password')
|
||||
if not proxy_password:
|
||||
proxy_password = os.environ.get('proxy_password')
|
||||
if proxy_username:
|
||||
user_auth = base64.b64encode(('%s:%s' % (proxy_username, proxy_password)).encode('utf-8'))
|
||||
return 'Basic %s\r\n' % (user_auth.strip().decode('utf-8'))
|
||||
else:
|
||||
return ''
|
||||
144
src/gam/atom/http_interface.py
Normal file
144
src/gam/atom/http_interface.py
Normal file
@@ -0,0 +1,144 @@
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License 2.0;
|
||||
|
||||
|
||||
"""This module provides a common interface for all HTTP requests.
|
||||
|
||||
HttpResponse: Represents the server's response to an HTTP request. Provides
|
||||
an interface identical to httplib.HTTPResponse which is the response
|
||||
expected from higher level classes which use HttpClient.request.
|
||||
|
||||
GenericHttpClient: Provides an interface (superclass) for an object
|
||||
responsible for making HTTP requests. Subclasses of this object are
|
||||
used in AtomService and GDataService to make requests to the server. By
|
||||
changing the http_client member object, the AtomService is able to make
|
||||
HTTP requests using different logic (for example, when running on
|
||||
Google App Engine, the http_client makes requests using the App Engine
|
||||
urlfetch API).
|
||||
"""
|
||||
|
||||
# __author__ = 'api.jscudder (Jeff Scudder)'
|
||||
|
||||
import io
|
||||
|
||||
USER_AGENT = '%s GData-Python/2.0.18'
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnparsableUrlObject(Error):
|
||||
pass
|
||||
|
||||
|
||||
class ContentLengthRequired(Error):
|
||||
pass
|
||||
|
||||
|
||||
class HttpResponse(object):
|
||||
def __init__(self, body=None, status=None, reason=None, headers=None):
|
||||
"""Constructor for an HttpResponse object.
|
||||
|
||||
HttpResponse represents the server's response to an HTTP request from
|
||||
the client. The HttpClient.request method returns a httplib.HTTPResponse
|
||||
object and this HttpResponse class is designed to mirror the interface
|
||||
exposed by httplib.HTTPResponse.
|
||||
|
||||
Args:
|
||||
body: A file like object, with a read() method. The body could also
|
||||
be a string, and the constructor will wrap it so that
|
||||
HttpResponse.read(self) will return the full string.
|
||||
status: The HTTP status code as an int. Example: 200, 201, 404.
|
||||
reason: The HTTP status message which follows the code. Example:
|
||||
OK, Created, Not Found
|
||||
headers: A dictionary containing the HTTP headers in the server's
|
||||
response. A common header in the response is Content-Length.
|
||||
"""
|
||||
if body:
|
||||
if hasattr(body, 'read'):
|
||||
self._body = body
|
||||
else:
|
||||
self._body = io.StringIO(body)
|
||||
else:
|
||||
self._body = None
|
||||
if status is not None:
|
||||
self.status = int(status)
|
||||
else:
|
||||
self.status = None
|
||||
self.reason = reason
|
||||
self._headers = headers or {}
|
||||
|
||||
def getheader(self, name, default=None):
|
||||
if name in self._headers:
|
||||
return self._headers[name]
|
||||
else:
|
||||
return default
|
||||
|
||||
def read(self, amt=None):
|
||||
if not amt:
|
||||
return self._body.read()
|
||||
else:
|
||||
return self._body.read(amt)
|
||||
|
||||
|
||||
class GenericHttpClient(object):
|
||||
debug = False
|
||||
|
||||
def __init__(self, http_client, headers=None):
|
||||
"""
|
||||
|
||||
Args:
|
||||
http_client: An object which provides a request method to make an HTTP
|
||||
request. The request method in GenericHttpClient performs a
|
||||
call-through to the contained HTTP client object.
|
||||
headers: A dictionary containing HTTP headers which should be included
|
||||
in every HTTP request. Common persistent headers include
|
||||
'User-Agent'.
|
||||
"""
|
||||
self.http_client = http_client
|
||||
self.headers = headers or {}
|
||||
|
||||
def request(self, operation, url, data=None, headers=None):
|
||||
all_headers = self.headers.copy()
|
||||
if headers:
|
||||
all_headers.update(headers)
|
||||
return self.http_client.request(operation, url, data=data,
|
||||
headers=all_headers)
|
||||
|
||||
def get(self, url, headers=None):
|
||||
return self.request('GET', url, headers=headers)
|
||||
|
||||
def post(self, url, data, headers=None):
|
||||
return self.request('POST', url, data=data, headers=headers)
|
||||
|
||||
def put(self, url, data, headers=None):
|
||||
return self.request('PUT', url, data=data, headers=headers)
|
||||
|
||||
def delete(self, url, headers=None):
|
||||
return self.request('DELETE', url, headers=headers)
|
||||
|
||||
|
||||
class GenericToken(object):
|
||||
"""Represents an Authorization token to be added to HTTP requests.
|
||||
|
||||
Some Authorization headers included calculated fields (digital
|
||||
signatures for example) which are based on the parameters of the HTTP
|
||||
request. Therefore the token is responsible for signing the request
|
||||
and adding the Authorization header.
|
||||
"""
|
||||
|
||||
def perform_request(self, http_client, operation, url, data=None,
|
||||
headers=None):
|
||||
"""For the GenericToken, no Authorization token is set."""
|
||||
return http_client.request(operation, url, data=data, headers=headers)
|
||||
|
||||
def valid_for_scope(self, url):
|
||||
"""Tells the caller if the token authorizes access to the desired URL.
|
||||
|
||||
Since the generic token doesn't add an auth header, it is not valid for
|
||||
any scope.
|
||||
"""
|
||||
return False
|
||||
123
src/gam/atom/mock_http.py
Normal file
123
src/gam/atom/mock_http.py
Normal file
@@ -0,0 +1,123 @@
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License 2.0;
|
||||
|
||||
|
||||
|
||||
# __author__ = 'api.jscudder (Jeff Scudder)'
|
||||
|
||||
import atom.http_interface
|
||||
import atom.url
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoRecordingFound(Error):
|
||||
pass
|
||||
|
||||
|
||||
class MockRequest(object):
|
||||
"""Holds parameters of an HTTP request for matching against future requests.
|
||||
"""
|
||||
|
||||
def __init__(self, operation, url, data=None, headers=None):
|
||||
self.operation = operation
|
||||
if isinstance(url, str):
|
||||
url = atom.url.parse_url(url)
|
||||
self.url = url
|
||||
self.data = data
|
||||
self.headers = headers
|
||||
|
||||
|
||||
class MockResponse(atom.http_interface.HttpResponse):
|
||||
"""Simulates an httplib.HTTPResponse object."""
|
||||
|
||||
def __init__(self, body=None, status=None, reason=None, headers=None):
|
||||
if body and hasattr(body, 'read'):
|
||||
self.body = body.read()
|
||||
else:
|
||||
self.body = body
|
||||
if status is not None:
|
||||
self.status = int(status)
|
||||
else:
|
||||
self.status = None
|
||||
self.reason = reason
|
||||
self._headers = headers or {}
|
||||
|
||||
def read(self):
|
||||
return self.body
|
||||
|
||||
|
||||
class MockHttpClient(atom.http_interface.GenericHttpClient):
|
||||
def __init__(self, headers=None, recordings=None, real_client=None):
|
||||
"""An HttpClient which responds to request with stored data.
|
||||
|
||||
The request-response pairs are stored as tuples in a member list named
|
||||
recordings.
|
||||
|
||||
The MockHttpClient can be switched from replay mode to record mode by
|
||||
setting the real_client member to an instance of an HttpClient which will
|
||||
make real HTTP requests and store the server's response in list of
|
||||
recordings.
|
||||
|
||||
Args:
|
||||
headers: dict containing HTTP headers which should be included in all
|
||||
HTTP requests.
|
||||
recordings: The initial recordings to be used for responses. This list
|
||||
contains tuples in the form: (MockRequest, MockResponse)
|
||||
real_client: An HttpClient which will make a real HTTP request. The
|
||||
response will be converted into a MockResponse and stored in
|
||||
recordings.
|
||||
"""
|
||||
self.recordings = recordings or []
|
||||
self.real_client = real_client
|
||||
self.headers = headers or {}
|
||||
|
||||
def add_response(self, response, operation, url, data=None, headers=None):
|
||||
"""Adds a request-response pair to the recordings list.
|
||||
|
||||
After the recording is added, future matching requests will receive the
|
||||
response.
|
||||
|
||||
Args:
|
||||
response: MockResponse
|
||||
operation: str
|
||||
url: str
|
||||
data: str, Currently the data is ignored when looking for matching
|
||||
requests.
|
||||
headers: dict of strings: Currently the headers are ignored when
|
||||
looking for matching requests.
|
||||
"""
|
||||
request = MockRequest(operation, url, data=data, headers=headers)
|
||||
self.recordings.append((request, response))
|
||||
|
||||
def request(self, operation, url, data=None, headers=None):
|
||||
"""Returns a matching MockResponse from the recordings.
|
||||
|
||||
If the real_client is set, the request will be passed along and the
|
||||
server's response will be added to the recordings and also returned.
|
||||
|
||||
If there is no match, a NoRecordingFound error will be raised.
|
||||
"""
|
||||
if self.real_client is None:
|
||||
if isinstance(url, str):
|
||||
url = atom.url.parse_url(url)
|
||||
for recording in self.recordings:
|
||||
if recording[0].operation == operation and recording[0].url == url:
|
||||
return recording[1]
|
||||
raise NoRecordingFound('No recodings found for %s %s' % (
|
||||
operation, url))
|
||||
else:
|
||||
# There is a real HTTP client, so make the request, and record the
|
||||
# response.
|
||||
response = self.real_client.request(operation, url, data=data,
|
||||
headers=headers)
|
||||
# TODO: copy the headers
|
||||
stored_response = MockResponse(body=response, status=response.status,
|
||||
reason=response.reason)
|
||||
self.add_response(stored_response, operation, url, data=data,
|
||||
headers=headers)
|
||||
return stored_response
|
||||
313
src/gam/atom/mock_http_core.py
Normal file
313
src/gam/atom/mock_http_core.py
Normal file
@@ -0,0 +1,313 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2009 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License 2.0;
|
||||
|
||||
|
||||
|
||||
# This module is used for version 2 of the Google Data APIs.
|
||||
|
||||
|
||||
# __author__ = 'j.s@google.com (Jeff Scudder)'
|
||||
|
||||
import io
|
||||
import os.path
|
||||
import pickle
|
||||
import tempfile
|
||||
|
||||
import atom.http_core
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoRecordingFound(Error):
|
||||
pass
|
||||
|
||||
|
||||
class MockHttpClient(object):
|
||||
debug = None
|
||||
real_client = None
|
||||
last_request_was_live = False
|
||||
|
||||
# The following members are used to construct the session cache temp file
|
||||
# name.
|
||||
# These are combined to form the file name
|
||||
# /tmp/cache_prefix.cache_case_name.cache_test_name
|
||||
cache_name_prefix = 'gdata_live_test'
|
||||
cache_case_name = ''
|
||||
cache_test_name = ''
|
||||
|
||||
def __init__(self, recordings=None, real_client=None):
|
||||
self._recordings = recordings or []
|
||||
if real_client is not None:
|
||||
self.real_client = real_client
|
||||
|
||||
def add_response(self, http_request, status, reason, headers=None,
|
||||
body=None):
|
||||
response = MockHttpResponse(status, reason, headers, body)
|
||||
# TODO Scrub the request and the response.
|
||||
self._recordings.append((http_request._copy(), response))
|
||||
|
||||
AddResponse = add_response
|
||||
|
||||
def request(self, http_request):
|
||||
"""Provide a recorded response, or record a response for replay.
|
||||
|
||||
If the real_client is set, the request will be made using the
|
||||
real_client, and the response from the server will be recorded.
|
||||
If the real_client is None (the default), this method will examine
|
||||
the recordings and find the first which matches.
|
||||
"""
|
||||
request = http_request._copy()
|
||||
_scrub_request(request)
|
||||
if self.real_client is None:
|
||||
self.last_request_was_live = False
|
||||
for recording in self._recordings:
|
||||
if _match_request(recording[0], request):
|
||||
return recording[1]
|
||||
else:
|
||||
# Pass along the debug settings to the real client.
|
||||
self.real_client.debug = self.debug
|
||||
# Make an actual request since we can use the real HTTP client.
|
||||
self.last_request_was_live = True
|
||||
response = self.real_client.request(http_request)
|
||||
scrubbed_response = _scrub_response(response)
|
||||
self.add_response(request, scrubbed_response.status,
|
||||
scrubbed_response.reason,
|
||||
dict(atom.http_core.get_headers(scrubbed_response)),
|
||||
scrubbed_response.read())
|
||||
# Return the recording which we just added.
|
||||
return self._recordings[-1][1]
|
||||
raise NoRecordingFound('No recoding was found for request: %s %s' % (
|
||||
request.method, str(request.uri)))
|
||||
|
||||
Request = request
|
||||
|
||||
def _save_recordings(self, filename):
|
||||
recording_file = open(os.path.join(tempfile.gettempdir(), filename),
|
||||
'wb')
|
||||
pickle.dump(self._recordings, recording_file)
|
||||
recording_file.close()
|
||||
|
||||
def _load_recordings(self, filename):
|
||||
recording_file = open(os.path.join(tempfile.gettempdir(), filename),
|
||||
'rb')
|
||||
self._recordings = pickle.load(recording_file)
|
||||
recording_file.close()
|
||||
|
||||
def _delete_recordings(self, filename):
|
||||
full_path = os.path.join(tempfile.gettempdir(), filename)
|
||||
if os.path.exists(full_path):
|
||||
os.remove(full_path)
|
||||
|
||||
def _load_or_use_client(self, filename, http_client):
|
||||
if os.path.exists(os.path.join(tempfile.gettempdir(), filename)):
|
||||
self._load_recordings(filename)
|
||||
else:
|
||||
self.real_client = http_client
|
||||
|
||||
def use_cached_session(self, name=None, real_http_client=None):
|
||||
"""Attempts to load recordings from a previous live request.
|
||||
|
||||
If a temp file with the recordings exists, then it is used to fulfill
|
||||
requests. If the file does not exist, then a real client is used to
|
||||
actually make the desired HTTP requests. Requests and responses are
|
||||
recorded and will be written to the desired temprary cache file when
|
||||
close_session is called.
|
||||
|
||||
Args:
|
||||
name: str (optional) The file name of session file to be used. The file
|
||||
is loaded from the temporary directory of this machine. If no name
|
||||
is passed in, a default name will be constructed using the
|
||||
cache_name_prefix, cache_case_name, and cache_test_name of this
|
||||
object.
|
||||
real_http_client: atom.http_core.HttpClient the real client to be used
|
||||
if the cached recordings are not found. If the default
|
||||
value is used, this will be an
|
||||
atom.http_core.HttpClient.
|
||||
"""
|
||||
if real_http_client is None:
|
||||
real_http_client = atom.http_core.HttpClient()
|
||||
if name is None:
|
||||
self._recordings_cache_name = self.get_cache_file_name()
|
||||
else:
|
||||
self._recordings_cache_name = name
|
||||
self._load_or_use_client(self._recordings_cache_name, real_http_client)
|
||||
|
||||
def close_session(self):
|
||||
"""Saves recordings in the temporary file named in use_cached_session."""
|
||||
if self.real_client is not None:
|
||||
self._save_recordings(self._recordings_cache_name)
|
||||
|
||||
def delete_session(self, name=None):
|
||||
"""Removes recordings from a previous live request."""
|
||||
if name is None:
|
||||
self._delete_recordings(self._recordings_cache_name)
|
||||
else:
|
||||
self._delete_recordings(name)
|
||||
|
||||
def get_cache_file_name(self):
|
||||
return '%s.%s.%s' % (self.cache_name_prefix, self.cache_case_name,
|
||||
self.cache_test_name)
|
||||
|
||||
def _dump(self):
|
||||
"""Provides debug information in a string."""
|
||||
output = 'MockHttpClient\n real_client: %s\n cache file name: %s\n' % (
|
||||
self.real_client, self.get_cache_file_name())
|
||||
output += ' recordings:\n'
|
||||
i = 0
|
||||
for recording in self._recordings:
|
||||
output += ' recording %i is for: %s %s\n' % (
|
||||
i, recording[0].method, str(recording[0].uri))
|
||||
i += 1
|
||||
return output
|
||||
|
||||
|
||||
def _match_request(http_request, stored_request):
|
||||
"""Determines whether a request is similar enough to a stored request
|
||||
to cause the stored response to be returned."""
|
||||
# Check to see if the host names match.
|
||||
if (http_request.uri.host is not None
|
||||
and http_request.uri.host != stored_request.uri.host):
|
||||
return False
|
||||
# Check the request path in the URL (/feeds/private/full/x)
|
||||
elif http_request.uri.path != stored_request.uri.path:
|
||||
return False
|
||||
# Check the method used in the request (GET, POST, etc.)
|
||||
elif http_request.method != stored_request.method:
|
||||
return False
|
||||
# If there is a gsession ID in either request, make sure that it is matched
|
||||
# exactly.
|
||||
elif ('gsessionid' in http_request.uri.query
|
||||
or 'gsessionid' in stored_request.uri.query):
|
||||
if 'gsessionid' not in stored_request.uri.query:
|
||||
return False
|
||||
elif 'gsessionid' not in http_request.uri.query:
|
||||
return False
|
||||
elif (http_request.uri.query['gsessionid']
|
||||
!= stored_request.uri.query['gsessionid']):
|
||||
return False
|
||||
# Ignores differences in the query params (?start-index=5&max-results=20),
|
||||
# the body of the request, the port number, HTTP headers, just to name a
|
||||
# few.
|
||||
return True
|
||||
|
||||
|
||||
def _scrub_request(http_request):
|
||||
""" Removes email address and password from a client login request.
|
||||
|
||||
Since the mock server saves the request and response in plantext, sensitive
|
||||
information like the password should be removed before saving the
|
||||
recordings. At the moment only requests sent to a ClientLogin url are
|
||||
scrubbed.
|
||||
"""
|
||||
if (http_request and http_request.uri and http_request.uri.path and
|
||||
http_request.uri.path.endswith('ClientLogin')):
|
||||
# Remove the email and password from a ClientLogin request.
|
||||
http_request._body_parts = []
|
||||
http_request.add_form_inputs(
|
||||
{'form_data': 'client login request has been scrubbed'})
|
||||
else:
|
||||
# We can remove the body of the post from the recorded request, since
|
||||
# the request body is not used when finding a matching recording.
|
||||
http_request._body_parts = []
|
||||
return http_request
|
||||
|
||||
|
||||
def _scrub_response(http_response):
|
||||
return http_response
|
||||
|
||||
|
||||
class EchoHttpClient(object):
|
||||
"""Sends the request data back in the response.
|
||||
|
||||
Used to check the formatting of the request as it was sent. Always responds
|
||||
with a 200 OK, and some information from the HTTP request is returned in
|
||||
special Echo-X headers in the response. The following headers are added
|
||||
in the response:
|
||||
'Echo-Host': The host name and port number to which the HTTP connection is
|
||||
made. If no port was passed in, the header will contain
|
||||
host:None.
|
||||
'Echo-Uri': The path portion of the URL being requested. /example?x=1&y=2
|
||||
'Echo-Scheme': The beginning of the URL, usually 'http' or 'https'
|
||||
'Echo-Method': The HTTP method being used, 'GET', 'POST', 'PUT', etc.
|
||||
"""
|
||||
|
||||
def request(self, http_request):
|
||||
return self._http_request(http_request.uri, http_request.method,
|
||||
http_request.headers, http_request._body_parts)
|
||||
|
||||
def _http_request(self, uri, method, headers=None, body_parts=None):
|
||||
body = io.StringIO()
|
||||
response = atom.http_core.HttpResponse(status=200, reason='OK', body=body)
|
||||
if headers is None:
|
||||
response._headers = {}
|
||||
else:
|
||||
# Copy headers from the request to the response but convert values to
|
||||
# strings. Server response headers always come in as strings, so an int
|
||||
# should be converted to a corresponding string when echoing.
|
||||
for header, value in headers.items():
|
||||
response._headers[header] = str(value)
|
||||
response._headers['Echo-Host'] = '%s:%s' % (uri.host, str(uri.port))
|
||||
response._headers['Echo-Uri'] = uri._get_relative_path()
|
||||
response._headers['Echo-Scheme'] = uri.scheme
|
||||
response._headers['Echo-Method'] = method
|
||||
for part in body_parts:
|
||||
if isinstance(part, str):
|
||||
body.write(part)
|
||||
elif hasattr(part, 'read'):
|
||||
body.write(part.read())
|
||||
body.seek(0)
|
||||
return response
|
||||
|
||||
|
||||
class SettableHttpClient(object):
|
||||
"""An HTTP Client which responds with the data given in set_response."""
|
||||
|
||||
def __init__(self, status, reason, body, headers):
|
||||
"""Configures the response for the server.
|
||||
|
||||
See set_response for details on the arguments to the constructor.
|
||||
"""
|
||||
self.set_response(status, reason, body, headers)
|
||||
self.last_request = None
|
||||
|
||||
def set_response(self, status, reason, body, headers):
|
||||
"""Determines the response which will be sent for each request.
|
||||
|
||||
Args:
|
||||
status: An int for the HTTP status code, example: 200, 404, etc.
|
||||
reason: String for the HTTP reason, example: OK, NOT FOUND, etc.
|
||||
body: The body of the HTTP response as a string or a file-like
|
||||
object (something with a read method).
|
||||
headers: dict of strings containing the HTTP headers in the response.
|
||||
"""
|
||||
self.response = atom.http_core.HttpResponse(status=status, reason=reason,
|
||||
body=body)
|
||||
self.response._headers = headers.copy()
|
||||
|
||||
def request(self, http_request):
|
||||
self.last_request = http_request
|
||||
return self.response
|
||||
|
||||
|
||||
class MockHttpResponse(atom.http_core.HttpResponse):
|
||||
def __init__(self, status=None, reason=None, headers=None, body=None):
|
||||
self._headers = headers or {}
|
||||
if status is not None:
|
||||
self.status = status
|
||||
if reason is not None:
|
||||
self.reason = reason
|
||||
if body is not None:
|
||||
# Instead of using a file-like object for the body, store as a string
|
||||
# so that reads can be repeated.
|
||||
if hasattr(body, 'read'):
|
||||
self._body = body.read()
|
||||
else:
|
||||
self._body = body
|
||||
|
||||
def read(self):
|
||||
return self._body
|
||||
235
src/gam/atom/mock_service.py
Normal file
235
src/gam/atom/mock_service.py
Normal file
@@ -0,0 +1,235 @@
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License 2.0;
|
||||
|
||||
|
||||
|
||||
"""MockService provides CRUD ops. for mocking calls to AtomPub services.
|
||||
|
||||
MockService: Exposes the publicly used methods of AtomService to provide
|
||||
a mock interface which can be used in unit tests.
|
||||
"""
|
||||
|
||||
import pickle
|
||||
|
||||
import atom.service
|
||||
|
||||
# __author__ = 'api.jscudder (Jeffrey Scudder)'
|
||||
|
||||
# Recordings contains pairings of HTTP MockRequest objects with MockHttpResponse objects.
|
||||
recordings = []
|
||||
# If set, the mock service HttpRequest are actually made through this object.
|
||||
real_request_handler = None
|
||||
|
||||
|
||||
def ConcealValueWithSha(source):
|
||||
import sha
|
||||
return sha.new(source[:-5]).hexdigest()
|
||||
|
||||
|
||||
def DumpRecordings(conceal_func=ConcealValueWithSha):
|
||||
if conceal_func:
|
||||
for recording_pair in recordings:
|
||||
recording_pair[0].ConcealSecrets(conceal_func)
|
||||
return pickle.dumps(recordings)
|
||||
|
||||
|
||||
def LoadRecordings(recordings_file_or_string):
|
||||
if isinstance(recordings_file_or_string, str):
|
||||
atom.mock_service.recordings = pickle.loads(recordings_file_or_string)
|
||||
elif hasattr(recordings_file_or_string, 'read'):
|
||||
atom.mock_service.recordings = pickle.loads(
|
||||
recordings_file_or_string.read())
|
||||
|
||||
|
||||
def HttpRequest(service, operation, data, uri, extra_headers=None,
|
||||
url_params=None, escape_params=True, content_type='application/atom+xml'):
|
||||
"""Simulates an HTTP call to the server, makes an actual HTTP request if
|
||||
real_request_handler is set.
|
||||
|
||||
This function operates in two different modes depending on if
|
||||
real_request_handler is set or not. If real_request_handler is not set,
|
||||
HttpRequest will look in this module's recordings list to find a response
|
||||
which matches the parameters in the function call. If real_request_handler
|
||||
is set, this function will call real_request_handler.HttpRequest, add the
|
||||
response to the recordings list, and respond with the actual response.
|
||||
|
||||
Args:
|
||||
service: atom.AtomService object which contains some of the parameters
|
||||
needed to make the request. The following members are used to
|
||||
construct the HTTP call: server (str), additional_headers (dict),
|
||||
port (int), and ssl (bool).
|
||||
operation: str The HTTP operation to be performed. This is usually one of
|
||||
'GET', 'POST', 'PUT', or 'DELETE'
|
||||
data: ElementTree, filestream, list of parts, or other object which can be
|
||||
converted to a string.
|
||||
Should be set to None when performing a GET or PUT.
|
||||
If data is a file-like object which can be read, this method will read
|
||||
a chunk of 100K bytes at a time and send them.
|
||||
If the data is a list of parts to be sent, each part will be evaluated
|
||||
and sent.
|
||||
uri: The beginning of the URL to which the request should be sent.
|
||||
Examples: '/', '/base/feeds/snippets',
|
||||
'/m8/feeds/contacts/default/base'
|
||||
extra_headers: dict of strings. HTTP headers which should be sent
|
||||
in the request. These headers are in addition to those stored in
|
||||
service.additional_headers.
|
||||
url_params: dict of strings. Key value pairs to be added to the URL as
|
||||
URL parameters. For example {'foo':'bar', 'test':'param'} will
|
||||
become ?foo=bar&test=param.
|
||||
escape_params: bool default True. If true, the keys and values in
|
||||
url_params will be URL escaped when the form is constructed
|
||||
(Special characters converted to %XX form.)
|
||||
content_type: str The MIME type for the data being sent. Defaults to
|
||||
'application/atom+xml', this is only used if data is set.
|
||||
"""
|
||||
full_uri = atom.service.BuildUri(uri, url_params, escape_params)
|
||||
(server, port, ssl, uri) = atom.service.ProcessUrl(service, uri)
|
||||
current_request = MockRequest(operation, full_uri, host=server, ssl=ssl,
|
||||
data=data, extra_headers=extra_headers, url_params=url_params,
|
||||
escape_params=escape_params, content_type=content_type)
|
||||
# If the request handler is set, we should actually make the request using
|
||||
# the request handler and record the response to replay later.
|
||||
if real_request_handler:
|
||||
response = real_request_handler.HttpRequest(service, operation, data, uri,
|
||||
extra_headers=extra_headers, url_params=url_params,
|
||||
escape_params=escape_params, content_type=content_type)
|
||||
# TODO: need to copy the HTTP headers from the real response into the
|
||||
# recorded_response.
|
||||
recorded_response = MockHttpResponse(body=response.read(),
|
||||
status=response.status, reason=response.reason)
|
||||
# Insert a tuple which maps the request to the response object returned
|
||||
# when making an HTTP call using the real_request_handler.
|
||||
recordings.append((current_request, recorded_response))
|
||||
return recorded_response
|
||||
else:
|
||||
# Look through available recordings to see if one matches the current
|
||||
# request.
|
||||
for request_response_pair in recordings:
|
||||
if request_response_pair[0].IsMatch(current_request):
|
||||
return request_response_pair[1]
|
||||
return None
|
||||
|
||||
|
||||
class MockRequest(object):
|
||||
"""Represents a request made to an AtomPub server.
|
||||
|
||||
These objects are used to determine if a client request matches a recorded
|
||||
HTTP request to determine what the mock server's response will be.
|
||||
"""
|
||||
|
||||
def __init__(self, operation, uri, host=None, ssl=False, port=None,
|
||||
data=None, extra_headers=None, url_params=None, escape_params=True,
|
||||
content_type='application/atom+xml'):
|
||||
"""Constructor for a MockRequest
|
||||
|
||||
Args:
|
||||
operation: str One of 'GET', 'POST', 'PUT', or 'DELETE' this is the
|
||||
HTTP operation requested on the resource.
|
||||
uri: str The URL describing the resource to be modified or feed to be
|
||||
retrieved. This should include the protocol (http/https) and the host
|
||||
(aka domain). For example, these are some valud full_uris:
|
||||
'http://example.com', 'https://www.google.com/accounts/ClientLogin'
|
||||
host: str (optional) The server name which will be placed at the
|
||||
beginning of the URL if the uri parameter does not begin with 'http'.
|
||||
Examples include 'example.com', 'www.google.com', 'www.blogger.com'.
|
||||
ssl: boolean (optional) If true, the request URL will begin with https
|
||||
instead of http.
|
||||
data: ElementTree, filestream, list of parts, or other object which can be
|
||||
converted to a string. (optional)
|
||||
Should be set to None when performing a GET or PUT.
|
||||
If data is a file-like object which can be read, the constructor
|
||||
will read the entire file into memory. If the data is a list of
|
||||
parts to be sent, each part will be evaluated and stored.
|
||||
extra_headers: dict (optional) HTTP headers included in the request.
|
||||
url_params: dict (optional) Key value pairs which should be added to
|
||||
the URL as URL parameters in the request. For example uri='/',
|
||||
url_parameters={'foo':'1','bar':'2'} could become '/?foo=1&bar=2'.
|
||||
escape_params: boolean (optional) Perform URL escaping on the keys and
|
||||
values specified in url_params. Defaults to True.
|
||||
content_type: str (optional) Provides the MIME type of the data being
|
||||
sent.
|
||||
"""
|
||||
self.operation = operation
|
||||
self.uri = _ConstructFullUrlBase(uri, host=host, ssl=ssl)
|
||||
self.data = data
|
||||
self.extra_headers = extra_headers
|
||||
self.url_params = url_params or {}
|
||||
self.escape_params = escape_params
|
||||
self.content_type = content_type
|
||||
|
||||
def ConcealSecrets(self, conceal_func):
|
||||
"""Conceal secret data in this request."""
|
||||
if 'Authorization' in self.extra_headers:
|
||||
self.extra_headers['Authorization'] = conceal_func(
|
||||
self.extra_headers['Authorization'])
|
||||
|
||||
def IsMatch(self, other_request):
|
||||
"""Check to see if the other_request is equivalent to this request.
|
||||
|
||||
Used to determine if a recording matches an incoming request so that a
|
||||
recorded response should be sent to the client.
|
||||
|
||||
The matching is not exact, only the operation and URL are examined
|
||||
currently.
|
||||
|
||||
Args:
|
||||
other_request: MockRequest The request which we want to check this
|
||||
(self) MockRequest against to see if they are equivalent.
|
||||
"""
|
||||
# More accurate matching logic will likely be required.
|
||||
return (self.operation == other_request.operation and self.uri ==
|
||||
other_request.uri)
|
||||
|
||||
|
||||
def _ConstructFullUrlBase(uri, host=None, ssl=False):
|
||||
"""Puts URL components into the form http(s)://full.host.strinf/uri/path
|
||||
|
||||
Used to construct a roughly canonical URL so that URLs which begin with
|
||||
'http://example.com/' can be compared to a uri of '/' when the host is
|
||||
set to 'example.com'
|
||||
|
||||
If the uri contains 'http://host' already, the host and ssl parameters
|
||||
are ignored.
|
||||
|
||||
Args:
|
||||
uri: str The path component of the URL, examples include '/'
|
||||
host: str (optional) The host name which should prepend the URL. Example:
|
||||
'example.com'
|
||||
ssl: boolean (optional) If true, the returned URL will begin with https
|
||||
instead of http.
|
||||
|
||||
Returns:
|
||||
String which has the form http(s)://example.com/uri/string/contents
|
||||
"""
|
||||
if uri.startswith('http'):
|
||||
return uri
|
||||
if ssl:
|
||||
return 'https://%s%s' % (host, uri)
|
||||
else:
|
||||
return 'http://%s%s' % (host, uri)
|
||||
|
||||
|
||||
class MockHttpResponse(object):
|
||||
"""Returned from MockService crud methods as the server's response."""
|
||||
|
||||
def __init__(self, body=None, status=None, reason=None, headers=None):
|
||||
"""Construct a mock HTTPResponse and set members.
|
||||
|
||||
Args:
|
||||
body: str (optional) The HTTP body of the server's response.
|
||||
status: int (optional)
|
||||
reason: str (optional)
|
||||
headers: dict (optional)
|
||||
"""
|
||||
self.body = body
|
||||
self.status = status
|
||||
self.reason = reason
|
||||
self.headers = headers or {}
|
||||
|
||||
def read(self):
|
||||
return self.body
|
||||
|
||||
def getheader(self, header_name):
|
||||
return self.headers[header_name]
|
||||
723
src/gam/atom/service.py
Normal file
723
src/gam/atom/service.py
Normal file
@@ -0,0 +1,723 @@
|
||||
#
|
||||
# Copyright (C) 2006, 2007, 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License 2.0;
|
||||
|
||||
|
||||
|
||||
"""AtomService provides CRUD ops. in line with the Atom Publishing Protocol.
|
||||
|
||||
AtomService: Encapsulates the ability to perform insert, update and delete
|
||||
operations with the Atom Publishing Protocol on which GData is
|
||||
based. An instance can perform query, insertion, deletion, and
|
||||
update.
|
||||
|
||||
HttpRequest: Function that performs a GET, POST, PUT, or DELETE HTTP request
|
||||
to the specified end point. An AtomService object or a subclass can be
|
||||
used to specify information about the request.
|
||||
"""
|
||||
|
||||
# __author__ = 'api.jscudder (Jeff Scudder)'
|
||||
|
||||
import base64
|
||||
import http.client
|
||||
import os
|
||||
import socket
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import warnings
|
||||
|
||||
import atom.http
|
||||
import atom.http_interface
|
||||
import atom.token_store
|
||||
import atom.url
|
||||
|
||||
import lxml.etree as ElementTree
|
||||
import atom
|
||||
|
||||
|
||||
class AtomService(object):
|
||||
"""Performs Atom Publishing Protocol CRUD operations.
|
||||
|
||||
The AtomService contains methods to perform HTTP CRUD operations.
|
||||
"""
|
||||
|
||||
# Default values for members
|
||||
port = 80
|
||||
ssl = False
|
||||
# Set the current_token to force the AtomService to use this token
|
||||
# instead of searching for an appropriate token in the token_store.
|
||||
current_token = None
|
||||
auto_store_tokens = True
|
||||
auto_set_current_token = True
|
||||
|
||||
def _get_override_token(self):
|
||||
return self.current_token
|
||||
|
||||
def _set_override_token(self, token):
|
||||
self.current_token = token
|
||||
|
||||
override_token = property(_get_override_token, _set_override_token)
|
||||
|
||||
# @atom.v1_deprecated('Please use atom.client.AtomPubClient instead.')
|
||||
def __init__(self, server=None, additional_headers=None,
|
||||
application_name='', http_client=None, token_store=None):
|
||||
"""Creates a new AtomService client.
|
||||
|
||||
Args:
|
||||
server: string (optional) The start of a URL for the server
|
||||
to which all operations should be directed. Example:
|
||||
'www.google.com'
|
||||
additional_headers: dict (optional) Any additional HTTP headers which
|
||||
should be included with CRUD operations.
|
||||
http_client: An object responsible for making HTTP requests using a
|
||||
request method. If none is provided, a new instance of
|
||||
atom.http.ProxiedHttpClient will be used.
|
||||
token_store: Keeps a collection of authorization tokens which can be
|
||||
applied to requests for a specific URLs. Critical methods are
|
||||
find_token based on a URL (atom.url.Url or a string), add_token,
|
||||
and remove_token.
|
||||
"""
|
||||
self.http_client = http_client or atom.http.ProxiedHttpClient()
|
||||
self.token_store = token_store or atom.token_store.TokenStore()
|
||||
self.server = server
|
||||
self.additional_headers = additional_headers or {}
|
||||
self.additional_headers['User-Agent'] = atom.http_interface.USER_AGENT % (
|
||||
application_name,)
|
||||
# If debug is True, the HTTPConnection will display debug information
|
||||
self._set_debug(False)
|
||||
|
||||
__init__ = atom.v1_deprecated(
|
||||
'Please use atom.client.AtomPubClient instead.')(
|
||||
__init__)
|
||||
|
||||
def _get_debug(self):
|
||||
return self.http_client.debug
|
||||
|
||||
def _set_debug(self, value):
|
||||
self.http_client.debug = value
|
||||
|
||||
debug = property(_get_debug, _set_debug,
|
||||
doc='If True, HTTP debug information is printed.')
|
||||
|
||||
def use_basic_auth(self, username, password, scopes=None):
|
||||
if username is not None and password is not None:
|
||||
if scopes is None:
|
||||
scopes = [atom.token_store.SCOPE_ALL]
|
||||
base_64_string = base64.encodestring('%s:%s' % (username, password))
|
||||
token = BasicAuthToken('Basic %s' % base_64_string.strip(),
|
||||
scopes=[atom.token_store.SCOPE_ALL])
|
||||
if self.auto_set_current_token:
|
||||
self.current_token = token
|
||||
if self.auto_store_tokens:
|
||||
return self.token_store.add_token(token)
|
||||
return True
|
||||
return False
|
||||
|
||||
def UseBasicAuth(self, username, password, for_proxy=False):
|
||||
"""Sets an Authenticaiton: Basic HTTP header containing plaintext.
|
||||
|
||||
Deprecated, use use_basic_auth instead.
|
||||
|
||||
The username and password are base64 encoded and added to an HTTP header
|
||||
which will be included in each request. Note that your username and
|
||||
password are sent in plaintext.
|
||||
|
||||
Args:
|
||||
username: str
|
||||
password: str
|
||||
"""
|
||||
self.use_basic_auth(username, password)
|
||||
|
||||
# @atom.v1_deprecated('Please use atom.client.AtomPubClient for requests.')
|
||||
def request(self, operation, url, data=None, headers=None,
|
||||
url_params=None):
|
||||
if isinstance(url, str):
|
||||
if url.startswith('http:') and self.ssl:
|
||||
# Force all requests to be https if self.ssl is True.
|
||||
url = atom.url.parse_url('https:' + url[5:])
|
||||
elif not url.startswith('http') and self.ssl:
|
||||
url = atom.url.parse_url('https://%s%s' % (self.server, url))
|
||||
elif not url.startswith('http'):
|
||||
url = atom.url.parse_url('http://%s%s' % (self.server, url))
|
||||
else:
|
||||
url = atom.url.parse_url(url)
|
||||
|
||||
if url_params:
|
||||
for name, value in url_params.items():
|
||||
url.params[name] = value
|
||||
|
||||
all_headers = self.additional_headers.copy()
|
||||
if headers:
|
||||
all_headers.update(headers)
|
||||
|
||||
# If the list of headers does not include a Content-Length, attempt to
|
||||
# calculate it based on the data object.
|
||||
if data and 'Content-Length' not in all_headers:
|
||||
content_length = CalculateDataLength(data)
|
||||
if content_length:
|
||||
all_headers['Content-Length'] = str(content_length)
|
||||
|
||||
# Find an Authorization token for this URL if one is available.
|
||||
if self.override_token:
|
||||
auth_token = self.override_token
|
||||
else:
|
||||
auth_token = self.token_store.find_token(url)
|
||||
return auth_token.perform_request(self.http_client, operation, url,
|
||||
data=data, headers=all_headers)
|
||||
|
||||
request = atom.v1_deprecated(
|
||||
'Please use atom.client.AtomPubClient for requests.')(
|
||||
request)
|
||||
|
||||
# CRUD operations
|
||||
def Get(self, uri, extra_headers=None, url_params=None, escape_params=True):
|
||||
"""Query the APP server with the given URI
|
||||
|
||||
The uri is the portion of the URI after the server value
|
||||
(server example: 'www.google.com').
|
||||
|
||||
Example use:
|
||||
To perform a query against Google Base, set the server to
|
||||
'base.google.com' and set the uri to '/base/feeds/...', where ... is
|
||||
your query. For example, to find snippets for all digital cameras uri
|
||||
should be set to: '/base/feeds/snippets?bq=digital+camera'
|
||||
|
||||
Args:
|
||||
uri: string The query in the form of a URI. Example:
|
||||
'/base/feeds/snippets?bq=digital+camera'.
|
||||
extra_headers: dicty (optional) Extra HTTP headers to be included
|
||||
in the GET request. These headers are in addition to
|
||||
those stored in the client's additional_headers property.
|
||||
The client automatically sets the Content-Type and
|
||||
Authorization headers.
|
||||
url_params: dict (optional) Additional URL parameters to be included
|
||||
in the query. These are translated into query arguments
|
||||
in the form '&dict_key=value&...'.
|
||||
Example: {'max-results': '250'} becomes &max-results=250
|
||||
escape_params: boolean (optional) If false, the calling code has already
|
||||
ensured that the query will form a valid URL (all
|
||||
reserved characters have been escaped). If true, this
|
||||
method will escape the query and any URL parameters
|
||||
provided.
|
||||
|
||||
Returns:
|
||||
httplib.HTTPResponse The server's response to the GET request.
|
||||
"""
|
||||
return self.request('GET', uri, data=None, headers=extra_headers,
|
||||
url_params=url_params)
|
||||
|
||||
def Post(self, data, uri, extra_headers=None, url_params=None,
|
||||
escape_params=True, content_type='application/atom+xml'):
|
||||
"""Insert data into an APP server at the given URI.
|
||||
|
||||
Args:
|
||||
data: string, ElementTree._Element, or something with a __str__ method
|
||||
The XML to be sent to the uri.
|
||||
uri: string The location (feed) to which the data should be inserted.
|
||||
Example: '/base/feeds/items'.
|
||||
extra_headers: dict (optional) HTTP headers which are to be included.
|
||||
The client automatically sets the Content-Type,
|
||||
Authorization, and Content-Length headers.
|
||||
url_params: dict (optional) Additional URL parameters to be included
|
||||
in the URI. These are translated into query arguments
|
||||
in the form '&dict_key=value&...'.
|
||||
Example: {'max-results': '250'} becomes &max-results=250
|
||||
escape_params: boolean (optional) If false, the calling code has already
|
||||
ensured that the query will form a valid URL (all
|
||||
reserved characters have been escaped). If true, this
|
||||
method will escape the query and any URL parameters
|
||||
provided.
|
||||
|
||||
Returns:
|
||||
httplib.HTTPResponse Server's response to the POST request.
|
||||
"""
|
||||
if extra_headers is None:
|
||||
extra_headers = {}
|
||||
if content_type:
|
||||
extra_headers['Content-Type'] = content_type
|
||||
return self.request('POST', uri, data=data, headers=extra_headers,
|
||||
url_params=url_params)
|
||||
|
||||
def Put(self, data, uri, extra_headers=None, url_params=None,
|
||||
escape_params=True, content_type='application/atom+xml'):
|
||||
"""Updates an entry at the given URI.
|
||||
|
||||
Args:
|
||||
data: string, ElementTree._Element, or xml_wrapper.ElementWrapper The
|
||||
XML containing the updated data.
|
||||
uri: string A URI indicating entry to which the update will be applied.
|
||||
Example: '/base/feeds/items/ITEM-ID'
|
||||
extra_headers: dict (optional) HTTP headers which are to be included.
|
||||
The client automatically sets the Content-Type,
|
||||
Authorization, and Content-Length headers.
|
||||
url_params: dict (optional) Additional URL parameters to be included
|
||||
in the URI. These are translated into query arguments
|
||||
in the form '&dict_key=value&...'.
|
||||
Example: {'max-results': '250'} becomes &max-results=250
|
||||
escape_params: boolean (optional) If false, the calling code has already
|
||||
ensured that the query will form a valid URL (all
|
||||
reserved characters have been escaped). If true, this
|
||||
method will escape the query and any URL parameters
|
||||
provided.
|
||||
|
||||
Returns:
|
||||
httplib.HTTPResponse Server's response to the PUT request.
|
||||
"""
|
||||
if extra_headers is None:
|
||||
extra_headers = {}
|
||||
if content_type:
|
||||
extra_headers['Content-Type'] = content_type
|
||||
return self.request('PUT', uri, data=data, headers=extra_headers,
|
||||
url_params=url_params)
|
||||
|
||||
def Delete(self, uri, extra_headers=None, url_params=None,
|
||||
escape_params=True):
|
||||
"""Deletes the entry at the given URI.
|
||||
|
||||
Args:
|
||||
uri: string The URI of the entry to be deleted. Example:
|
||||
'/base/feeds/items/ITEM-ID'
|
||||
extra_headers: dict (optional) HTTP headers which are to be included.
|
||||
The client automatically sets the Content-Type and
|
||||
Authorization headers.
|
||||
url_params: dict (optional) Additional URL parameters to be included
|
||||
in the URI. These are translated into query arguments
|
||||
in the form '&dict_key=value&...'.
|
||||
Example: {'max-results': '250'} becomes &max-results=250
|
||||
escape_params: boolean (optional) If false, the calling code has already
|
||||
ensured that the query will form a valid URL (all
|
||||
reserved characters have been escaped). If true, this
|
||||
method will escape the query and any URL parameters
|
||||
provided.
|
||||
|
||||
Returns:
|
||||
httplib.HTTPResponse Server's response to the DELETE request.
|
||||
"""
|
||||
return self.request('DELETE', uri, data=None, headers=extra_headers,
|
||||
url_params=url_params)
|
||||
|
||||
|
||||
class BasicAuthToken(atom.http_interface.GenericToken):
|
||||
def __init__(self, auth_header, scopes=None):
|
||||
"""Creates a token used to add Basic Auth headers to HTTP requests.
|
||||
|
||||
Args:
|
||||
auth_header: str The value for the Authorization header.
|
||||
scopes: list of str or atom.url.Url specifying the beginnings of URLs
|
||||
for which this token can be used. For example, if scopes contains
|
||||
'http://example.com/foo', then this token can be used for a request to
|
||||
'http://example.com/foo/bar' but it cannot be used for a request to
|
||||
'http://example.com/baz'
|
||||
"""
|
||||
self.auth_header = auth_header
|
||||
self.scopes = scopes or []
|
||||
|
||||
def perform_request(self, http_client, operation, url, data=None,
|
||||
headers=None):
|
||||
"""Sets the Authorization header to the basic auth string."""
|
||||
if headers is None:
|
||||
headers = {'Authorization': self.auth_header}
|
||||
else:
|
||||
headers['Authorization'] = self.auth_header
|
||||
return http_client.request(operation, url, data=data, headers=headers)
|
||||
|
||||
def __str__(self):
|
||||
return self.auth_header
|
||||
|
||||
def valid_for_scope(self, url):
|
||||
"""Tells the caller if the token authorizes access to the desired URL.
|
||||
"""
|
||||
if isinstance(url, str):
|
||||
url = atom.url.parse_url(url)
|
||||
for scope in self.scopes:
|
||||
if scope == atom.token_store.SCOPE_ALL:
|
||||
return True
|
||||
if isinstance(scope, str):
|
||||
scope = atom.url.parse_url(scope)
|
||||
if scope == url:
|
||||
return True
|
||||
# Check the host and the path, but ignore the port and protocol.
|
||||
elif scope.host == url.host and not scope.path:
|
||||
return True
|
||||
elif scope.host == url.host and scope.path and not url.path:
|
||||
continue
|
||||
elif scope.host == url.host and url.path.startswith(scope.path):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def PrepareConnection(service, full_uri):
|
||||
"""Opens a connection to the server based on the full URI.
|
||||
|
||||
This method is deprecated, instead use atom.http.HttpClient.request.
|
||||
|
||||
Examines the target URI and the proxy settings, which are set as
|
||||
environment variables, to open a connection with the server. This
|
||||
connection is used to make an HTTP request.
|
||||
|
||||
Args:
|
||||
service: atom.AtomService or a subclass. It must have a server string which
|
||||
represents the server host to which the request should be made. It may also
|
||||
have a dictionary of additional_headers to send in the HTTP request.
|
||||
full_uri: str Which is the target relative (lacks protocol and host) or
|
||||
absolute URL to be opened. Example:
|
||||
'https://www.google.com/accounts/ClientLogin' or
|
||||
'base/feeds/snippets' where the server is set to www.google.com.
|
||||
|
||||
Returns:
|
||||
A tuple containing the httplib.HTTPConnection and the full_uri for the
|
||||
request.
|
||||
"""
|
||||
deprecation('calling deprecated function PrepareConnection')
|
||||
(server, port, ssl, partial_uri) = ProcessUrl(service, full_uri)
|
||||
if ssl:
|
||||
# destination is https
|
||||
proxy = os.environ.get('https_proxy')
|
||||
if proxy:
|
||||
(p_server, p_port, p_ssl, p_uri) = ProcessUrl(service, proxy, True)
|
||||
proxy_username = os.environ.get('proxy-username')
|
||||
if not proxy_username:
|
||||
proxy_username = os.environ.get('proxy_username')
|
||||
proxy_password = os.environ.get('proxy-password')
|
||||
if not proxy_password:
|
||||
proxy_password = os.environ.get('proxy_password')
|
||||
if proxy_username:
|
||||
user_auth = base64.encodestring('%s:%s' % (proxy_username,
|
||||
proxy_password))
|
||||
proxy_authorization = ('Proxy-authorization: Basic %s\r\n' % (
|
||||
user_auth.strip()))
|
||||
else:
|
||||
proxy_authorization = ''
|
||||
proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (server, port)
|
||||
user_agent = 'User-Agent: %s\r\n' % (
|
||||
service.additional_headers['User-Agent'])
|
||||
proxy_pieces = (proxy_connect + proxy_authorization + user_agent
|
||||
+ '\r\n')
|
||||
|
||||
# now connect, very simple recv and error checking
|
||||
p_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
p_sock.connect((p_server, p_port))
|
||||
p_sock.sendall(proxy_pieces)
|
||||
response = ''
|
||||
|
||||
# Wait for the full response.
|
||||
while response.find("\r\n\r\n") == -1:
|
||||
response += p_sock.recv(8192)
|
||||
|
||||
p_status = response.split()[1]
|
||||
if p_status != str(200):
|
||||
raise atom.http.ProxyError('Error status=%s' % p_status)
|
||||
|
||||
# Trivial setup for ssl socket.
|
||||
ssl = socket.ssl(p_sock, None, None)
|
||||
fake_sock = http.client.FakeSocket(p_sock, ssl)
|
||||
|
||||
# Initalize httplib and replace with the proxy socket.
|
||||
connection = http.client.HTTPConnection(server)
|
||||
connection.sock = fake_sock
|
||||
full_uri = partial_uri
|
||||
|
||||
else:
|
||||
connection = http.client.HTTPSConnection(server, port)
|
||||
full_uri = partial_uri
|
||||
|
||||
else:
|
||||
# destination is http
|
||||
proxy = os.environ.get('http_proxy')
|
||||
if proxy:
|
||||
(p_server, p_port, p_ssl, p_uri) = ProcessUrl(service.server, proxy, True)
|
||||
proxy_username = os.environ.get('proxy-username')
|
||||
if not proxy_username:
|
||||
proxy_username = os.environ.get('proxy_username')
|
||||
proxy_password = os.environ.get('proxy-password')
|
||||
if not proxy_password:
|
||||
proxy_password = os.environ.get('proxy_password')
|
||||
if proxy_username:
|
||||
UseBasicAuth(service, proxy_username, proxy_password, True)
|
||||
connection = http.client.HTTPConnection(p_server, p_port)
|
||||
if not full_uri.startswith("http://"):
|
||||
if full_uri.startswith("/"):
|
||||
full_uri = "http://%s%s" % (service.server, full_uri)
|
||||
else:
|
||||
full_uri = "http://%s/%s" % (service.server, full_uri)
|
||||
else:
|
||||
connection = http.client.HTTPConnection(server, port)
|
||||
full_uri = partial_uri
|
||||
|
||||
return (connection, full_uri)
|
||||
|
||||
|
||||
def UseBasicAuth(service, username, password, for_proxy=False):
|
||||
"""Sets an Authenticaiton: Basic HTTP header containing plaintext.
|
||||
|
||||
Deprecated, use AtomService.use_basic_auth insread.
|
||||
|
||||
The username and password are base64 encoded and added to an HTTP header
|
||||
which will be included in each request. Note that your username and
|
||||
password are sent in plaintext. The auth header is added to the
|
||||
additional_headers dictionary in the service object.
|
||||
|
||||
Args:
|
||||
service: atom.AtomService or a subclass which has an
|
||||
additional_headers dict as a member.
|
||||
username: str
|
||||
password: str
|
||||
"""
|
||||
deprecation('calling deprecated function UseBasicAuth')
|
||||
base_64_string = base64.encodestring('%s:%s' % (username, password))
|
||||
base_64_string = base_64_string.strip()
|
||||
if for_proxy:
|
||||
header_name = 'Proxy-Authorization'
|
||||
else:
|
||||
header_name = 'Authorization'
|
||||
service.additional_headers[header_name] = 'Basic %s' % (base_64_string,)
|
||||
|
||||
|
||||
def ProcessUrl(service, url, for_proxy=False):
|
||||
"""Processes a passed URL. If the URL does not begin with https?, then
|
||||
the default value for server is used
|
||||
|
||||
This method is deprecated, use atom.url.parse_url instead.
|
||||
"""
|
||||
if not isinstance(url, atom.url.Url):
|
||||
url = atom.url.parse_url(url)
|
||||
|
||||
server = url.host
|
||||
ssl = False
|
||||
port = 80
|
||||
|
||||
if not server:
|
||||
if hasattr(service, 'server'):
|
||||
server = service.server
|
||||
else:
|
||||
server = service
|
||||
if not url.protocol and hasattr(service, 'ssl'):
|
||||
ssl = service.ssl
|
||||
if hasattr(service, 'port'):
|
||||
port = service.port
|
||||
else:
|
||||
if url.protocol == 'https':
|
||||
ssl = True
|
||||
elif url.protocol == 'http':
|
||||
ssl = False
|
||||
if url.port:
|
||||
port = int(url.port)
|
||||
elif port == 80 and ssl:
|
||||
port = 443
|
||||
|
||||
return (server, port, ssl, url.get_request_uri())
|
||||
|
||||
|
||||
def DictionaryToParamList(url_parameters, escape_params=True):
|
||||
"""Convert a dictionary of URL arguments into a URL parameter string.
|
||||
|
||||
This function is deprcated, use atom.url.Url instead.
|
||||
|
||||
Args:
|
||||
url_parameters: The dictionaty of key-value pairs which will be converted
|
||||
into URL parameters. For example,
|
||||
{'dry-run': 'true', 'foo': 'bar'}
|
||||
will become ['dry-run=true', 'foo=bar'].
|
||||
|
||||
Returns:
|
||||
A list which contains a string for each key-value pair. The strings are
|
||||
ready to be incorporated into a URL by using '&'.join([] + parameter_list)
|
||||
"""
|
||||
# Choose which function to use when modifying the query and parameters.
|
||||
# Use quote_plus when escape_params is true.
|
||||
transform_op = [str, urllib.parse.quote_plus][bool(escape_params)]
|
||||
# Create a list of tuples containing the escaped version of the
|
||||
# parameter-value pairs.
|
||||
parameter_tuples = [(transform_op(param), transform_op(value))
|
||||
for param, value in list((url_parameters or {}).items())]
|
||||
# Turn parameter-value tuples into a list of strings in the form
|
||||
# 'PARAMETER=VALUE'.
|
||||
return ['='.join(x) for x in parameter_tuples]
|
||||
|
||||
|
||||
def BuildUri(uri, url_params=None, escape_params=True):
|
||||
"""Converts a uri string and a collection of parameters into a URI.
|
||||
|
||||
This function is deprcated, use atom.url.Url instead.
|
||||
|
||||
Args:
|
||||
uri: string
|
||||
url_params: dict (optional)
|
||||
escape_params: boolean (optional)
|
||||
uri: string The start of the desired URI. This string can alrady contain
|
||||
URL parameters. Examples: '/base/feeds/snippets',
|
||||
'/base/feeds/snippets?bq=digital+camera'
|
||||
url_parameters: dict (optional) Additional URL parameters to be included
|
||||
in the query. These are translated into query arguments
|
||||
in the form '&dict_key=value&...'.
|
||||
Example: {'max-results': '250'} becomes &max-results=250
|
||||
escape_params: boolean (optional) If false, the calling code has already
|
||||
ensured that the query will form a valid URL (all
|
||||
reserved characters have been escaped). If true, this
|
||||
method will escape the query and any URL parameters
|
||||
provided.
|
||||
|
||||
Returns:
|
||||
string The URI consisting of the escaped URL parameters appended to the
|
||||
initial uri string.
|
||||
"""
|
||||
# Prepare URL parameters for inclusion into the GET request.
|
||||
parameter_list = DictionaryToParamList(url_params, escape_params)
|
||||
|
||||
# Append the URL parameters to the URL.
|
||||
if parameter_list:
|
||||
if uri.find('?') != -1:
|
||||
# If there are already URL parameters in the uri string, add the
|
||||
# parameters after a new & character.
|
||||
full_uri = '&'.join([uri] + parameter_list)
|
||||
else:
|
||||
# The uri string did not have any URL parameters (no ? character)
|
||||
# so put a ? between the uri and URL parameters.
|
||||
full_uri = '%s%s' % (uri, '?%s' % ('&'.join([] + parameter_list)))
|
||||
else:
|
||||
full_uri = uri
|
||||
|
||||
return full_uri
|
||||
|
||||
|
||||
def HttpRequest(service, operation, data, uri, extra_headers=None,
|
||||
url_params=None, escape_params=True, content_type='application/atom+xml'):
|
||||
"""Performs an HTTP call to the server, supports GET, POST, PUT, and DELETE.
|
||||
|
||||
This method is deprecated, use atom.http.HttpClient.request instead.
|
||||
|
||||
Usage example, perform and HTTP GET on http://www.google.com/:
|
||||
import atom.service
|
||||
client = atom.service.AtomService()
|
||||
http_response = client.Get('http://www.google.com/')
|
||||
or you could set the client.server to 'www.google.com' and use the
|
||||
following:
|
||||
client.server = 'www.google.com'
|
||||
http_response = client.Get('/')
|
||||
|
||||
Args:
|
||||
service: atom.AtomService object which contains some of the parameters
|
||||
needed to make the request. The following members are used to
|
||||
construct the HTTP call: server (str), additional_headers (dict),
|
||||
port (int), and ssl (bool).
|
||||
operation: str The HTTP operation to be performed. This is usually one of
|
||||
'GET', 'POST', 'PUT', or 'DELETE'
|
||||
data: ElementTree, filestream, list of parts, or other object which can be
|
||||
converted to a string.
|
||||
Should be set to None when performing a GET or PUT.
|
||||
If data is a file-like object which can be read, this method will read
|
||||
a chunk of 100K bytes at a time and send them.
|
||||
If the data is a list of parts to be sent, each part will be evaluated
|
||||
and sent.
|
||||
uri: The beginning of the URL to which the request should be sent.
|
||||
Examples: '/', '/base/feeds/snippets',
|
||||
'/m8/feeds/contacts/default/base'
|
||||
extra_headers: dict of strings. HTTP headers which should be sent
|
||||
in the request. These headers are in addition to those stored in
|
||||
service.additional_headers.
|
||||
url_params: dict of strings. Key value pairs to be added to the URL as
|
||||
URL parameters. For example {'foo':'bar', 'test':'param'} will
|
||||
become ?foo=bar&test=param.
|
||||
escape_params: bool default True. If true, the keys and values in
|
||||
url_params will be URL escaped when the form is constructed
|
||||
(Special characters converted to %XX form.)
|
||||
content_type: str The MIME type for the data being sent. Defaults to
|
||||
'application/atom+xml', this is only used if data is set.
|
||||
"""
|
||||
deprecation('call to deprecated function HttpRequest')
|
||||
full_uri = BuildUri(uri, url_params, escape_params)
|
||||
(connection, full_uri) = PrepareConnection(service, full_uri)
|
||||
|
||||
if extra_headers is None:
|
||||
extra_headers = {}
|
||||
|
||||
# Turn on debug mode if the debug member is set.
|
||||
if service.debug:
|
||||
connection.debuglevel = 1
|
||||
|
||||
connection.putrequest(operation, full_uri)
|
||||
|
||||
# If the list of headers does not include a Content-Length, attempt to
|
||||
# calculate it based on the data object.
|
||||
if (data and 'Content-Length' not in service.additional_headers and
|
||||
'Content-Length' not in extra_headers):
|
||||
content_length = CalculateDataLength(data)
|
||||
if content_length:
|
||||
extra_headers['Content-Length'] = str(content_length)
|
||||
|
||||
if content_type:
|
||||
extra_headers['Content-Type'] = content_type
|
||||
|
||||
# Send the HTTP headers.
|
||||
if isinstance(service.additional_headers, dict):
|
||||
for header in service.additional_headers:
|
||||
connection.putheader(header, service.additional_headers[header])
|
||||
if isinstance(extra_headers, dict):
|
||||
for header in extra_headers:
|
||||
connection.putheader(header, extra_headers[header])
|
||||
connection.endheaders()
|
||||
|
||||
# If there is data, send it in the request.
|
||||
if data:
|
||||
if isinstance(data, list):
|
||||
for data_part in data:
|
||||
__SendDataPart(data_part, connection)
|
||||
else:
|
||||
__SendDataPart(data, connection)
|
||||
|
||||
# Return the HTTP Response from the server.
|
||||
return connection.getresponse()
|
||||
|
||||
|
||||
def __SendDataPart(data, connection):
|
||||
"""This method is deprecated, use atom.http._send_data_part"""
|
||||
deprecated('call to deprecated function __SendDataPart')
|
||||
if isinstance(data, str):
|
||||
# TODO add handling for unicode.
|
||||
connection.send(data)
|
||||
return
|
||||
elif ElementTree.iselement(data):
|
||||
connection.send(ElementTree.tostring(data))
|
||||
return
|
||||
# Check to see if data is a file-like object that has a read method.
|
||||
elif hasattr(data, 'read'):
|
||||
# Read the file and send it a chunk at a time.
|
||||
while 1:
|
||||
binarydata = data.read(100000)
|
||||
if binarydata == '': break
|
||||
connection.send(binarydata)
|
||||
return
|
||||
else:
|
||||
# The data object was not a file.
|
||||
# Try to convert to a string and send the data.
|
||||
connection.send(str(data))
|
||||
return
|
||||
|
||||
|
||||
def CalculateDataLength(data):
|
||||
"""Attempts to determine the length of the data to send.
|
||||
|
||||
This method will respond with a length only if the data is a string or
|
||||
and ElementTree element.
|
||||
|
||||
Args:
|
||||
data: object If this is not a string or ElementTree element this funtion
|
||||
will return None.
|
||||
"""
|
||||
if isinstance(data, str):
|
||||
return len(data)
|
||||
elif isinstance(data, list):
|
||||
return None
|
||||
elif ElementTree.iselement(data):
|
||||
return len(ElementTree.tostring(data))
|
||||
elif hasattr(data, 'read'):
|
||||
# If this is a file-like object, don't try to guess the length.
|
||||
return None
|
||||
else:
|
||||
return len(str(data).encode('utf-8'))
|
||||
|
||||
|
||||
def deprecation(message):
|
||||
warnings.warn(message, DeprecationWarning, stacklevel=2)
|
||||
105
src/gam/atom/token_store.py
Normal file
105
src/gam/atom/token_store.py
Normal file
@@ -0,0 +1,105 @@
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License 2.0;
|
||||
|
||||
|
||||
"""This module provides a TokenStore class which is designed to manage
|
||||
auth tokens required for different services.
|
||||
|
||||
Each token is valid for a set of scopes which is the start of a URL. An HTTP
|
||||
client will use a token store to find a valid Authorization header to send
|
||||
in requests to the specified URL. If the HTTP client determines that a token
|
||||
has expired or been revoked, it can remove the token from the store so that
|
||||
it will not be used in future requests.
|
||||
"""
|
||||
|
||||
# __author__ = 'api.jscudder (Jeff Scudder)'
|
||||
|
||||
import atom.http_interface
|
||||
import atom.url
|
||||
|
||||
SCOPE_ALL = 'http'
|
||||
|
||||
|
||||
class TokenStore(object):
|
||||
"""Manages Authorization tokens which will be sent in HTTP headers."""
|
||||
|
||||
def __init__(self, scoped_tokens=None):
|
||||
self._tokens = scoped_tokens or {}
|
||||
|
||||
def add_token(self, token):
|
||||
"""Adds a new token to the store (replaces tokens with the same scope).
|
||||
|
||||
Args:
|
||||
token: A subclass of http_interface.GenericToken. The token object is
|
||||
responsible for adding the Authorization header to the HTTP request.
|
||||
The scopes defined in the token are used to determine if the token
|
||||
is valid for a requested scope when find_token is called.
|
||||
|
||||
Returns:
|
||||
True if the token was added, False if the token was not added becase
|
||||
no scopes were provided.
|
||||
"""
|
||||
if not hasattr(token, 'scopes') or not token.scopes:
|
||||
return False
|
||||
|
||||
for scope in token.scopes:
|
||||
self._tokens[str(scope)] = token
|
||||
return True
|
||||
|
||||
def find_token(self, url):
|
||||
"""Selects an Authorization header token which can be used for the URL.
|
||||
|
||||
Args:
|
||||
url: str or atom.url.Url or a list containing the same.
|
||||
The URL which is going to be requested. All
|
||||
tokens are examined to see if any scopes begin match the beginning
|
||||
of the URL. The first match found is returned.
|
||||
|
||||
Returns:
|
||||
The token object which should execute the HTTP request. If there was
|
||||
no token for the url (the url did not begin with any of the token
|
||||
scopes available), then the atom.http_interface.GenericToken will be
|
||||
returned because the GenericToken calls through to the http client
|
||||
without adding an Authorization header.
|
||||
"""
|
||||
if url is None:
|
||||
return None
|
||||
if isinstance(url, str):
|
||||
url = atom.url.parse_url(url)
|
||||
if url in self._tokens:
|
||||
token = self._tokens[url]
|
||||
if token.valid_for_scope(url):
|
||||
return token
|
||||
else:
|
||||
del self._tokens[url]
|
||||
for scope, token in self._tokens.items():
|
||||
if token.valid_for_scope(url):
|
||||
return token
|
||||
return atom.http_interface.GenericToken()
|
||||
|
||||
def remove_token(self, token):
|
||||
"""Removes the token from the token_store.
|
||||
|
||||
This method is used when a token is determined to be invalid. If the
|
||||
token was found by find_token, but resulted in a 401 or 403 error stating
|
||||
that the token was invlid, then the token should be removed to prevent
|
||||
future use.
|
||||
|
||||
Returns:
|
||||
True if a token was found and then removed from the token
|
||||
store. False if the token was not in the TokenStore.
|
||||
"""
|
||||
token_found = False
|
||||
scopes_to_delete = []
|
||||
for scope, stored_token in self._tokens.items():
|
||||
if stored_token == token:
|
||||
scopes_to_delete.append(scope)
|
||||
token_found = True
|
||||
for scope in scopes_to_delete:
|
||||
del self._tokens[scope]
|
||||
return token_found
|
||||
|
||||
def remove_all_tokens(self):
|
||||
self._tokens = {}
|
||||
130
src/gam/atom/url.py
Normal file
130
src/gam/atom/url.py
Normal file
@@ -0,0 +1,130 @@
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License 2.0;
|
||||
|
||||
|
||||
|
||||
# __author__ = 'api.jscudder (Jeff Scudder)'
|
||||
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
DEFAULT_PROTOCOL = 'http'
|
||||
DEFAULT_PORT = 80
|
||||
|
||||
|
||||
def parse_url(url_string):
|
||||
"""Creates a Url object which corresponds to the URL string.
|
||||
|
||||
This method can accept partial URLs, but it will leave missing
|
||||
members of the Url unset.
|
||||
"""
|
||||
parts = urllib.parse.urlparse(url_string)
|
||||
url = Url()
|
||||
if parts[0]:
|
||||
url.protocol = parts[0]
|
||||
if parts[1]:
|
||||
host_parts = parts[1].split(':')
|
||||
if host_parts[0]:
|
||||
url.host = host_parts[0]
|
||||
if len(host_parts) > 1:
|
||||
url.port = host_parts[1]
|
||||
if parts[2]:
|
||||
url.path = parts[2]
|
||||
if parts[4]:
|
||||
param_pairs = parts[4].split('&')
|
||||
for pair in param_pairs:
|
||||
pair_parts = pair.split('=')
|
||||
if len(pair_parts) > 1:
|
||||
url.params[urllib.parse.unquote_plus(pair_parts[0])] = (
|
||||
urllib.parse.unquote_plus(pair_parts[1]))
|
||||
elif len(pair_parts) == 1:
|
||||
url.params[urllib.parse.unquote_plus(pair_parts[0])] = None
|
||||
return url
|
||||
|
||||
|
||||
class Url(object):
|
||||
"""Represents a URL and implements comparison logic.
|
||||
|
||||
URL strings which are not identical can still be equivalent, so this object
|
||||
provides a better interface for comparing and manipulating URLs than
|
||||
strings. URL parameters are represented as a dictionary of strings, and
|
||||
defaults are used for the protocol (http) and port (80) if not provided.
|
||||
"""
|
||||
|
||||
def __init__(self, protocol=None, host=None, port=None, path=None,
|
||||
params=None):
|
||||
self.protocol = protocol
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.path = path
|
||||
self.params = params or {}
|
||||
|
||||
def to_string(self):
|
||||
url_parts = ['', '', '', '', '', '']
|
||||
if self.protocol:
|
||||
url_parts[0] = self.protocol
|
||||
if self.host:
|
||||
if self.port:
|
||||
url_parts[1] = ':'.join((self.host, str(self.port)))
|
||||
else:
|
||||
url_parts[1] = self.host
|
||||
if self.path:
|
||||
url_parts[2] = self.path
|
||||
if self.params:
|
||||
url_parts[4] = self.get_param_string()
|
||||
return urllib.parse.urlunparse(url_parts)
|
||||
|
||||
def get_param_string(self):
|
||||
param_pairs = []
|
||||
for key, value in self.params.items():
|
||||
param_pairs.append('='.join((urllib.parse.quote_plus(key),
|
||||
urllib.parse.quote_plus(str(value)))))
|
||||
return '&'.join(param_pairs)
|
||||
|
||||
def get_request_uri(self):
|
||||
"""Returns the path with the parameters escaped and appended."""
|
||||
param_string = self.get_param_string()
|
||||
if param_string:
|
||||
return '?'.join([self.path, param_string])
|
||||
else:
|
||||
return self.path
|
||||
|
||||
def __cmp__(self, other):
|
||||
if not isinstance(other, Url):
|
||||
return cmp(self.to_string(), str(other))
|
||||
difference = 0
|
||||
# Compare the protocol
|
||||
if self.protocol and other.protocol:
|
||||
difference = cmp(self.protocol, other.protocol)
|
||||
elif self.protocol and not other.protocol:
|
||||
difference = cmp(self.protocol, DEFAULT_PROTOCOL)
|
||||
elif not self.protocol and other.protocol:
|
||||
difference = cmp(DEFAULT_PROTOCOL, other.protocol)
|
||||
if difference != 0:
|
||||
return difference
|
||||
# Compare the host
|
||||
difference = cmp(self.host, other.host)
|
||||
if difference != 0:
|
||||
return difference
|
||||
# Compare the port
|
||||
if self.port and other.port:
|
||||
difference = cmp(self.port, other.port)
|
||||
elif self.port and not other.port:
|
||||
difference = cmp(self.port, DEFAULT_PORT)
|
||||
elif not self.port and other.port:
|
||||
difference = cmp(DEFAULT_PORT, other.port)
|
||||
if difference != 0:
|
||||
return difference
|
||||
# Compare the path
|
||||
difference = cmp(self.path, other.path)
|
||||
if difference != 0:
|
||||
return difference
|
||||
# Compare the parameters
|
||||
return cmp(self.params, other.params)
|
||||
|
||||
def __str__(self):
|
||||
return self.to_string()
|
||||
1130
src/gam/cacerts.pem
Normal file
1130
src/gam/cacerts.pem
Normal file
File diff suppressed because it is too large
Load Diff
593
src/gam/cbcm-v1.1beta1.json
Normal file
593
src/gam/cbcm-v1.1beta1.json
Normal file
@@ -0,0 +1,593 @@
|
||||
{
|
||||
"auth": {
|
||||
"oauth2": {
|
||||
"scopes": {
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers": {
|
||||
"description": "View and manage your Chrome browsers registered with Cloud Management"
|
||||
},
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers.readonly": {
|
||||
"description": "View your Chrome browsers registered with Cloud Management"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"basePath": "",
|
||||
"baseUrl": "https://admin.googleapis.com/admin/directory/v1.1beta1/customer/",
|
||||
"batchPath": "batch",
|
||||
"canonicalName": "cbcm",
|
||||
"discoveryVersion": "v1",
|
||||
"documentationLink": "https://support.google.com/chrome/a/answer/9681204",
|
||||
"fullyEncodeReservedExpansion": true,
|
||||
"icons": {
|
||||
"x16": "http://www.google.com/images/icons/product/search-16.gif",
|
||||
"x32": "http://www.google.com/images/icons/product/search-32.gif"
|
||||
},
|
||||
"id": "cbcm:v1.1beta1",
|
||||
"kind": "discovery#restDescription",
|
||||
"mtlsRootUrl": "https://admin.mtls.googleapis.com/",
|
||||
"name": "cbcm",
|
||||
"ownerDomain": "google.com",
|
||||
"ownerName": "Jay Lee",
|
||||
"packagePath": "cbcm",
|
||||
"parameters": {
|
||||
"$.xgafv": {
|
||||
"description": "V1 error format.",
|
||||
"enum": [
|
||||
"1",
|
||||
"2"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"v1 error format",
|
||||
"v2 error format"
|
||||
],
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"access_token": {
|
||||
"description": "OAuth access token.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"alt": {
|
||||
"default": "json",
|
||||
"description": "Data format for response.",
|
||||
"enum": [
|
||||
"json",
|
||||
"media",
|
||||
"proto"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Responses with Content-Type of application/json",
|
||||
"Media download with context-dependent Content-Type",
|
||||
"Responses with Content-Type of application/x-protobuf"
|
||||
],
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"callback": {
|
||||
"description": "JSONP",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"description": "Selector specifying which fields to include in a partial response.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"oauth_token": {
|
||||
"description": "OAuth 2.0 token for the current user.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"prettyPrint": {
|
||||
"default": "true",
|
||||
"description": "Returns response with indentations and line breaks.",
|
||||
"location": "query",
|
||||
"type": "boolean"
|
||||
},
|
||||
"quotaUser": {
|
||||
"description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"uploadType": {
|
||||
"description": "Legacy upload protocol for media (e.g. \"media\", \"multipart\").",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"upload_protocol": {
|
||||
"description": "Upload protocol for media (e.g. \"raw\", \"multipart\").",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"protocol": "rest",
|
||||
"resources": {
|
||||
"chromebrowsers": {
|
||||
"methods": {
|
||||
"delete": {
|
||||
"description": "Deletes a browser.",
|
||||
"flatPath": "{customer}/devices/chromebrowsers/{deviceId}",
|
||||
"httpMethod": "DELETE",
|
||||
"id": "cbcm.chromebrowsers.delete",
|
||||
"parameterOrder": [
|
||||
"customer",
|
||||
"deviceId"
|
||||
],
|
||||
"parameters": {
|
||||
"customer": {
|
||||
"description": "Immutable ID of the G Suite account.",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
"deviceId": {
|
||||
"description": "Immutable ID of the browser.",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": "{customer}/devices/chromebrowsers/{deviceId}",
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"description": "Retrieves a browser.",
|
||||
"flatPath": "{customer}/devices/chromebrowsers/{deviceId}",
|
||||
"httpMethod": "GET",
|
||||
"id": "cbcm.chromebrowsers.get",
|
||||
"parameterOrder": [
|
||||
"customer",
|
||||
"deviceId"
|
||||
],
|
||||
"parameters": {
|
||||
"customer": {
|
||||
"description": "Immutable ID of the G Suite account.",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
"deviceId": {
|
||||
"description": "Immutable ID of the browser.",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
"projection": {
|
||||
"description": "Restrict information returned to a set of selected fields. FULL or BASIC.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": "{customer}/devices/chromebrowsers/{deviceId}",
|
||||
"response": {
|
||||
"$ref": "ChromeBrowser"
|
||||
},
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers",
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers.readonly"
|
||||
]
|
||||
},
|
||||
"list": {
|
||||
"description": "Retrieves a paginated list of all the browsers in a domain.",
|
||||
"flatPath": "{customer}/devices/chromebrowsers",
|
||||
"httpMethod": "GET",
|
||||
"id": "cbcm.chromebrowsers.list",
|
||||
"parameterOrder": [
|
||||
"customer"
|
||||
],
|
||||
"parameters": {
|
||||
"customer": {
|
||||
"description": "Immutable ID of the G Suite account.",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
"maxResults": {
|
||||
"description": "Maximum number of results to return.",
|
||||
"format": "int32",
|
||||
"location": "query",
|
||||
"maximum": "100",
|
||||
"minimum": "1",
|
||||
"type": "integer"
|
||||
},
|
||||
"orderBy": {
|
||||
"description": "property to use for sorting results.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"orgUnitPath": {
|
||||
"description": "The full path of the organizational unit or its unique ID.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"pageToken": {
|
||||
"description": "Token to specify the next page in the list.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"projection": {
|
||||
"description": "Restrict information returned to a set of selected fields. FULL or BASIC.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"query": {
|
||||
"description": "Search string using the list page query language.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"sortOrder": {
|
||||
"description": "Whether to return results in ascending or descending order. Must be used with the orderBy parameter.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": "{customer}/devices/chromebrowsers",
|
||||
"response": {
|
||||
"$ref": "ChromeBrowsers"
|
||||
},
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers",
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers.readonly"
|
||||
]
|
||||
},
|
||||
"moveChromeBrowsersToOu": {
|
||||
"description": "Move Chrome Browsers Device between Organization Units",
|
||||
"flatPath": "{customer}/devices/chromebrowsers/moveChromeBrowsersToOu",
|
||||
"httpMethod": "POST",
|
||||
"id": "cbcm.chromebrowsers.moveChromeBrowsersToOu",
|
||||
"parameterOrder": [
|
||||
"customer"
|
||||
],
|
||||
"parameters": {
|
||||
"customer": {
|
||||
"description": "Immutable ID of the G Suite account.",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": "{customer}/devices/chromebrowsers/moveChromeBrowsersToOu",
|
||||
"request": {
|
||||
"$ref": "MoveChromeBrowsersRequest"
|
||||
},
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
|
||||
]
|
||||
},
|
||||
"update": {
|
||||
"description": "Updates a browser.",
|
||||
"flatPath": "{customer}/devices/chromebrowsers/{deviceId}",
|
||||
"httpMethod": "PUT",
|
||||
"id": "cbcm.chromebrowsers.update",
|
||||
"parameterOrder": [
|
||||
"customer",
|
||||
"deviceId"
|
||||
],
|
||||
"parameters": {
|
||||
"customer": {
|
||||
"description": "Immutable ID of the G Suite account.",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
"deviceId": {
|
||||
"description": "Immutable ID of the browser.",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
"projection": {
|
||||
"description": "BASIC or FULL",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": "{customer}/devices/chromebrowsers/{deviceId}",
|
||||
"request": {
|
||||
"$ref": "ChromeBrowser"
|
||||
},
|
||||
"response": {
|
||||
"$ref": "ChromeBrowser"
|
||||
},
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"enrollmentTokens": {
|
||||
"methods": {
|
||||
"list": {
|
||||
"description": "Retrieves a paginated list of all the browser entollment tokens in a domain.",
|
||||
"flatPath": "{customer}/chrome/enrollmentTokens",
|
||||
"httpMethod": "GET",
|
||||
"id": "cbcm.enrollmentTokens.list",
|
||||
"parameterOrder": [
|
||||
"customer"
|
||||
],
|
||||
"parameters": {
|
||||
"customer": {
|
||||
"description": "Immutable ID of the G Suite account.",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
"pageSize": {
|
||||
"description": "Maximum number of results to return.",
|
||||
"format": "int32",
|
||||
"location": "query",
|
||||
"maximum": "100",
|
||||
"minimum": "1",
|
||||
"type": "integer"
|
||||
},
|
||||
"orgUnitPath": {
|
||||
"description": "The full path of the organizational unit or its unique ID.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"pageToken": {
|
||||
"description": "Token to specify the next page in the list.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"query": {
|
||||
"description": "Search string using the list page query language.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": "{customer}/chrome/enrollmentTokens",
|
||||
"response": {
|
||||
"$ref": "EnrollmentTokens"
|
||||
},
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers",
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers.readonly"
|
||||
]
|
||||
},
|
||||
"create": {
|
||||
"description": "Creates a browser enrollment token in a domain.",
|
||||
"flatPath": "{customer}/chrome/enrollmentTokens",
|
||||
"httpMethod": "POST",
|
||||
"id": "cbcm.enrollmentTokens.create",
|
||||
"parameterOrder": [
|
||||
"customer"
|
||||
],
|
||||
"parameters": {
|
||||
"customer": {
|
||||
"description": "Immutable ID of the G Suite account.",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": "{customer}/chrome/enrollmentTokens",
|
||||
"request": {
|
||||
"$ref": "CreateEnrollmentTokenRequest"
|
||||
},
|
||||
"response": {
|
||||
"$ref": "EnrollmentToken"
|
||||
},
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
|
||||
]
|
||||
},
|
||||
"revoke": {
|
||||
"description": "Revokes a browser enrollment token in a domain.",
|
||||
"flatPath": "{customer}/chrome/enrollmentTokens/{tokenPermanentId}:revoke",
|
||||
"httpMethod": "POST",
|
||||
"id": "cbcm.enrollmentTokens.revoke",
|
||||
"parameterOrder": [
|
||||
"customer",
|
||||
"tokenPermanentId"
|
||||
],
|
||||
"parameters": {
|
||||
"customer": {
|
||||
"description": "Immutable ID of the G Suite account.",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
"tokenPermanentId": {
|
||||
"description": "Unique identifier for an enrollment token.",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": "{customer}/chrome/enrollmentTokens/{tokenPermanentId}:revoke",
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"revision": "20201203",
|
||||
"rootUrl": "https://admin.googleapis.com/admin/directory/v1.1beta1/customer/",
|
||||
"schemas": {
|
||||
"ChromeBrowser": {
|
||||
"id": "ChromeBrowser",
|
||||
"properties": {
|
||||
"annotatedAssetId": {
|
||||
"description": "Asset identifier as annotated by the administrator or specified during enrollment.",
|
||||
"type": "string"
|
||||
},
|
||||
"annotatedLocation": {
|
||||
"description": "Address or location of the device as annotated by the administrator.",
|
||||
"type": "string"
|
||||
},
|
||||
"annotatedNotes": {
|
||||
"description": "Notes about this device as annotated by the administrator",
|
||||
"type": "string"
|
||||
},
|
||||
"annotatedUser": {
|
||||
"description": "User of the device as annotated by the administrator.",
|
||||
"type": "string"
|
||||
},
|
||||
"deviceId": {
|
||||
"annotations": {
|
||||
"required": [
|
||||
"cbcm.chromebrowsers.update"
|
||||
]
|
||||
},
|
||||
"description": "The unique ID of the device.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ChromeBrowsers": {
|
||||
"id": "ChromeBrowsers",
|
||||
"properties": {
|
||||
"browsers": {
|
||||
"description": "List of Chrome browser objects.",
|
||||
"items": {
|
||||
"$ref": "ChromeBrowser"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"etag": {
|
||||
"description": "ETag of the resource.",
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"default": "admin#directory#chromeosdevices",
|
||||
"description": "Kind of resource this is.",
|
||||
"type": "string"
|
||||
},
|
||||
"nextPageToken": {
|
||||
"description": "Token used to access the next page of this result. To access the next page, use this token's value in the `pageToken` query string of this request.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EnrollmentToken": {
|
||||
"id": "EnrollmentToken",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"default": "admin#directory#chromeEnrollmentToken",
|
||||
"description": "Kind of resource this is.",
|
||||
"type": "string"
|
||||
},
|
||||
"tokenId": {
|
||||
"description": "Enrollment Token ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"tokenPermanentId": {
|
||||
"description": "Enrollment Token Permanent ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"customerId": {
|
||||
"description": "Immutable ID of the G Suite account.",
|
||||
"type": "string"
|
||||
},
|
||||
"orgUnitPath": {
|
||||
"description": "The full path of the organizational unit or its unique ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"creatorId": {
|
||||
"description": "Creator ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"createTime": {
|
||||
"description": "Creation Time.",
|
||||
"type": "string"
|
||||
},
|
||||
"revokerId": {
|
||||
"description": "Revoker ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"revokeTime": {
|
||||
"description": "Revoke Time",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EnrollmentTokens": {
|
||||
"id": "EnrollmentTokens",
|
||||
"properties": {
|
||||
"chrome_enrollment_tokens": {
|
||||
"description": "List of Chrome browser enrollment token objects.",
|
||||
"items": {
|
||||
"$ref": "EnrollmentToken"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"default": "admin#directory#chromeEnrollmentTokens",
|
||||
"description": "Kind of resource this is.",
|
||||
"type": "string"
|
||||
},
|
||||
"nextPageToken": {
|
||||
"description": "Token used to access the next page of this result. To access the next page, use this token's value in the `pageToken` query string of this request.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"CreateEnrollmentTokenRequest": {
|
||||
"id": "CreateEnrollmentTokenRequest",
|
||||
"properties": {
|
||||
"org_unit_path": {
|
||||
"description": "The full path of the organizational unit or its unique ID.",
|
||||
"type": "string"
|
||||
},
|
||||
"expire_time": {
|
||||
"description": "Expiration Time.",
|
||||
"type": "string"
|
||||
},
|
||||
"token_type": {
|
||||
"id": "token_type",
|
||||
"annotations": {
|
||||
"required": [
|
||||
"cbcm.enrollmentTokens.create"
|
||||
]
|
||||
},
|
||||
"description": "CHROME_BROWSER.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MoveChromeBrowsersRequest": {
|
||||
"id": "MoveChromeBrowsersRequest",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"org_unit_path": {
|
||||
"annotations": {
|
||||
"required": [
|
||||
"cbcm.chromebrowsers.moveChromeBrowsersToOu"
|
||||
]
|
||||
},
|
||||
"description": "Destination organization unit to move devices to. Full path of the organizational unit or its ID prefixed with id:",
|
||||
"type": "string"
|
||||
},
|
||||
"resource_ids": {
|
||||
"annotations": {
|
||||
"required": [
|
||||
"cbcm.chromebrowsers.moveChromeBrowsersToOu"
|
||||
]
|
||||
},
|
||||
"description": "List of unique device IDs of Chrome Browser Devices to move. A maximum of 600 browsers may be moved per request.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"servicePath": "",
|
||||
"title": "Admin SDK API",
|
||||
"version": "cbcm_v1.1beta1"
|
||||
}
|
||||
249
src/gam/contactdelegation-v1.json
Normal file
249
src/gam/contactdelegation-v1.json
Normal file
@@ -0,0 +1,249 @@
|
||||
{
|
||||
"auth": {
|
||||
"oauth2": {
|
||||
"scopes": {
|
||||
"https://www.googleapis.com/auth/admin.contact.delegation": {
|
||||
"description": "View and manage your Contact Delegation"
|
||||
},
|
||||
"https://www.googleapis.com/auth/admin.contact.delegation.readonly": {
|
||||
"description": "View your Contact Delegation"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"basePath": "",
|
||||
"baseUrl": "https://admin.googleapis.com/admin/contacts/v1/",
|
||||
"batchPath": "batch",
|
||||
"canonicalName": "contactdelegation",
|
||||
"description": "The Contact Delegation API allows Admins to delegate access of one user's, called the delegator, contacts to another user, called the delegate.",
|
||||
"discoveryVersion": "v1",
|
||||
"documentationLink": "https://developers.google.com/admin-sdk/contact-delegation",
|
||||
"fullyEncodeReservedExpansion": true,
|
||||
"icons": {
|
||||
"x16": "http://www.google.com/images/icons/product/search-16.gif",
|
||||
"x32": "http://www.google.com/images/icons/product/search-32.gif"
|
||||
},
|
||||
"id": "contactdelegation:v1",
|
||||
"kind": "discovery#restDescription",
|
||||
"name": "contactdelegation",
|
||||
"ownerDomain": "google.com",
|
||||
"ownerName": "Google",
|
||||
"packagePath": "admin",
|
||||
"parameters": {
|
||||
"$.xgafv": {
|
||||
"description": "V1 error format.",
|
||||
"enum": [
|
||||
"1",
|
||||
"2"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"v1 error format",
|
||||
"v2 error format"
|
||||
],
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"access_token": {
|
||||
"description": "OAuth access token.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"alt": {
|
||||
"default": "json",
|
||||
"description": "Data format for response.",
|
||||
"enum": [
|
||||
"json",
|
||||
"media",
|
||||
"proto"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Responses with Content-Type of application/json",
|
||||
"Media download with context-dependent Content-Type",
|
||||
"Responses with Content-Type of application/x-protobuf"
|
||||
],
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"callback": {
|
||||
"description": "JSONP",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"description": "Selector specifying which fields to include in a partial response.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"oauth_token": {
|
||||
"description": "OAuth 2.0 token for the current user.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"prettyPrint": {
|
||||
"default": "true",
|
||||
"description": "Returns response with indentations and line breaks.",
|
||||
"location": "query",
|
||||
"type": "boolean"
|
||||
},
|
||||
"quotaUser": {
|
||||
"description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"uploadType": {
|
||||
"description": "Legacy upload protocol for media (e.g. \"media\", \"multipart\").",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"upload_protocol": {
|
||||
"description": "Upload protocol for media (e.g. \"raw\", \"multipart\").",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"protocol": "rest",
|
||||
"resources": {
|
||||
"delegates": {
|
||||
"methods": {
|
||||
"create": {
|
||||
"description": "Creates a contact delegations",
|
||||
"flatPath": "users/{user}/delegates",
|
||||
"httpMethod": "POST",
|
||||
"id": "contactdelegations.delegates.create",
|
||||
"parameterOrder": [
|
||||
"user"
|
||||
],
|
||||
"parameters": {
|
||||
"user": {
|
||||
"description": "Email address of the delegator.",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": "users/{user}/delegates/{delegate}",
|
||||
"request": {
|
||||
"$ref": "Delegate"
|
||||
},
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.contact.delegation"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"description": "Deletes a contact delegation.",
|
||||
"flatPath": "users/{user}/delegates/{delegate}",
|
||||
"httpMethod": "DELETE",
|
||||
"id": "contactdelegations.delegates.delete",
|
||||
"parameterOrder": [
|
||||
"user",
|
||||
"delegate"
|
||||
],
|
||||
"parameters": {
|
||||
"delegate": {
|
||||
"description": "Email address of the delegate",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"description": "Email address of the delegator.",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": "users/{user}/delegates/{delegate}",
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.contact.delegation"
|
||||
]
|
||||
},
|
||||
"list": {
|
||||
"description": "Lists contact delegates for a user",
|
||||
"flatPath": "users/{user}/delegates",
|
||||
"httpMethod": "GET",
|
||||
"id": "contactdelegations.delegates.list",
|
||||
"parameterOrder": [
|
||||
"user"
|
||||
],
|
||||
"parameters": {
|
||||
"pageSize": {
|
||||
"description": "Determines how many delegates are returned in each response. ",
|
||||
"format": "int32",
|
||||
"location": "query",
|
||||
"minimum": "1",
|
||||
"type": "integer"
|
||||
},
|
||||
"pageToken": {
|
||||
"description": "Token to specify the next page in the list.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"description": "Email address of the delegator.",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": "users/{user}/delegates",
|
||||
"response": {
|
||||
"$ref": "Delegates"
|
||||
},
|
||||
"scopes": [
|
||||
"https://www.googleapis.com/auth/admin.contact.delegation",
|
||||
"https://www.googleapis.com/auth/admin.contact.delegation.readonly"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rootUrl": "https://admin.googleapis.com/admin/contacts/v1/",
|
||||
"schemas": {
|
||||
"Delegate": {
|
||||
"description": "JSON template for a delegate.",
|
||||
"id": "Delegate",
|
||||
"properties": {
|
||||
"email": {
|
||||
"description": "Email of the delegate.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Delegates": {
|
||||
"id": "Delegates",
|
||||
"properties": {
|
||||
"delegates": {
|
||||
"description": "List of delegates.",
|
||||
"items": {
|
||||
"$ref": "Delegate"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"etag": {
|
||||
"description": "ETag of the resource.",
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"default": "",
|
||||
"description": "Kind of resource this is.",
|
||||
"type": "string"
|
||||
},
|
||||
"nextPageToken": {
|
||||
"description": "Token used to access the next page of this result. To access the next page, use this token's value in the `pageToken` query string of this request.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"servicePath": "",
|
||||
"title": "Contact Delegation API",
|
||||
"version": "v1",
|
||||
"version_module": true
|
||||
}
|
||||
486
src/gam/datastudio-v1.json
Normal file
486
src/gam/datastudio-v1.json
Normal file
@@ -0,0 +1,486 @@
|
||||
{
|
||||
"basePath": "",
|
||||
"discoveryVersion": "v1",
|
||||
"documentationLink": "https://support.google.com/datastudio",
|
||||
"canonicalName": "Data Studio",
|
||||
"id": "datastudio:v1",
|
||||
"ownerName": "Google",
|
||||
"description": "Allows programmatic viewing and editing of Data Studio assets.",
|
||||
"rootUrl": "https://datastudio.googleapis.com/",
|
||||
"ownerDomain": "google.com",
|
||||
"mtlsRootUrl": "https://datastudio.mtls.googleapis.com/",
|
||||
"batchPath": "batch",
|
||||
"version_module": true,
|
||||
"version": "v1",
|
||||
"schemas": {
|
||||
"Asset": {
|
||||
"id": "Asset",
|
||||
"properties": {
|
||||
"title": {
|
||||
"description": "The title of the asset.",
|
||||
"type": "string"
|
||||
},
|
||||
"createTime": {
|
||||
"format": "google-datetime",
|
||||
"description": "Date the asset was created.",
|
||||
"type": "string"
|
||||
},
|
||||
"lastViewByMeTime": {
|
||||
"type": "string",
|
||||
"description": "Date the asset was last viewed by me.",
|
||||
"format": "google-datetime"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "The owner of the asset."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the asset."
|
||||
},
|
||||
"trashed": {
|
||||
"type": "boolean",
|
||||
"description": "Value indicating if the asset is in the trash."
|
||||
},
|
||||
"updateTime": {
|
||||
"format": "google-datetime",
|
||||
"description": "Date the asset was last modified.",
|
||||
"type": "string"
|
||||
},
|
||||
"updateByMeTime": {
|
||||
"description": "Date the asset was last modified by me.",
|
||||
"type": "string",
|
||||
"format": "google-datetime"
|
||||
},
|
||||
"assetType": {
|
||||
"enumDescriptions": [
|
||||
"Asset type not specified.",
|
||||
"A report asset.",
|
||||
"A data Source asset."
|
||||
],
|
||||
"enum": [
|
||||
"ASSET_TYPE_UNSPECIFIED",
|
||||
"REPORT",
|
||||
"DATA_SOURCE"
|
||||
],
|
||||
"description": "The type of the asset.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "A Data Studio asset.",
|
||||
"type": "object"
|
||||
},
|
||||
"SearchAssetsResponse": {
|
||||
"id": "SearchAssetsResponse",
|
||||
"properties": {
|
||||
"assets": {
|
||||
"items": {
|
||||
"$ref": "Asset"
|
||||
},
|
||||
"type": "array",
|
||||
"description": "The list of assets."
|
||||
},
|
||||
"nextPageToken": {
|
||||
"type": "string",
|
||||
"description": "A token to retrieve next page of results. Pass this value in the SearchAssetsRequest.page_token field in the subsequent call to `SearchAssets` method to retrieve the next page of results."
|
||||
}
|
||||
},
|
||||
"description": "Response message for DataStudioService.SearchAssets",
|
||||
"type": "object"
|
||||
},
|
||||
"UpdatePermissionsRequest": {
|
||||
"description": "Request message for DataStudioService.UpdatePermissions",
|
||||
"properties": {
|
||||
"updateMask": {
|
||||
"description": "The list of fields to be updated. Currently not supported.",
|
||||
"type": "string",
|
||||
"format": "google-fieldmask"
|
||||
},
|
||||
"permissions": {
|
||||
"description": "The permissions object to update.",
|
||||
"$ref": "Permissions"
|
||||
}
|
||||
},
|
||||
"id": "UpdatePermissionsRequest",
|
||||
"type": "object"
|
||||
},
|
||||
"AddMembersRequest": {
|
||||
"properties": {
|
||||
"members": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Required. The members to add to the role. The format of a member is one of - user:alice@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-app@appspot.gserviceaccount.com"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"enumDescriptions": [
|
||||
"Role not specified.",
|
||||
"A viewer.",
|
||||
"An editor.",
|
||||
"An owner.",
|
||||
"Link shared viewer.",
|
||||
"Link shared editor."
|
||||
],
|
||||
"enum": [
|
||||
"ROLE_UNSPECIFIED",
|
||||
"VIEWER",
|
||||
"EDITOR",
|
||||
"OWNER",
|
||||
"LINK_VIEWER",
|
||||
"LINK_EDITOR"
|
||||
],
|
||||
"description": "Required. The role to add members to."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"id": "AddMembersRequest",
|
||||
"description": "Request message for DataStudioService.AddMembers"
|
||||
},
|
||||
"Permissions": {
|
||||
"type": "object",
|
||||
"id": "Permissions",
|
||||
"description": "A Data Studio asset's Permissions.",
|
||||
"properties": {
|
||||
"permissions": {
|
||||
"description": "A map from a Role to a list of members. Role is a string representation of the Role enum. One of: - OWNER - EDITOR - VIEWER",
|
||||
"additionalProperties": {
|
||||
"$ref": "Members"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"etag": {
|
||||
"format": "byte",
|
||||
"description": "etag to detect and fail concurrent modifications",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RevokeAllPermissionsRequest": {
|
||||
"description": "Request message for DataStudioService.RevokeAllPermissions",
|
||||
"id": "RevokeAllPermissionsRequest",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"members": {
|
||||
"description": "Required. The members that are having their access revoked. The format of a member is one of - user:alice@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-app@appspot.gserviceaccount.com",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Members": {
|
||||
"description": "A wrapper message for a list of members.",
|
||||
"properties": {
|
||||
"members": {
|
||||
"description": "Format of string is one of - user:alice@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-app@appspot.gserviceaccount.com",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"id": "Members"
|
||||
}
|
||||
},
|
||||
"name": "datastudio",
|
||||
"protocol": "rest",
|
||||
"baseUrl": "https://datastudio.googleapis.com/",
|
||||
"title": "Data Studio API",
|
||||
"revision": "20210412",
|
||||
"fullyEncodeReservedExpansion": true,
|
||||
"icons": {
|
||||
"x32": "http://www.google.com/images/icons/product/search-32.gif",
|
||||
"x16": "http://www.google.com/images/icons/product/search-16.gif"
|
||||
},
|
||||
"parameters": {
|
||||
"quotaUser": {
|
||||
"location": "query",
|
||||
"description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters.",
|
||||
"type": "string"
|
||||
},
|
||||
"prettyPrint": {
|
||||
"location": "query",
|
||||
"type": "boolean",
|
||||
"description": "Returns response with indentations and line breaks.",
|
||||
"default": "true"
|
||||
},
|
||||
"callback": {
|
||||
"location": "query",
|
||||
"type": "string",
|
||||
"description": "JSONP"
|
||||
},
|
||||
"uploadType": {
|
||||
"description": "Legacy upload protocol for media (e.g. \"media\", \"multipart\").",
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"upload_protocol": {
|
||||
"type": "string",
|
||||
"location": "query",
|
||||
"description": "Upload protocol for media (e.g. \"raw\", \"multipart\")."
|
||||
},
|
||||
"$.xgafv": {
|
||||
"enumDescriptions": [
|
||||
"v1 error format",
|
||||
"v2 error format"
|
||||
],
|
||||
"description": "V1 error format.",
|
||||
"location": "query",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
},
|
||||
"oauth_token": {
|
||||
"type": "string",
|
||||
"location": "query",
|
||||
"description": "OAuth 2.0 token for the current user."
|
||||
},
|
||||
"alt": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"json",
|
||||
"media",
|
||||
"proto"
|
||||
],
|
||||
"location": "query",
|
||||
"description": "Data format for response.",
|
||||
"enumDescriptions": [
|
||||
"Responses with Content-Type of application/json",
|
||||
"Media download with context-dependent Content-Type",
|
||||
"Responses with Content-Type of application/x-protobuf"
|
||||
],
|
||||
"default": "json"
|
||||
},
|
||||
"fields": {
|
||||
"description": "Selector specifying which fields to include in a partial response.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"access_token": {
|
||||
"type": "string",
|
||||
"description": "OAuth access token.",
|
||||
"location": "query"
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"location": "query",
|
||||
"description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token."
|
||||
}
|
||||
},
|
||||
"servicePath": "",
|
||||
"kind": "discovery#restDescription",
|
||||
"resources": {
|
||||
"assets": {
|
||||
"methods": {
|
||||
"getPermissions": {
|
||||
"parameters": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"location": "path",
|
||||
"description": "Required. The name of the asset.",
|
||||
"required": true
|
||||
},
|
||||
"role": {
|
||||
"enumDescriptions": [
|
||||
"Role not specified.",
|
||||
"A viewer.",
|
||||
"An editor.",
|
||||
"An owner.",
|
||||
"Link shared viewer.",
|
||||
"Link shared editor."
|
||||
],
|
||||
"type": "string",
|
||||
"location": "query",
|
||||
"description": "The role of the permssion.",
|
||||
"enum": [
|
||||
"ROLE_UNSPECIFIED",
|
||||
"VIEWER",
|
||||
"EDITOR",
|
||||
"OWNER",
|
||||
"LINK_VIEWER",
|
||||
"LINK_EDITOR"
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "datastudio.assets.getPermissions",
|
||||
"response": {
|
||||
"$ref": "Permissions"
|
||||
},
|
||||
"flatPath": "v1/assets/{name}/permissions",
|
||||
"path": "v1/assets/{name}/permissions",
|
||||
"description": "Gets the asset's permission for a given role.",
|
||||
"parameterOrder": [
|
||||
"name"
|
||||
],
|
||||
"httpMethod": "GET"
|
||||
},
|
||||
"updatePermissions": {
|
||||
"id": "datastudio.assets.updatePermissions",
|
||||
"parameterOrder": [
|
||||
"name"
|
||||
],
|
||||
"flatPath": "v1/assets/{name}/permissions",
|
||||
"description": "Updates a permission.",
|
||||
"request": {
|
||||
"$ref": "UpdatePermissionsRequest"
|
||||
},
|
||||
"path": "v1/assets/{name}/permissions",
|
||||
"parameters": {
|
||||
"name": {
|
||||
"description": "Required. The name of the asset.",
|
||||
"location": "path",
|
||||
"type": "string",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Permissions"
|
||||
},
|
||||
"httpMethod": "PATCH"
|
||||
},
|
||||
"get": {
|
||||
"path": "v1/{+name}",
|
||||
"id": "datastudio.assets.get",
|
||||
"parameterOrder": [
|
||||
"name"
|
||||
],
|
||||
"description": "Gets the asset by name.",
|
||||
"parameters": {
|
||||
"name": {
|
||||
"description": "Required. The name of the asset. Format: assets/{asset}",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"pattern": "^assets/[^/]+$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"flatPath": "v1/assets/{assetsId}",
|
||||
"response": {
|
||||
"$ref": "Asset"
|
||||
},
|
||||
"httpMethod": "GET"
|
||||
},
|
||||
"search": {
|
||||
"response": {
|
||||
"$ref": "SearchAssetsResponse"
|
||||
},
|
||||
"path": "v1/assets:search",
|
||||
"parameters": {
|
||||
"pageToken": {
|
||||
"type": "string",
|
||||
"location": "query",
|
||||
"description": "A token identifying a page of results the server should return. Use the value of SearchAssetsResponse.next_page_token returned from the previous call to `SearchAssets` method."
|
||||
},
|
||||
"assetTypes": {
|
||||
"type": "string",
|
||||
"repeated": true,
|
||||
"description": "Exactly one AssetType must be specified.",
|
||||
"enumDescriptions": [
|
||||
"Asset type not specified.",
|
||||
"A report asset.",
|
||||
"A data Source asset."
|
||||
],
|
||||
"location": "query",
|
||||
"enum": [
|
||||
"ASSET_TYPE_UNSPECIFIED",
|
||||
"REPORT",
|
||||
"DATA_SOURCE"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of assets to include. Not an exact match, works the same as search from the UI.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"location": "query",
|
||||
"description": "The email of the owner of the asset."
|
||||
},
|
||||
"pageSize": {
|
||||
"description": "Requested page size. If unspecified, server will pick an appropriate default.",
|
||||
"location": "query",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"orderBy": {
|
||||
"location": "query",
|
||||
"description": "How the results should be ordered. Valid options are: - title",
|
||||
"type": "string"
|
||||
},
|
||||
"includeTrashed": {
|
||||
"location": "query",
|
||||
"type": "boolean",
|
||||
"description": "Value indicating if assets in trash should be included."
|
||||
}
|
||||
},
|
||||
"flatPath": "v1/assets:search",
|
||||
"httpMethod": "GET",
|
||||
"description": "Searches assets.",
|
||||
"id": "datastudio.assets.search",
|
||||
"parameterOrder": []
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"permissions": {
|
||||
"methods": {
|
||||
"revokeAllPermissions": {
|
||||
"path": "v1/assets/{name}/permissions:revokeAllPermissions",
|
||||
"response": {
|
||||
"$ref": "Permissions"
|
||||
},
|
||||
"flatPath": "v1/assets/{name}/permissions:revokeAllPermissions",
|
||||
"id": "datastudio.assets.permissions.revokeAllPermissions",
|
||||
"description": "Revokes one or more members' access to an asset.",
|
||||
"parameters": {
|
||||
"name": {
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"location": "path",
|
||||
"description": "Required. The name of the asset."
|
||||
}
|
||||
},
|
||||
"parameterOrder": [
|
||||
"name"
|
||||
],
|
||||
"httpMethod": "POST",
|
||||
"request": {
|
||||
"$ref": "RevokeAllPermissionsRequest"
|
||||
}
|
||||
},
|
||||
"addMembers": {
|
||||
"path": "v1/assets/{name}/permissions:addMembers",
|
||||
"parameters": {
|
||||
"name": {
|
||||
"required": true,
|
||||
"location": "path",
|
||||
"type": "string",
|
||||
"description": "Required. The name of the asset."
|
||||
}
|
||||
},
|
||||
"httpMethod": "POST",
|
||||
"parameterOrder": [
|
||||
"name"
|
||||
],
|
||||
"response": {
|
||||
"$ref": "Permissions"
|
||||
},
|
||||
"id": "datastudio.assets.permissions.addMembers",
|
||||
"request": {
|
||||
"$ref": "AddMembersRequest"
|
||||
},
|
||||
"description": "Adds one or more members to a role.",
|
||||
"flatPath": "v1/assets/{name}/permissions:addMembers"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/gam/gamlib/__init__.py
Normal file
17
src/gam/gamlib/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2023 Ross Scroggs All Rights Reserved.
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
308
src/gam/gamlib/glaction.py
Normal file
308
src/gam/gamlib/glaction.py
Normal file
@@ -0,0 +1,308 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2024 Ross Scroggs All Rights Reserved.
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""GAM action processing
|
||||
|
||||
"""
|
||||
|
||||
class GamAction():
|
||||
|
||||
# Keys into NAMES; arbitrary values but must be unique
|
||||
ACCEPT = 'acpt'
|
||||
ADD = 'add '
|
||||
ADD_PREVIEW = 'addp'
|
||||
APPEND = 'apnd'
|
||||
APPROVE = 'aprv'
|
||||
ARCHIVE = 'arch'
|
||||
BACKUP = 'back'
|
||||
BLOCK = 'blok'
|
||||
CANCEL = 'canc'
|
||||
CANCEL_WIPE = 'canw'
|
||||
CHECK = 'chek'
|
||||
CLAIM = 'clai'
|
||||
CLAIM_OWNERSHIP = 'clow'
|
||||
CLEAR = 'clea'
|
||||
CLOSE = 'clos'
|
||||
COLLECT = 'coll'
|
||||
COMMENT = 'comm'
|
||||
COPY = 'copy'
|
||||
COPY_MERGE = 'copm'
|
||||
CREATE = 'crea'
|
||||
CREATE_PREVIEW = 'crep'
|
||||
CREATE_SHORTCUT = 'crsc'
|
||||
DEDUP = 'dedu'
|
||||
DELETE = 'dele'
|
||||
DELETE_EMPTY = 'delm'
|
||||
DELETE_PREVIEW = 'delp'
|
||||
DELETE_SHORTCUT = 'desc'
|
||||
DEPROVISION = 'depr'
|
||||
DISABLE = 'disa'
|
||||
DOWNLOAD = 'down'
|
||||
DRAFT = 'draf'
|
||||
EMPTY = 'empt'
|
||||
ENABLE = 'enbl'
|
||||
END = 'end '
|
||||
EXISTS = 'exis'
|
||||
EXPORT = 'expo'
|
||||
EXTRACT = 'extr'
|
||||
GET_COMMAND_RESULT = 'gtcr'
|
||||
FETCH = 'fetc'
|
||||
FORWARD = 'forw'
|
||||
HIDE = 'hide'
|
||||
IMPORT = 'impo'
|
||||
INFO = 'info'
|
||||
INITIALIZE = 'init'
|
||||
INSERT = 'insr'
|
||||
INVALIDATE = 'inva'
|
||||
ISSUE_COMMAND = 'isco'
|
||||
LIST = 'list'
|
||||
LOOKUP = 'look'
|
||||
MERGE = 'merg'
|
||||
MODIFY = 'modi'
|
||||
MOVE = 'move'
|
||||
MOVE_MERGE = 'movm'
|
||||
NOACTION = 'noac'
|
||||
NOACTION_PREVIEW = 'noap'
|
||||
OBLITERATE = 'obli'
|
||||
PERFORM = 'perf'
|
||||
PRE_PROVISIONED_DISABLE ='ppdi'
|
||||
PRE_PROVISIONED_REENABLE ='ppre'
|
||||
PRINT = 'prin'
|
||||
PROCESS = 'proc'
|
||||
PROCESS_PREVIEW = 'prop'
|
||||
PURGE = 'purg'
|
||||
RECREATE = 'recr'
|
||||
REENABLE = 'reen'
|
||||
REFRESH = 'refr'
|
||||
RELABEL = 'rela'
|
||||
REMOVE = 'remo'
|
||||
REMOVE_PREVIEW = 'remp'
|
||||
RENAME = 'rena'
|
||||
REOPEN = 'reop'
|
||||
REPLACE = 'repl'
|
||||
REPLACE_DOMAIN = 'repd'
|
||||
REPORT = 'repo'
|
||||
RESET_YUBIKEY_PIV = 'rpiv'
|
||||
RESPOND = 'resp'
|
||||
RESTORE = 'rest'
|
||||
RESUBMIT = 'res'
|
||||
RETAIN = 'reta'
|
||||
RETRIEVE_DATA = 'retd'
|
||||
REVOKE = 'revo'
|
||||
SAVE = 'save'
|
||||
SEND = 'send'
|
||||
SENDEMAIL = 'snem'
|
||||
SET = 'set '
|
||||
SETUP = 'setu'
|
||||
SHARE = 'shar'
|
||||
SHOW = 'show'
|
||||
SIGNOUT = 'siou'
|
||||
SKIP = 'skip'
|
||||
SPAM = 'spam'
|
||||
SUBMIT = 'subm'
|
||||
SUSPEND = 'susp'
|
||||
SYNC = 'sync'
|
||||
TRANSFER = 'tran'
|
||||
TRANSFER_OWNERSHIP = 'trow'
|
||||
TRASH = 'tras'
|
||||
TURNOFF2SV = 'to2s'
|
||||
UNDELETE = 'unde'
|
||||
UNHIDE = 'unhi'
|
||||
UNSUSPEND = 'unsu'
|
||||
UNTRASH = 'untr'
|
||||
UPDATE = 'upda'
|
||||
UPDATE_MOVE = 'upmo'
|
||||
UPDATE_OWNER = 'updo'
|
||||
UPDATE_PREVIEW = 'updp'
|
||||
UPLOAD = 'uplo'
|
||||
UNZIP = 'unzi'
|
||||
USE = 'use '
|
||||
VERIFY = 'vrfy'
|
||||
WAITFORMAILBOX = 'wamb'
|
||||
WATCH = 'watc'
|
||||
WIPE = 'wipe'
|
||||
WIPE_PREVIEW = 'wipp'
|
||||
# Usage:
|
||||
# ACTION_NAMES[1] n Items - Delete 10 Users
|
||||
# Item xxx ACTION_NAMES[0] - User xxx Deleted
|
||||
# These values can be translated into other languages
|
||||
_NAMES = {
|
||||
ACCEPT: ['Accepted', 'Accept'],
|
||||
ADD: ['Added', 'Add'],
|
||||
ADD_PREVIEW: ['Added (Preview)', 'Add (Preview)'],
|
||||
APPEND: ['Appended', 'Append'],
|
||||
APPROVE: ['Approved', 'Approve'],
|
||||
ARCHIVE: ['Archived', 'Archive'],
|
||||
BACKUP: ['Backed up', 'Backup'],
|
||||
BLOCK: ['Blocked', 'Block'],
|
||||
CANCEL: ['Cancelled', 'Cancel'],
|
||||
CANCEL_WIPE: ['Wipe Cancelled', 'Cancel Wipe'],
|
||||
CHECK: ['Checked', 'Check'],
|
||||
CLAIM: ['Claimed', 'Claim'],
|
||||
CLAIM_OWNERSHIP: ['Ownership Claimed', 'Claim Ownership'],
|
||||
CLEAR: ['Cleared', 'Clear'],
|
||||
CLOSE: ['Closed', 'Close'],
|
||||
COLLECT: ['Collected', 'Collect'],
|
||||
COMMENT: ['Commented', 'Comment'],
|
||||
COPY: ['Copied', 'Copy'],
|
||||
COPY_MERGE: ['Copied(Merge)', 'Copy(Merge)'],
|
||||
CREATE: ['Created', 'Create'],
|
||||
CREATE_PREVIEW: ['Created (Preview)', 'Create (Preview)'],
|
||||
CREATE_SHORTCUT: ['Created Shortcut', 'Create Shortcut'],
|
||||
DEDUP: ['Duplicates Deleted', 'Delete Duplicates'],
|
||||
DELETE: ['Deleted', 'Delete'],
|
||||
DELETE_EMPTY: ['Deleted', 'Delete Empty'],
|
||||
DELETE_PREVIEW: ['Deleted (Preview)', 'Delete (Preview)'],
|
||||
DELETE_SHORTCUT: ['Deleted Shortcut', 'Delete Shortcut'],
|
||||
DEPROVISION: ['Deprovisioned', 'Deprovision'],
|
||||
DISABLE: ['Disabled', 'Disable'],
|
||||
DOWNLOAD: ['Downloaded', 'Download'],
|
||||
DRAFT: ['Drafted', 'Draft'],
|
||||
EMPTY: ['Emptied', 'Empty'],
|
||||
ENABLE: ['Enabled', 'Enable'],
|
||||
END: ['Ended', 'End'],
|
||||
EXISTS: ['Exists', 'Exists'],
|
||||
EXPORT: ['Exported', 'Export'],
|
||||
EXTRACT: ['Extracted', 'Extract'],
|
||||
FORWARD: ['Forwarded', 'Forward'],
|
||||
GET_COMMAND_RESULT: ['Got Command Result', 'Get Command Result'],
|
||||
HIDE: ['Hidden', 'Hide'],
|
||||
IMPORT: ['Imported', 'Import'],
|
||||
INFO: ['Shown', 'Show Info'],
|
||||
INITIALIZE: ['Initialized', 'Initialize'],
|
||||
INSERT: ['Inserted', 'Insert'],
|
||||
INVALIDATE: ['Invalidated', 'Invalidate'],
|
||||
ISSUE_COMMAND: ['Command Issued', 'Issue Command'],
|
||||
LIST: ['Listed', 'List'],
|
||||
LOOKUP: ['Lookedup', 'Lookup'],
|
||||
MERGE: ['Merged', 'Merge'],
|
||||
MODIFY: ['Modified', 'Modify'],
|
||||
MOVE: ['Moved', 'Move'],
|
||||
MOVE_MERGE: ['Moved(Merge)', 'Move(Merge)'],
|
||||
NOACTION: ['No Action', 'No Action'],
|
||||
NOACTION_PREVIEW: ['No Action (Preview)', 'No Action (Preview)'],
|
||||
OBLITERATE: ['Obliterated', 'Obliterate'],
|
||||
PERFORM: ['Action Performed', 'Perform Action'],
|
||||
PRE_PROVISIONED_DISABLE: ['PreProvisioned Disabled', 'PreProvisioned Disable'],
|
||||
PRE_PROVISIONED_REENABLE: ['PreProvisioned Reenabled', 'PreProvisioned Reenable'],
|
||||
PRINT: ['Printed', 'Print'],
|
||||
PROCESS: ['Processed', 'Process'],
|
||||
PROCESS_PREVIEW: ['Processed (Preview)', 'Process (Preview)'],
|
||||
PURGE: ['Purged', 'Purge'],
|
||||
RECREATE: ['Recreated', 'Recreate'],
|
||||
REENABLE: ['Reenabled', 'Reenable'],
|
||||
REFRESH: ['Refreshed', 'Refresh'],
|
||||
RELABEL: ['Relabeled', 'Relabel'],
|
||||
REMOVE: ['Removed', 'Remove'],
|
||||
REMOVE_PREVIEW: ['Removed (Preview)', 'Remove (Preview)'],
|
||||
RENAME: ['Renamed', 'Rename'],
|
||||
REOPEN: ['Reopened', 'Reopen'],
|
||||
REPLACE: ['Replaced', 'Replace'],
|
||||
REPLACE_DOMAIN: ['Domain Replaced', 'Replace Domain'],
|
||||
REPORT: ['Reported', 'Report'],
|
||||
RESET_YUBIKEY_PIV: ['Yubikey PIV Reset', 'Reset Yubikey PIV'],
|
||||
RESPOND: ['Responded', 'Respond'],
|
||||
RESTORE: ['Restored', 'Restore'],
|
||||
RESUBMIT: ['Resubmitted', 'Resubmit'],
|
||||
RETAIN: ['Retained', 'Retain'],
|
||||
RETRIEVE_DATA: ['Data Retrieved', 'Retrieve Data'],
|
||||
REVOKE: ['Revoked', 'Revoke'],
|
||||
SAVE: ['Saved', 'Save'],
|
||||
SEND: ['Sent', 'Send'],
|
||||
SENDEMAIL: ['Email Sent', 'Send Email'],
|
||||
SET: ['Set', 'Set'],
|
||||
SETUP: ['Set Up', 'Set Up'],
|
||||
SHARE: ['Shared', 'Share'],
|
||||
SHOW: ['Shown', 'Show'],
|
||||
SIGNOUT: ['Signed Out', 'Signout'],
|
||||
SKIP: ['Skipped', 'Skip'],
|
||||
SPAM: ['Marked as Spam', 'Mark as Spam'],
|
||||
SUBMIT: ['Submitted', 'Submit'],
|
||||
SUSPEND: ['Suspended', 'Suspend'],
|
||||
SYNC: ['Synced', 'Sync'],
|
||||
TRANSFER: ['Transferred', 'Transfer'],
|
||||
TRANSFER_OWNERSHIP: ['Ownership Transferred', 'Transfer Ownership'],
|
||||
TRASH: ['Trashed', 'Trash'],
|
||||
TURNOFF2SV: ['2-Step Verification Turned Off', 'Turn Off 2-Step Verification'],
|
||||
UNDELETE: ['Undeleted', 'Undelete'],
|
||||
UNHIDE: ['Unhidden', 'Unhide'],
|
||||
UNSUSPEND: ['Unsuspended', 'Unsuspend'],
|
||||
UNTRASH: ['Untrashed', 'Untrash'],
|
||||
UNZIP: ['Unzipped', 'Unzip'],
|
||||
UPDATE: ['Updated', 'Update'],
|
||||
UPDATE_MOVE: ['Updated/Moved', 'Update/Move'],
|
||||
UPDATE_OWNER: ['Updated to Owner', 'Update to Owner'],
|
||||
UPDATE_PREVIEW: ['Updated (Preview)', 'Update (Preview)'],
|
||||
UPLOAD: ['Uploaded', 'Upload'],
|
||||
USE: ['Used', 'Use'],
|
||||
VERIFY: ['Verified', 'Verify'],
|
||||
WAITFORMAILBOX: ['Mailbox is Setup', 'Check Mailbox is Setup'],
|
||||
WATCH: ['Watched', 'Watch'],
|
||||
WIPE: ['Wiped', 'Wipe'],
|
||||
WIPE_PREVIEW: ['Wiped (Preview)', 'Wipe (Preview)'],
|
||||
}
|
||||
#
|
||||
MODIFIER_CONTENTS_WITH = 'contents with'
|
||||
MODIFIER_FOR = 'for'
|
||||
MODIFIER_FROM = 'from'
|
||||
MODIFIER_IN = 'in'
|
||||
MODIFIER_INTO = 'into'
|
||||
MODIFIER_PREVIOUSLY_IN = 'previously in'
|
||||
MODIFIER_TO = 'to'
|
||||
MODIFIER_WITH_COTEACHER_OWNER = 'with co-teacher as owner'
|
||||
MODIFIER_WITH_NEW_TEACHER_OWNER = 'with new teacher as owner'
|
||||
MODIFIER_WITH_CURRENT_OWNER = 'with current owner'
|
||||
MODIFIER_WITH = 'with'
|
||||
MODIFIER_WITH_CONTENT_FROM = 'with content from'
|
||||
PREFIX_NOT = 'Not'
|
||||
PREVIEW = 'Preview'
|
||||
SUCCESS = 'Success'
|
||||
SUFFIX_FAILED = 'Failed'
|
||||
|
||||
def __init__(self):
|
||||
self.action = None
|
||||
|
||||
def Set(self, action):
|
||||
self.action = action
|
||||
|
||||
def Get(self):
|
||||
return self.action
|
||||
|
||||
def ToPerform(self):
|
||||
return self._NAMES[self.action][1]
|
||||
|
||||
def Performed(self):
|
||||
return self._NAMES[self.action][0]
|
||||
|
||||
def Failed(self):
|
||||
return f'{self._NAMES[self.action][1]} {self.SUFFIX_FAILED}'
|
||||
|
||||
def NotPerformed(self):
|
||||
actionWords = self._NAMES[self.action][0].split(' ')
|
||||
if len(actionWords) != 2:
|
||||
return f'{self.PREFIX_NOT} {self._NAMES[self.action][0]}'
|
||||
return f'{actionWords[0]} {self.PREFIX_NOT} {actionWords[1]}'
|
||||
|
||||
def PerformedName(self, action):
|
||||
return self._NAMES[action][0]
|
||||
|
||||
def ToPerformName(self, action):
|
||||
return self._NAMES[action][1]
|
||||
|
||||
def csvFormat(self):
|
||||
return self.action == self.PRINT
|
||||
834
src/gam/gamlib/glapi.py
Normal file
834
src/gam/gamlib/glapi.py
Normal file
@@ -0,0 +1,834 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2024 Ross Scroggs All Rights Reserved.
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Google API resources
|
||||
|
||||
"""
|
||||
# APIs
|
||||
ACCESSCONTEXTMANAGER = 'accesscontextmanager'
|
||||
ALERTCENTER = 'alertcenter'
|
||||
ANALYTICS = 'analytics'
|
||||
ANALYTICS_ADMIN = 'analyticsadmin'
|
||||
CALENDAR = 'calendar'
|
||||
CBCM = 'cbcm'
|
||||
CHAT = 'chat'
|
||||
CHAT_EVENTS = 'chatevents'
|
||||
CHAT_MEMBERSHIPS = 'chatmemberships'
|
||||
CHAT_MEMBERSHIPS_ADMIN = 'chatmembershipsadmin'
|
||||
CHAT_MESSAGES = 'chatmessages'
|
||||
CHAT_SPACES = 'chatspaces'
|
||||
CHAT_SPACES_ADMIN = 'chatspacesadmin'
|
||||
CHAT_SPACES_DELETE = 'chatspacesdelete'
|
||||
CHAT_SPACES_DELETE_ADMIN = 'chatspacesdeleteadmin'
|
||||
CHROMEMANAGEMENT = 'chromemanagement'
|
||||
CHROMEMANAGEMENT_APPDETAILS = 'chromemanagementappdetails'
|
||||
CHROMEMANAGEMENT_CHROMEPROFILES = 'chromemanagementchromeprofiles'
|
||||
CHROMEMANAGEMENT_TELEMETRY = 'chromemanagementtelemetry'
|
||||
CHROMEPOLICY = 'chromepolicy'
|
||||
CHROMEVERSIONHISTORY = 'versionhistory'
|
||||
CLASSROOM = 'classroom'
|
||||
CLOUDCHANNEL = 'cloudchannel'
|
||||
CLOUDIDENTITY_DEVICES = 'cloudidentitydevices'
|
||||
CLOUDIDENTITY_GROUPS = 'cloudidentitygroups'
|
||||
CLOUDIDENTITY_INBOUND_SSO = 'cloudidentityinboundsso'
|
||||
CLOUDIDENTITY_ORGUNITS = 'cloudidentityorgunits'
|
||||
CLOUDIDENTITY_POLICY = 'cloudidentitypolicy'
|
||||
CLOUDIDENTITY_ORGUNITS_BETA = 'cloudidentityorgunitsbeta'
|
||||
CLOUDIDENTITY_USERINVITATIONS = 'cloudidentityuserinvitations'
|
||||
CLOUDRESOURCEMANAGER = 'cloudresourcemanager'
|
||||
CONTACTS = 'contacts'
|
||||
CONTACTDELEGATION = 'contactdelegation'
|
||||
DATATRANSFER = 'datatransfer'
|
||||
DIRECTORY = 'directory'
|
||||
DOCS = 'docs'
|
||||
DRIVE2 = 'drive2'
|
||||
DRIVE3 = 'drive3'
|
||||
DRIVETD = 'drivetd'
|
||||
DRIVEACTIVITY = 'driveactivity'
|
||||
DRIVELABELS = 'drivelabels'
|
||||
DRIVELABELS_ADMIN = 'drivelabelsadmin'
|
||||
DRIVELABELS_USER = 'drivelabelsuser'
|
||||
EMAIL_AUDIT = 'email-audit'
|
||||
FORMS = 'forms'
|
||||
GMAIL = 'gmail'
|
||||
GROUPSMIGRATION = 'groupsmigration'
|
||||
GROUPSSETTINGS = 'groupssettings'
|
||||
IAM = 'iam'
|
||||
IAM_CREDENTIALS = 'iamcredentials'
|
||||
IAP = 'iap'
|
||||
KEEP = 'keep'
|
||||
LICENSING = 'licensing'
|
||||
LOOKERSTUDIO = 'datastudio'
|
||||
MEET = 'meet'
|
||||
OAUTH2 = 'oauth2'
|
||||
ORGPOLICY = 'orgpolicy'
|
||||
PEOPLE = 'people'
|
||||
PEOPLE_DIRECTORY = 'peopledirectory'
|
||||
PEOPLE_OTHERCONTACTS = 'peopleothercontacts'
|
||||
PRINTERS = 'printers'
|
||||
PUBSUB = 'pubsub'
|
||||
REPORTS = 'reports'
|
||||
RESELLER = 'reseller'
|
||||
SERVICEACCOUNTLOOKUP = 'serviceaccountlookup'
|
||||
SERVICEMANAGEMENT = 'servicemanagement'
|
||||
SERVICEUSAGE = 'serviceusage'
|
||||
SHEETS = 'sheets'
|
||||
SHEETSTD = 'sheetstd'
|
||||
SITES = 'sites'
|
||||
SITEVERIFICATION = 'siteVerification'
|
||||
STORAGE = 'storage'
|
||||
STORAGEREAD = 'storageread'
|
||||
STORAGEWRITE = 'storagewrite'
|
||||
TASKS = 'tasks'
|
||||
VAULT = 'vault'
|
||||
YOUTUBE = 'youtube'
|
||||
#
|
||||
CHROMEVERSIONHISTORY_URL = 'https://versionhistory.googleapis.com/v1/chrome/platforms'
|
||||
DRIVE_SCOPE = 'https://www.googleapis.com/auth/drive'
|
||||
GMAIL_SEND_SCOPE = 'https://www.googleapis.com/auth/gmail.send'
|
||||
GOOGLE_AUTH_PROVIDER_X509_CERT_URL = 'https://www.googleapis.com/oauth2/v1/certs'
|
||||
GOOGLE_OAUTH2_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth'
|
||||
GOOGLE_OAUTH2_TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token'
|
||||
CLOUD_PLATFORM_SCOPE = 'https://www.googleapis.com/auth/cloud-platform'
|
||||
IAM_SCOPE = 'https://www.googleapis.com/auth/iam'
|
||||
PEOPLE_SCOPE = 'https://www.googleapis.com/auth/contacts'
|
||||
STORAGE_READONLY_SCOPE = 'https://www.googleapis.com/auth/devstorage.read_only'
|
||||
STORAGE_READWRITE_SCOPE = 'https://www.googleapis.com/auth/devstorage.read_write'
|
||||
USERINFO_EMAIL_SCOPE = 'https://www.googleapis.com/auth/userinfo.email' # email
|
||||
USERINFO_PROFILE_SCOPE = 'https://www.googleapis.com/auth/userinfo.profile' # profile
|
||||
VAULT_SCOPES = ['https://www.googleapis.com/auth/ediscovery', 'https://www.googleapis.com/auth/ediscovery.readonly']
|
||||
REQUIRED_SCOPES = [USERINFO_EMAIL_SCOPE, USERINFO_PROFILE_SCOPE]
|
||||
REQUIRED_SCOPES_SET = set(REQUIRED_SCOPES)
|
||||
#
|
||||
JWT_APIS = {
|
||||
ACCESSCONTEXTMANAGER: [CLOUD_PLATFORM_SCOPE],
|
||||
CHAT: ['https://www.googleapis.com/auth/chat.bot'],
|
||||
CLOUDRESOURCEMANAGER: [CLOUD_PLATFORM_SCOPE],
|
||||
ORGPOLICY: [CLOUD_PLATFORM_SCOPE],
|
||||
}
|
||||
#
|
||||
SCOPELESS_APIS = {
|
||||
CHROMEVERSIONHISTORY,
|
||||
OAUTH2,
|
||||
SERVICEACCOUNTLOOKUP,
|
||||
}
|
||||
#
|
||||
APIS_NEEDING_ACCESS_TOKEN = {
|
||||
CBCM: ['https://www.googleapis.com/auth/admin.directory.device.chromebrowsers']
|
||||
}
|
||||
#
|
||||
REFRESH_PERM_ERRORS = [
|
||||
'invalid_grant: reauth related error (rapt_required)', # no way to reauth today
|
||||
'invalid_grant: Token has been expired or revoked',
|
||||
]
|
||||
|
||||
OAUTH2_TOKEN_ERRORS = [
|
||||
'access_denied',
|
||||
'access_denied: Requested client not authorized',
|
||||
'access_denied: Account restricted',
|
||||
'internal_failure: Backend Error',
|
||||
'internal_failure: None',
|
||||
'invalid_grant',
|
||||
'invalid_grant: Bad Request',
|
||||
'invalid_grant: Invalid email or User ID',
|
||||
'invalid_grant: Not a valid email',
|
||||
'invalid_grant: Invalid JWT: No valid verifier found for issuer',
|
||||
'invalid_grant: reauth related error (invalid_rapt)',
|
||||
'invalid_grant: The account has been deleted',
|
||||
'invalid_request: Invalid impersonation prn email address'
|
||||
]
|
||||
OAUTH2_UNAUTHORIZED_ERRORS = [
|
||||
'unauthorized_client: Client is unauthorized to retrieve access tokens using this method',
|
||||
'unauthorized_client: Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested',
|
||||
'unauthorized_client: Unauthorized client or scope in request',
|
||||
]
|
||||
|
||||
PROJECT_APIS = [
|
||||
'accesscontextmanager.googleapis.com',
|
||||
'admin.googleapis.com',
|
||||
'alertcenter.googleapis.com',
|
||||
'analytics.googleapis.com',
|
||||
'analyticsadmin.googleapis.com',
|
||||
# 'audit.googleapis.com',
|
||||
'calendar-json.googleapis.com',
|
||||
'chat.googleapis.com',
|
||||
'chromemanagement.googleapis.com',
|
||||
'chromepolicy.googleapis.com',
|
||||
'classroom.googleapis.com',
|
||||
'cloudchannel.googleapis.com',
|
||||
'cloudidentity.googleapis.com',
|
||||
'cloudresourcemanager.googleapis.com',
|
||||
'contacts.googleapis.com',
|
||||
'datastudio.googleapis.com',
|
||||
'docs.googleapis.com',
|
||||
'drive.googleapis.com',
|
||||
'driveactivity.googleapis.com',
|
||||
'drivelabels.googleapis.com',
|
||||
'forms.googleapis.com',
|
||||
'gmail.googleapis.com',
|
||||
'groupsmigration.googleapis.com',
|
||||
'groupssettings.googleapis.com',
|
||||
'iam.googleapis.com',
|
||||
'iap.googleapis.com',
|
||||
'keep.googleapis.com',
|
||||
'licensing.googleapis.com',
|
||||
'meet.googleapis.com',
|
||||
'people.googleapis.com',
|
||||
'pubsub.googleapis.com',
|
||||
'reseller.googleapis.com',
|
||||
'sheets.googleapis.com',
|
||||
'siteverification.googleapis.com',
|
||||
'storage-api.googleapis.com',
|
||||
'tasks.googleapis.com',
|
||||
'vault.googleapis.com',
|
||||
'youtube.googleapis.com',
|
||||
]
|
||||
|
||||
_INFO = {
|
||||
ACCESSCONTEXTMANAGER: {'name': 'Access Context Manager API', 'version': 'v1', 'v2discovery': True},
|
||||
ALERTCENTER: {'name': 'AlertCenter API', 'version': 'v1beta1', 'v2discovery': True},
|
||||
ANALYTICS: {'name': 'Analytics API', 'version': 'v3', 'v2discovery': False},
|
||||
ANALYTICS_ADMIN: {'name': 'Analytics Admin API', 'version': 'v1beta', 'v2discovery': True},
|
||||
CALENDAR: {'name': 'Calendar API', 'version': 'v3', 'v2discovery': True, 'mappedAPI': 'calendar-json'},
|
||||
CBCM: {'name': 'Chrome Browser Cloud Management API', 'version': 'v1.1beta1', 'v2discovery': True, 'localjson': True},
|
||||
CHAT: {'name': 'Chat API', 'version': 'v1', 'v2discovery': True},
|
||||
CHAT_EVENTS: {'name': 'Chat API - Events', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
|
||||
CHAT_MEMBERSHIPS: {'name': 'Chat API - Memberships', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
|
||||
CHAT_MEMBERSHIPS_ADMIN: {'name': 'Chat API - Memberships Admin', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
|
||||
CHAT_MESSAGES: {'name': 'Chat API - Messages', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
|
||||
CHAT_SPACES: {'name': 'Chat API - Spaces', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
|
||||
CHAT_SPACES_ADMIN: {'name': 'Chat API - Spaces Admin', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
|
||||
CHAT_SPACES_DELETE: {'name': 'Chat API - Spaces Delete', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
|
||||
CHAT_SPACES_DELETE_ADMIN: {'name': 'Chat API - Spaces Delete Admin', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
|
||||
CLASSROOM: {'name': 'Classroom API', 'version': 'v1', 'v2discovery': True},
|
||||
CHROMEMANAGEMENT: {'name': 'Chrome Management API', 'version': 'v1', 'v2discovery': True},
|
||||
CHROMEMANAGEMENT_APPDETAILS: {'name': 'Chrome Management API - AppDetails', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHROMEMANAGEMENT},
|
||||
CHROMEMANAGEMENT_TELEMETRY: {'name': 'Chrome Management API - Telemetry', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHROMEMANAGEMENT},
|
||||
CHROMEPOLICY: {'name': 'Chrome Policy API', 'version': 'v1', 'v2discovery': True},
|
||||
CHROMEVERSIONHISTORY: {'name': 'Chrome Version History API', 'version': 'v1', 'v2discovery': True},
|
||||
CLOUDCHANNEL: {'name': 'Channel Channel API', 'version': 'v1', 'v2discovery': True},
|
||||
CLOUDIDENTITY_DEVICES: {'name': 'Cloud Identity Devices API', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
|
||||
CLOUDIDENTITY_GROUPS: {'name': 'Cloud Identity Groups API', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
|
||||
CLOUDIDENTITY_INBOUND_SSO: {'name': 'Cloud Identity Inbound SSO API', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
|
||||
CLOUDIDENTITY_ORGUNITS: {'name': 'Cloud Identity OrgUnits API', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
|
||||
CLOUDIDENTITY_ORGUNITS_BETA: {'name': 'Cloud Identity OrgUnits API', 'version': 'v1beta1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
|
||||
CLOUDIDENTITY_POLICY: {'name': 'Cloud Identity Policy API', 'version': 'v1beta1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
|
||||
CLOUDIDENTITY_USERINVITATIONS: {'name': 'Cloud Identity User Invitations API', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
|
||||
CLOUDRESOURCEMANAGER: {'name': 'Cloud Resource Manager API v3', 'version': 'v3', 'v2discovery': True},
|
||||
CONTACTS: {'name': 'Contacts API', 'version': 'v3', 'v2discovery': False},
|
||||
CONTACTDELEGATION: {'name': 'Contact Delegation API', 'version': 'v1', 'v2discovery': True, 'localjson': True},
|
||||
DATATRANSFER: {'name': 'Data Transfer API', 'version': 'datatransfer_v1', 'v2discovery': True, 'mappedAPI': 'admin'},
|
||||
DIRECTORY: {'name': 'Directory API', 'version': 'directory_v1', 'v2discovery': True, 'mappedAPI': 'admin'},
|
||||
DOCS: {'name': 'Docs API', 'version': 'v1', 'v2discovery': True},
|
||||
DRIVE2: {'name': 'Drive API v2', 'version': 'v2', 'v2discovery': False, 'mappedAPI': 'drive'},
|
||||
DRIVE3: {'name': 'Drive API v3', 'version': 'v3', 'v2discovery': False, 'mappedAPI': 'drive'},
|
||||
DRIVETD: {'name': 'Drive API v3 - todrive', 'version': 'v3', 'v2discovery': False, 'mappedAPI': 'drive'},
|
||||
DRIVEACTIVITY: {'name': 'Drive Activity API v2', 'version': 'v2', 'v2discovery': True},
|
||||
DRIVELABELS_ADMIN: {'name': 'Drive Labels API - Admin', 'version': 'v2', 'v2discovery': True, 'mappedAPI': DRIVELABELS},
|
||||
DRIVELABELS_USER: {'name': 'Drive Labels API - User', 'version': 'v2', 'v2discovery': True, 'mappedAPI': DRIVELABELS},
|
||||
EMAIL_AUDIT: {'name': 'Email Audit API', 'version': 'v1', 'v2discovery': False},
|
||||
FORMS: {'name': 'Forms API', 'version': 'v1', 'v2discovery': True},
|
||||
GMAIL: {'name': 'Gmail API', 'version': 'v1', 'v2discovery': True},
|
||||
GROUPSMIGRATION: {'name': 'Groups Migration API', 'version': 'v1', 'v2discovery': False},
|
||||
GROUPSSETTINGS: {'name': 'Groups Settings API', 'version': 'v1', 'v2discovery': True},
|
||||
IAM: {'name': 'Identity and Access Management API', 'version': 'v1', 'v2discovery': True},
|
||||
IAM_CREDENTIALS: {'name': 'Identity and Access Management Credentials API', 'version': 'v1', 'v2discovery': True},
|
||||
IAP: {'name': 'Cloud Identity-Aware Proxy API', 'version': 'v1', 'v2discovery': True},
|
||||
KEEP: {'name': 'Keep API', 'version': 'v1', 'v2discovery': True},
|
||||
LICENSING: {'name': 'License Manager API', 'version': 'v1', 'v2discovery': True},
|
||||
LOOKERSTUDIO: {'name': 'Looker Studio API', 'version': 'v1', 'v2discovery': True, 'localjson': True},
|
||||
MEET: {'name': 'Meet API', 'version': 'v2', 'v2discovery': True},
|
||||
OAUTH2: {'name': 'OAuth2 API', 'version': 'v2', 'v2discovery': False},
|
||||
ORGPOLICY: {'name': 'Organization Policy API', 'version': 'v2', 'v2discovery': True},
|
||||
PEOPLE: {'name': 'People API', 'version': 'v1', 'v2discovery': True},
|
||||
PEOPLE_DIRECTORY: {'name': 'People Directory API', 'version': 'v1', 'v2discovery': True, 'mappedAPI': PEOPLE},
|
||||
PEOPLE_OTHERCONTACTS: {'name': 'People API - Other Contacts', 'version': 'v1', 'v2discovery': True, 'mappedAPI': PEOPLE},
|
||||
PRINTERS: {'name': 'Directory API Printers', 'version': 'directory_v1', 'v2discovery': True, 'mappedAPI': 'admin'},
|
||||
PUBSUB: {'name': 'Pub / Sub API', 'version': 'v1', 'v2discovery': True},
|
||||
REPORTS: {'name': 'Reports API', 'version': 'reports_v1', 'v2discovery': True, 'mappedAPI': 'admin'},
|
||||
RESELLER: {'name': 'Reseller API', 'version': 'v1', 'v2discovery': True},
|
||||
SERVICEACCOUNTLOOKUP: {'name': 'Service Account Lookup pseudo-API', 'version': 'v1', 'v2discovery': True, 'localjson': True},
|
||||
SERVICEMANAGEMENT: {'name': 'Service Management API', 'version': 'v1', 'v2discovery': True},
|
||||
SERVICEUSAGE: {'name': 'Service Usage API', 'version': 'v1', 'v2discovery': True},
|
||||
SHEETS: {'name': 'Sheets API', 'version': 'v4', 'v2discovery': True},
|
||||
SHEETSTD: {'name': 'Sheets API - todrive', 'version': 'v4', 'v2discovery': True, 'mappedAPI': SHEETS},
|
||||
SITES: {'name': 'Sites API', 'version': 'v1', 'v2discovery': False},
|
||||
SITEVERIFICATION: {'name': 'Site Verification API', 'version': 'v1', 'v2discovery': True},
|
||||
STORAGE: {'name': 'Cloud Storage API', 'version': 'v1', 'v2discovery': True},
|
||||
STORAGEREAD: {'name': 'Cloud Storage API - Read', 'version': 'v1', 'v2discovery': True, 'mappedAPI': STORAGE},
|
||||
STORAGEWRITE: {'name': 'Cloud Storage API - Write', 'version': 'v1', 'v2discovery': True, 'mappedAPI': STORAGE},
|
||||
TASKS: {'name': 'Tasks API', 'version': 'v1', 'v2discovery': True},
|
||||
VAULT: {'name': 'Vault API', 'version': 'v1', 'v2discovery': True},
|
||||
YOUTUBE: {'name': 'Youtube API', 'version': 'v3', 'v2discovery': True},
|
||||
}
|
||||
|
||||
READONLY = ['readonly',]
|
||||
|
||||
_CLIENT_SCOPES = [
|
||||
{'name': 'Calendar API',
|
||||
'api': CALENDAR,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/calendar'},
|
||||
{'name': 'Chrome Browser Cloud Management API',
|
||||
'api': CBCM,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/admin.directory.device.chromebrowsers'},
|
||||
{'name': 'Chrome Management API - read only',
|
||||
'api': CHROMEMANAGEMENT,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/chrome.management.reports.readonly'},
|
||||
{'name': 'Chrome Management API - AppDetails read only',
|
||||
'api': CHROMEMANAGEMENT_APPDETAILS,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/chrome.management.appdetails.readonly'},
|
||||
{'name': 'Chrome Management API - Profiles',
|
||||
'api': CHROMEMANAGEMENT_CHROMEPROFILES,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/chrome.management.profiles'},
|
||||
{'name': 'Chrome Management API - Telemetry read only',
|
||||
'api': CHROMEMANAGEMENT_TELEMETRY,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/chrome.management.telemetry.readonly'},
|
||||
{'name': 'Chrome Policy API',
|
||||
'api': CHROMEPOLICY,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/chrome.management.policy'},
|
||||
{'name': 'Chrome Printer Management API',
|
||||
'api': PRINTERS,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/admin.chrome.printers'},
|
||||
{'name': 'Chrome Version History API',
|
||||
'api': CHROMEVERSIONHISTORY,
|
||||
'subscopes': [],
|
||||
'scope': ''},
|
||||
{'name': 'Classroom API - Courses',
|
||||
'api': CLASSROOM,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/classroom.courses'},
|
||||
{'name': 'Classroom API - Course Announcements',
|
||||
'api': CLASSROOM,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/classroom.announcements'},
|
||||
{'name': 'Classroom API - Course Topics',
|
||||
'api': CLASSROOM,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/classroom.topics'},
|
||||
{'name': 'Classroom API - Course Work/Materials',
|
||||
'api': CLASSROOM,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/classroom.courseworkmaterials'},
|
||||
{'name': 'Classroom API - Course Work/Submissions',
|
||||
'api': CLASSROOM,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/classroom.coursework.students'},
|
||||
{'name': 'Classroom API - Student Guardians',
|
||||
'api': CLASSROOM,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/classroom.guardianlinks.students'},
|
||||
{'name': 'Classroom API - Profile Emails',
|
||||
'api': CLASSROOM,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/classroom.profile.emails'},
|
||||
{'name': 'Classroom API - Profile Photos',
|
||||
'api': CLASSROOM,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/classroom.profile.photos'},
|
||||
{'name': 'Classroom API - Rosters',
|
||||
'api': CLASSROOM,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/classroom.rosters'},
|
||||
{'name': 'Cloud Channel API',
|
||||
'api': CLOUDCHANNEL,
|
||||
'subscopes': READONLY,
|
||||
'offByDefault': True,
|
||||
'scope': 'https://www.googleapis.com/auth/apps.order'},
|
||||
{'name': 'Cloud Identity Groups API',
|
||||
'api': CLOUDIDENTITY_GROUPS,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/cloud-identity.groups'},
|
||||
{'name': 'Cloud Identity - Inbound SSO Settings',
|
||||
'api': CLOUDIDENTITY_INBOUND_SSO,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/cloud-identity.inboundsso'},
|
||||
{'name': 'Cloud Identity OrgUnits API',
|
||||
'api': CLOUDIDENTITY_ORGUNITS_BETA,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/cloud-identity.orgunits'},
|
||||
{'name': 'Cloud Identity - Policy',
|
||||
'api': CLOUDIDENTITY_POLICY,
|
||||
'subscopes': READONLY,
|
||||
'roByDefault': True,
|
||||
'scope': 'https://www.googleapis.com/auth/cloud-identity.policies'
|
||||
},
|
||||
{'name': 'Cloud Identity User Invitations API',
|
||||
'api': CLOUDIDENTITY_USERINVITATIONS,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/cloud-identity.userinvitations'},
|
||||
{'name': 'Cloud Storage API (Read Only, Vault/Takeout Download, Cloud Storage)',
|
||||
'api': STORAGEREAD,
|
||||
'subscopes': [],
|
||||
'offByDefault': True,
|
||||
'scope': STORAGE_READONLY_SCOPE},
|
||||
{'name': 'Cloud Storage API (Read/Write, Vault/Takeout Copy/Download, Cloud Storage)',
|
||||
'api': STORAGEWRITE,
|
||||
'subscopes': [],
|
||||
'offByDefault': True,
|
||||
'scope': STORAGE_READWRITE_SCOPE},
|
||||
{'name': 'Contacts API - Domain Shared Contacts and GAL',
|
||||
'api': CONTACTS,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.google.com/m8/feeds'},
|
||||
{'name': 'Contact Delegation API',
|
||||
'api': CONTACTDELEGATION,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/admin.contact.delegation'},
|
||||
{'name': 'Data Transfer API',
|
||||
'api': DATATRANSFER,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/admin.datatransfer'},
|
||||
{'name': 'Directory API - Chrome OS Devices',
|
||||
'api': DIRECTORY,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/admin.directory.device.chromeos'},
|
||||
{'name': 'Directory API - Customers',
|
||||
'api': DIRECTORY,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/admin.directory.customer'},
|
||||
{'name': 'Directory API - Domains',
|
||||
'api': DIRECTORY,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/admin.directory.domain'},
|
||||
{'name': 'Directory API - Groups',
|
||||
'api': DIRECTORY,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/admin.directory.group'},
|
||||
{'name': 'Directory API - Mobile Devices Directory',
|
||||
'api': DIRECTORY,
|
||||
'subscopes': ['readonly', 'action'],
|
||||
'scope': 'https://www.googleapis.com/auth/admin.directory.device.mobile'},
|
||||
{'name': 'Directory API - Organizational Units',
|
||||
'api': DIRECTORY,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/admin.directory.orgunit'},
|
||||
{'name': 'Directory API - Resource Calendars',
|
||||
'api': DIRECTORY,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/admin.directory.resource.calendar'},
|
||||
{'name': 'Directory API - Roles',
|
||||
'api': DIRECTORY,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/admin.directory.rolemanagement'},
|
||||
{'name': 'Directory API - User Schemas',
|
||||
'api': DIRECTORY,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/admin.directory.userschema'},
|
||||
{'name': 'Directory API - User Security',
|
||||
'api': DIRECTORY,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/admin.directory.user.security'},
|
||||
{'name': 'Directory API - Users',
|
||||
'api': DIRECTORY,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/admin.directory.user'},
|
||||
{'name': 'Email Audit API',
|
||||
'api': EMAIL_AUDIT,
|
||||
'subscopes': [],
|
||||
'offByDefault': True,
|
||||
'scope': 'https://apps-apis.google.com/a/feeds/compliance/audit/'},
|
||||
{'name': 'Groups Migration API',
|
||||
'api': GROUPSMIGRATION,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/apps.groups.migration'},
|
||||
{'name': 'Groups Settings API',
|
||||
'api': GROUPSSETTINGS,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/apps.groups.settings'},
|
||||
{'name': 'License Manager API',
|
||||
'api': LICENSING,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/apps.licensing'},
|
||||
{'name': 'People Directory API - read only',
|
||||
'api': PEOPLE_DIRECTORY,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/directory.readonly'},
|
||||
{'name': 'People API',
|
||||
'api': PEOPLE,
|
||||
'subscopes': READONLY,
|
||||
'scope': PEOPLE_SCOPE},
|
||||
{'name': 'Pub / Sub API',
|
||||
'api': PUBSUB,
|
||||
'subscopes': [],
|
||||
'offByDefault': True,
|
||||
'scope': 'https://www.googleapis.com/auth/pubsub'},
|
||||
{'name': 'Reports API - Audit Reports',
|
||||
'api': REPORTS,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/admin.reports.audit.readonly'},
|
||||
{'name': 'Reports API - Usage Reports',
|
||||
'api': REPORTS,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/admin.reports.usage.readonly'},
|
||||
{'name': 'Reseller API',
|
||||
'api': RESELLER,
|
||||
'subscopes': [],
|
||||
'offByDefault': True,
|
||||
'scope': 'https://www.googleapis.com/auth/apps.order'},
|
||||
{'name': 'Service Account Lookup pseudo-API',
|
||||
'api': SERVICEACCOUNTLOOKUP,
|
||||
'subscopes': [],
|
||||
'scope': ''},
|
||||
{'name': 'Site Verification API',
|
||||
'api': SITEVERIFICATION,
|
||||
'subscopes': [],
|
||||
'offByDefault': True,
|
||||
'scope': 'https://www.googleapis.com/auth/siteverification'},
|
||||
{'name': 'Sites API',
|
||||
'api': SITES,
|
||||
'subscopes': [],
|
||||
'offByDefault': True,
|
||||
'scope': 'https://sites.google.com/feeds'},
|
||||
{'name': 'Vault API',
|
||||
'api': VAULT,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/ediscovery'},
|
||||
]
|
||||
|
||||
_TODRIVE_CLIENT_SCOPES = [
|
||||
{'name': 'Drive API - todrive_clientaccess',
|
||||
'api': DRIVE3,
|
||||
'subscopes': [],
|
||||
'scope': DRIVE_SCOPE},
|
||||
{'name': 'Drive File API - todrive_clientaccess',
|
||||
'api': DRIVE3,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/drive.file'},
|
||||
{'name': 'Gmail API - todrive_clientaccess',
|
||||
'api': GMAIL,
|
||||
'subscopes': [],
|
||||
'scope': GMAIL_SEND_SCOPE},
|
||||
{'name': 'Sheets API - todrive_clientaccess',
|
||||
'api': SHEETS,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/spreadsheets'},
|
||||
]
|
||||
|
||||
OAUTH2SA_SCOPES = 'us_scopes'
|
||||
|
||||
_SVCACCT_SCOPES = [
|
||||
{'name': 'AlertCenter API',
|
||||
'api': ALERTCENTER,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/apps.alerts'},
|
||||
{'name': 'Analytics API - read only',
|
||||
'api': ANALYTICS,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/analytics.readonly'},
|
||||
{'name': 'Analytics Admin API - read only',
|
||||
'api': ANALYTICS_ADMIN,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/analytics.readonly'},
|
||||
{'name': 'Calendar API',
|
||||
'api': CALENDAR,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/calendar'},
|
||||
{'name': 'Chat API - Memberships',
|
||||
'api': CHAT_MEMBERSHIPS,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/chat.memberships'},
|
||||
{'name': 'Chat API - Memberships Admin',
|
||||
'api': CHAT_MEMBERSHIPS_ADMIN,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/chat.admin.memberships'},
|
||||
{'name': 'Chat API - Messages',
|
||||
'api': CHAT_MESSAGES,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/chat.messages'},
|
||||
{'name': 'Chat API - Spaces',
|
||||
'api': CHAT_SPACES,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/chat.spaces'},
|
||||
{'name': 'Chat API - Spaces Admin',
|
||||
'api': CHAT_SPACES_ADMIN,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/chat.admin.spaces'},
|
||||
{'name': 'Chat API - Spaces Delete',
|
||||
'api': CHAT_SPACES_DELETE,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/chat.delete'},
|
||||
{'name': 'Chat API - Spaces Delete Admin',
|
||||
'api': CHAT_SPACES_DELETE_ADMIN,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/chat.admin.delete'},
|
||||
{'name': 'Classroom API - Course Announcements',
|
||||
'api': CLASSROOM,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/classroom.announcements'},
|
||||
{'name': 'Classroom API - Course Topics',
|
||||
'api': CLASSROOM,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/classroom.topics'},
|
||||
{'name': 'Classroom API - Course Work/Materials',
|
||||
'api': CLASSROOM,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/classroom.courseworkmaterials'},
|
||||
{'name': 'Classroom API - Course Work/Submissions',
|
||||
'api': CLASSROOM,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/classroom.coursework.students'},
|
||||
{'name': 'Classroom API - Profile Emails',
|
||||
'api': CLASSROOM,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/classroom.profile.emails'},
|
||||
{'name': 'Classroom API - Profile Photos',
|
||||
'api': CLASSROOM,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/classroom.profile.photos'},
|
||||
{'name': 'Classroom API - Rosters',
|
||||
'api': CLASSROOM,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/classroom.rosters'},
|
||||
{'name': 'Cloud Identity Devices API',
|
||||
'api': CLOUDIDENTITY_DEVICES,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/cloud-identity'},
|
||||
# {'name': 'Cloud Identity User Invitations API',
|
||||
# 'api': CLOUDIDENTITY_USERINVITATIONS,
|
||||
# 'subscopes': READONLY,
|
||||
# 'scope': 'https://www.googleapis.com/auth/cloud-identity'},
|
||||
# {'name': 'Contacts API - Users',
|
||||
# 'api': CONTACTS,
|
||||
# 'subscopes': [],
|
||||
# 'scope': 'https://www.google.com/m8/feeds'},
|
||||
{'name': 'Drive API',
|
||||
'api': DRIVE3,
|
||||
'subscopes': READONLY,
|
||||
'scope': DRIVE_SCOPE},
|
||||
{'name': 'Drive Activity API v2 - must pair with Drive API',
|
||||
'api': DRIVEACTIVITY,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/drive.activity'},
|
||||
{'name': 'Drive Labels API - Admin',
|
||||
'api': DRIVELABELS_ADMIN,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/drive.admin.labels'},
|
||||
{'name': 'Drive Labels API - User',
|
||||
'api': DRIVELABELS_USER,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/drive.labels'},
|
||||
{'name': 'Docs API',
|
||||
'api': DOCS,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/documents'},
|
||||
{'name': 'Forms API',
|
||||
'api': FORMS,
|
||||
'subscopes': [],
|
||||
'scope': DRIVE_SCOPE},
|
||||
{'name': 'Gmail API - Full Access (Labels, Messages)',
|
||||
'api': GMAIL,
|
||||
'subscopes': [],
|
||||
'scope': 'https://mail.google.com/'},
|
||||
{'name': 'Gmail API - Full Access (Labels, Messages) except delete message',
|
||||
'api': GMAIL,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/gmail.modify'},
|
||||
{'name': 'Gmail API - Basic Settings (Filters,IMAP, Language, POP, Vacation) - read/write, Sharing Settings (Delegates, Forwarding, SendAs) - read',
|
||||
'api': GMAIL,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/gmail.settings.basic'},
|
||||
{'name': 'Gmail API - Sharing Settings (Delegates, Forwarding, SendAs) - write',
|
||||
'api': GMAIL,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/gmail.settings.sharing'},
|
||||
{'name': 'Identity and Access Management API',
|
||||
'api': IAM,
|
||||
'subscopes': [],
|
||||
'scope': CLOUD_PLATFORM_SCOPE},
|
||||
{'name': 'Keep API',
|
||||
'api': KEEP,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/keep'},
|
||||
{'name': 'Looker Studio API',
|
||||
'api': LOOKERSTUDIO,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/datastudio'},
|
||||
{'name': 'Meet API',
|
||||
'api': MEET,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/meetings.space.created',
|
||||
'roscope': 'https://www.googleapis.com/auth/meetings.space.readonly'},
|
||||
{'name': 'OAuth2 API',
|
||||
'api': OAUTH2,
|
||||
'subscopes': [],
|
||||
'scope': USERINFO_PROFILE_SCOPE},
|
||||
{'name': 'People API',
|
||||
'api': PEOPLE,
|
||||
'subscopes': READONLY,
|
||||
'scope': PEOPLE_SCOPE},
|
||||
{'name': 'People Directory API - read only',
|
||||
'api': PEOPLE_DIRECTORY,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/directory.readonly'},
|
||||
{'name': 'People API - Other Contacts - read only',
|
||||
'api': PEOPLE_OTHERCONTACTS,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/contacts.other.readonly'},
|
||||
{'name': 'Sheets API',
|
||||
'api': SHEETS,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/spreadsheets'},
|
||||
{'name': 'Sites API',
|
||||
'api': SITES,
|
||||
'subscopes': [],
|
||||
'scope': 'https://sites.google.com/feeds'},
|
||||
{'name': 'Tasks API',
|
||||
'api': TASKS,
|
||||
'subscopes': READONLY,
|
||||
'scope': 'https://www.googleapis.com/auth/tasks'},
|
||||
{'name': 'Youtube API - read only',
|
||||
'api': YOUTUBE,
|
||||
'subscopes': [],
|
||||
'offByDefault': True,
|
||||
'scope': 'https://www.googleapis.com/auth/youtube.readonly'},
|
||||
]
|
||||
|
||||
_SVCACCT_SPECIAL_SCOPES = [
|
||||
{'name': 'Drive API - todrive',
|
||||
'api': DRIVETD,
|
||||
'subscopes': [],
|
||||
'scope': DRIVE_SCOPE},
|
||||
{'name': 'Gmail API - Full Access - read only',
|
||||
'api': GMAIL,
|
||||
'subscopes': [],
|
||||
'offByDefault': True,
|
||||
'scope': 'https://www.googleapis.com/auth/gmail.readonly'},
|
||||
{'name': 'Gmail API - Send Messages - including todrive',
|
||||
'api': GMAIL,
|
||||
'subscopes': [],
|
||||
'offByDefault': True,
|
||||
'scope': GMAIL_SEND_SCOPE},
|
||||
{'name': 'Sheets API - todrive',
|
||||
'api': SHEETSTD,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/spreadsheets'},
|
||||
]
|
||||
|
||||
_USER_SVCACCT_ONLY_SCOPES = [
|
||||
{'name': 'Groups Migration API',
|
||||
'api': GROUPSMIGRATION,
|
||||
'subscopes': [],
|
||||
'scope': 'https://www.googleapis.com/auth/apps.groups.migration'},
|
||||
]
|
||||
|
||||
DRIVE3_TO_DRIVE2_ABOUT_FIELDS_MAP = {
|
||||
'displayName': 'name',
|
||||
'limit': 'quotaBytesTotal',
|
||||
'usage': 'quotaBytesUsedAggregate',
|
||||
'usageInDrive': 'quotaBytesUsed',
|
||||
'usageInDriveTrash': 'quotaBytesUsedInTrash',
|
||||
}
|
||||
|
||||
DRIVE3_TO_DRIVE2_CAPABILITIES_FIELDS_MAP = {
|
||||
'canComment': 'canComment',
|
||||
'canReadRevisions': 'canReadRevisions',
|
||||
'canCopy': 'copyable',
|
||||
'canEdit': 'editable',
|
||||
'canShare': 'shareable',
|
||||
}
|
||||
|
||||
DRIVE3_TO_DRIVE2_CAPABILITIES_NAMES_MAP = {
|
||||
'canChangeViewersCanCopyContent': 'canChangeRestrictedDownload',
|
||||
}
|
||||
|
||||
DRIVE3_TO_DRIVE2_FILES_FIELDS_MAP = {
|
||||
'allowFileDiscovery': 'withLink',
|
||||
'createdTime': 'createdDate',
|
||||
'expirationTime': 'expirationDate',
|
||||
'modifiedByMe': 'modified',
|
||||
'modifiedByMeTime': 'modifiedByMeDate',
|
||||
'modifiedTime': 'modifiedDate',
|
||||
'name': 'title',
|
||||
'restrictionTime': 'restrictionDate',
|
||||
'sharedWithMeTime': 'sharedWithMeDate',
|
||||
'size': 'fileSize',
|
||||
'trashedTime': 'trashedDate',
|
||||
'viewedByMe': 'viewed',
|
||||
'viewedByMeTime': 'lastViewedByMeDate',
|
||||
'webViewLink': 'alternateLink',
|
||||
}
|
||||
|
||||
DRIVE3_TO_DRIVE2_LABELS_MAP = {
|
||||
'modifiedByMe': 'modified',
|
||||
'starred': 'starred',
|
||||
'trashed': 'trashed',
|
||||
'viewedByMe': 'viewed',
|
||||
}
|
||||
|
||||
DRIVE3_TO_DRIVE2_REVISIONS_FIELDS_MAP = {
|
||||
'modifiedTime': 'modifiedDate',
|
||||
'keepForever': 'pinned',
|
||||
'size': 'fileSize',
|
||||
}
|
||||
|
||||
def getAPIName(api):
|
||||
return _INFO[api]['name']
|
||||
|
||||
def getVersion(api):
|
||||
version = _INFO[api]['version']
|
||||
v2discovery = _INFO[api]['v2discovery']
|
||||
api = _INFO[api].get('mappedAPI', api)
|
||||
return (api, version, v2discovery)
|
||||
|
||||
def getClientScopesSet(api):
|
||||
return {scope['scope'] for scope in _CLIENT_SCOPES if scope['api'] == api}
|
||||
|
||||
def getClientScopesList(todriveClientAccess):
|
||||
caScopes = _CLIENT_SCOPES[:]
|
||||
if todriveClientAccess:
|
||||
caScopes.extend(_TODRIVE_CLIENT_SCOPES)
|
||||
return sorted(caScopes, key=lambda k: k['name'])
|
||||
|
||||
def getClientScopesURLs(todriveClientAccess):
|
||||
caScopes = _CLIENT_SCOPES[:]
|
||||
if todriveClientAccess:
|
||||
caScopes.extend(_TODRIVE_CLIENT_SCOPES)
|
||||
return sorted({scope['scope'] for scope in _CLIENT_SCOPES})
|
||||
|
||||
def getSvcAcctScopeAPI(uscope):
|
||||
for scope in _SVCACCT_SCOPES:
|
||||
if uscope == scope['scope'] or (uscope.endswith('.readonly') and 'readonly' in scope['subscopes']):
|
||||
return scope['api']
|
||||
return None
|
||||
|
||||
def getSvcAcctScopes(userServiceAccountAccessOnly, svcAcctSpecialScopes):
|
||||
saScopes = [scope['scope'] for scope in _SVCACCT_SCOPES]
|
||||
if userServiceAccountAccessOnly:
|
||||
saScopes.extend([scope['scope'] for scope in _USER_SVCACCT_ONLY_SCOPES])
|
||||
if svcAcctSpecialScopes:
|
||||
saScopes.extend([scope['scope'] for scope in _SVCACCT_SPECIAL_SCOPES])
|
||||
return saScopes
|
||||
|
||||
def getSvcAcctScopesList(userServiceAccountAccessOnly, svcAcctSpecialScopes):
|
||||
saScopes = _SVCACCT_SCOPES[:]
|
||||
if userServiceAccountAccessOnly:
|
||||
saScopes.extend(_USER_SVCACCT_ONLY_SCOPES)
|
||||
if svcAcctSpecialScopes:
|
||||
saScopes.extend(_SVCACCT_SPECIAL_SCOPES)
|
||||
return sorted(saScopes, key=lambda k: k['name'])
|
||||
|
||||
def hasLocalJSON(api):
|
||||
return _INFO[api].get('localjson', False)
|
||||
616
src/gam/gamlib/glcfg.py
Normal file
616
src/gam/gamlib/glcfg.py
Normal file
@@ -0,0 +1,616 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2024 Ross Scroggs All Rights Reserved.
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""GAM gam.cfg variables
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
TRUE = 'true'
|
||||
FALSE = 'false'
|
||||
DEFAULT_CHARSET = 'utf-8'
|
||||
MY_CUSTOMER = 'my_customer'
|
||||
NEVER = 'Never'
|
||||
TLS_CHOICE_MAP = {
|
||||
'': '',
|
||||
'tlsv1_2': 'TLSv1_2', 'tlsv1.2': 'TLSv1_2',
|
||||
'tlsv1_3': 'TLSv1_3', 'tlsv1.3': 'TLSv1_3',
|
||||
}
|
||||
|
||||
FN_CACERTS_PEM = 'cacerts.pem'
|
||||
FN_CLIENT_SECRETS_JSON = 'client_secrets.json'
|
||||
FN_EXTRA_ARGS_TXT = 'extra-args.txt'
|
||||
FN_OAUTH2_TXT = 'oauth2.txt'
|
||||
FN_OAUTH2SERVICE_JSON = 'oauth2service.json'
|
||||
|
||||
# Global variables defined in gam.cfg
|
||||
|
||||
# The following XXX constants are the names of the items in gam.cfg
|
||||
# When retrieving lists of Google Drive activities from API, how many should be retrieved in each chunk
|
||||
ACTIVITY_MAX_RESULTS = 'activity_max_results'
|
||||
# Admin email address, required when enable_dasa is true, overrides oauth2.txt value otherwise
|
||||
ADMIN_EMAIL = 'admin_email'
|
||||
# Check if API calls rate exceeds limit
|
||||
API_CALLS_RATE_CHECK = 'api_calls_rate_check'
|
||||
# API calls per 100 seconds limit
|
||||
API_CALLS_RATE_LIMIT = 'api_calls_rate_limit'
|
||||
# API calls tries limit
|
||||
API_CALLS_TRIES_LIMIT = 'api_calls_tries_limit'
|
||||
# Automatically generate gam batch command if number of users specified in gam users xxx command exceeds this number
|
||||
# Default: 0, do not automatically generate gam batch commands
|
||||
AUTO_BATCH_MIN = 'auto_batch_min'
|
||||
# When bailing on internal errors, how many total tries should be performed
|
||||
BAIL_ON_INTERNAL_ERROR_TRIES = 'bail_on_internal_error_tries'
|
||||
# When processing items in batches, how many should be processed in each batch
|
||||
BATCH_SIZE = 'batch_size'
|
||||
# Location of cacerts.pem for API calls
|
||||
CACERTS_PEM = 'cacerts_pem'
|
||||
# GAM cache directory
|
||||
CACHE_DIR = 'cache_dir'
|
||||
# GAM cache discovery only. If no_cache is False, only API discovery calls will be cached
|
||||
CACHE_DISCOVERY_ONLY = 'cache_discovery_only'
|
||||
# Channel custmerId from gam.cfg
|
||||
CHANNEL_CUSTOMER_ID = 'channel_customer_id'
|
||||
# Character set of batch, csv, data files
|
||||
CHARSET = 'charset'
|
||||
# When retrieving lists of Google Classroom items from API, how many should be retrieved in each chunk
|
||||
CLASSROOM_MAX_RESULTS = 'classroom_max_results'
|
||||
# Path to client_secrets.json
|
||||
CLIENT_SECRETS_JSON = 'client_secrets_json'
|
||||
# Allowed clock skew in seconds
|
||||
CLOCK_SKEW_IN_SECONDS = 'clock_skew_in_seconds'
|
||||
# Command logging filename
|
||||
CMDLOG = 'cmdlog'
|
||||
# Bogus Command logging maximum number of backup log files
|
||||
CMDLOG_MAX__BACKUPS = 'cmdlog_max__backups'
|
||||
# Command logging maximum number of backup log files
|
||||
CMDLOG_MAX_BACKUPS = 'cmdlog_max_backups'
|
||||
# Command logging max kilo bytes per log file
|
||||
CMDLOG_MAX_KILO_BYTES = 'cmdlog_max_kilo_bytes'
|
||||
# GAM config directory containing client_secrets.json, oauth2.txt, oauth2service.json, extra_args.txt
|
||||
CONFIG_DIR = 'config_dir'
|
||||
# When retrieving lists of Google Contacts from API, how many should be retrieved in each chunk
|
||||
CONTACT_MAX_RESULTS = 'contact_max_results'
|
||||
# Column delimiter in CSV input file
|
||||
CSV_INPUT_COLUMN_DELIMITER = 'csv_input_column_delimiter'
|
||||
# No escape character in CSV input file
|
||||
CSV_INPUT_NO_ESCAPE_CHAR = 'csv_input_no_escape_char'
|
||||
# Quote character in CSV input file
|
||||
CSV_INPUT_QUOTE_CHAR = 'csv_input_quote_char'
|
||||
# Filter for input column values
|
||||
CSV_INPUT_ROW_FILTER = 'csv_input_row_filter'
|
||||
# Mode (and|or) for input column values
|
||||
CSV_INPUT_ROW_FILTER_MODE = 'csv_input_row_filter_mode'
|
||||
# Filter for input column drop values
|
||||
CSV_INPUT_ROW_DROP_FILTER = 'csv_input_row_drop_filter'
|
||||
# Mode (and|or) for input column drop values
|
||||
CSV_INPUT_ROW_DROP_FILTER_MODE = 'csv_input_row_drop_filter_mode'
|
||||
# Limit number of input rows
|
||||
CSV_INPUT_ROW_LIMIT = 'csv_input_row_limit'
|
||||
# Convert newlines in text fields to "\n" in CSV output file
|
||||
CSV_OUTPUT_CONVERT_CR_NL = 'csv_output_convert_cr_nl'
|
||||
# Column delimiter in CSV output file
|
||||
CSV_OUTPUT_COLUMN_DELIMITER = 'csv_output_column_delimiter'
|
||||
# No escape character in CSV output file
|
||||
CSV_OUTPUT_NO_ESCAPE_CHAR = 'csv_output_no_escape_char'
|
||||
# Field list delimiter in CSV output file
|
||||
CSV_OUTPUT_FIELD_DELIMITER = 'csv_output_field_delimiter'
|
||||
# Filter for output column headers
|
||||
CSV_OUTPUT_HEADER_FILTER = 'csv_output_header_filter'
|
||||
# Filter for output column headers to drop
|
||||
CSV_OUTPUT_HEADER_DROP_FILTER = 'csv_output_header_drop_filter'
|
||||
# Force output column headers
|
||||
CSV_OUTPUT_HEADER_FORCE = 'csv_output_header_force'
|
||||
# Orde output column headers
|
||||
CSV_OUTPUT_HEADER_ORDER = 'csv_output_header_order'
|
||||
# Line terminator in CSV output file
|
||||
CSV_OUTPUT_LINE_TERMINATOR = 'csv_output_line_terminator'
|
||||
# Quote character in CSV output file
|
||||
CSV_OUTPUT_QUOTE_CHAR = 'csv_output_quote_char'
|
||||
# Filter for output column values
|
||||
CSV_OUTPUT_ROW_FILTER = 'csv_output_row_filter'
|
||||
# Mode (and|or) for output column values
|
||||
CSV_OUTPUT_ROW_FILTER_MODE = 'csv_output_row_filter_mode'
|
||||
# Filter for output column drop values
|
||||
CSV_OUTPUT_ROW_DROP_FILTER = 'csv_output_row_drop_filter'
|
||||
# Mode (and|or) for output column drop values
|
||||
CSV_OUTPUT_ROW_DROP_FILTER_MODE = 'csv_output_row_drop_filter_mode'
|
||||
# Limit number of output rows
|
||||
CSV_OUTPUT_ROW_LIMIT = 'csv_output_row_limit'
|
||||
# Output sort headers
|
||||
CSV_OUTPUT_SORT_HEADERS = 'csv_output_sort_headers'
|
||||
# Column header subfield name delimiter in CSV output file
|
||||
CSV_OUTPUT_SUBFIELD_DELIMITER = 'csv_output_subfield_delimiter'
|
||||
# Add timestamp column to CSV output file
|
||||
CSV_OUTPUT_TIMESTAMP_COLUMN = 'csv_output_timestamp_column'
|
||||
# Output rows for users even if they do not have the print object (delegate, filters, ...)
|
||||
CSV_OUTPUT_USERS_AUDIT = 'csv_output_users_audit'
|
||||
# custmerId from gam.cfg or retrieved from Google
|
||||
CUSTOMER_ID = 'customer_id'
|
||||
# If debug_level > 0: extra_args['prettyPrint'] = True, httplib2.debuglevel = gam_debug_level, appsObj.debug = True
|
||||
DEBUG_LEVEL = 'debug_level'
|
||||
# When retrieving lists of ChromeOS devices from API, how many should be retrieved in each chunk
|
||||
DEVICE_MAX_RESULTS = 'device_max_results'
|
||||
# Domain obtained from gam.cfg or oauth2.txt
|
||||
DOMAIN = 'domain'
|
||||
# Google Drive download directory
|
||||
DRIVE_DIR = 'drive_dir'
|
||||
# When retrieving lists of Drive files/folders from API, how many should be retrieved in each chunk
|
||||
DRIVE_MAX_RESULTS = 'drive_max_results'
|
||||
# Use Drive V3 beta
|
||||
DRIVE_V3_BETA = 'drive_v3_beta'
|
||||
# Use Drive V3 ntive names
|
||||
DRIVE_V3_NATIVE_NAMES = 'drive_v3_native_names'
|
||||
# When processing email messages in batches, how many should be processed in each batch
|
||||
EMAIL_BATCH_SIZE = 'email_batch_size'
|
||||
# Enable Delegated Admin Service Account
|
||||
ENABLE_DASA = 'enable_dasa'
|
||||
# Enable Cloud Session Reauthentication by borrowing a RAPT token from gcloud command
|
||||
ENABLE_GCLOUD_REAUTH = 'enable_gcloud_reauth'
|
||||
# When retrieving lists of calendar events from API, how many should be retrieved in each chunk
|
||||
EVENT_MAX_RESULTS = 'event_max_results'
|
||||
# Path to extra_args.txt
|
||||
EXTRA_ARGS = 'extra_args'
|
||||
# Gmail CSE certificates directory
|
||||
GMAIL_CSE_INCERT_DIR = 'gmail_cse_incert_dir'
|
||||
# Gmail CSE KACL wrapped key files
|
||||
GMAIL_CSE_INKEY_DIR = 'gmail_cse_inkey_dir'
|
||||
# When processing items in batches, how many seconds should GAM wait between batches
|
||||
INTER_BATCH_WAIT = 'inter_batch_wait'
|
||||
# When retrieving lists of licenses from API, how many should be retrieved in each chunk
|
||||
LICENSE_MAX_RESULTS = 'license_max_results'
|
||||
# License SKUs to process
|
||||
LICENSE_SKUS = 'license_skus'
|
||||
# When retrieving lists of Google Group members from API, how many should be retrieved in each chunk
|
||||
MEMBER_MAX_RESULTS = 'member_max_results'
|
||||
# When deleting or modifying Gmail messages, how many should be processed in each batch
|
||||
MESSAGE_BATCH_SIZE = 'message_batch_size'
|
||||
# When retrieving lists of Gmail messages from API, how many should be retrieved in each chunk
|
||||
MESSAGE_MAX_RESULTS = 'message_max_results'
|
||||
# When retrieving lists of Mobile devices from API, how many should be retrieved in each chunk
|
||||
MOBILE_MAX_RESULTS = 'mobile_max_results'
|
||||
# Number of parallel multiprocess pool.apply_async calls; -1: no limit, 0: NUM_THREADS, >0: specific limit
|
||||
MULTIPROCESS_POOL_LIMIT = 'multiprocess_pool_limit'
|
||||
# Value to substitute for NEVER_TIME
|
||||
NEVER_TIME = 'never_time'
|
||||
# If no_browser is False, writeCSVfile won't open a browser when todrive is set
|
||||
# and doOAuthRequest prints a link and waits for the verification code when oauth2.txt is being created
|
||||
NO_BROWSER = 'no_browser'
|
||||
# Disable GAM API caching
|
||||
NO_CACHE = 'no_cache'
|
||||
# Do noit use URL shortner for authentication URLs
|
||||
NO_SHORT_URLS = 'no_short_urls'
|
||||
# Disable GAM update check
|
||||
NO_UPDATE_CHECK = 'no_update_check'
|
||||
# Disable SSL certificate validation
|
||||
NO_VERIFY_SSL = 'no_verify_ssl'
|
||||
# Number of threads for gam tbatch
|
||||
NUM_TBATCH_THREADS = 'num_tbatch_threads'
|
||||
# Number of threads for gam batch/csv
|
||||
NUM_THREADS = 'num_threads'
|
||||
# Path to oauth2.txt
|
||||
OAUTH2_TXT = 'oauth2_txt'
|
||||
# Path to oauth2service.json
|
||||
OAUTH2SERVICE_JSON = 'oauth2service_json'
|
||||
# Output date format, empty defalts to ISOFormat
|
||||
OUTPUT_DATEFORMAT = 'output_dateformat'
|
||||
# Output time format, empty defalts to ISOFormat
|
||||
OUTPUT_TIMEFORMAT = 'output_timeformat'
|
||||
# When retrieving lists of people from API, how many should be retrieved in each chunk
|
||||
PEOPLE_MAX_RESULTS = 'people_max_results'
|
||||
# Domains for print alises|groups|users
|
||||
PRINT_AGU_DOMAINS = 'print_agu_domains'
|
||||
# OrgUnits for print cros
|
||||
PRINT_CROS_OUS = 'print_cros_ous'
|
||||
# OrgUnits and children for print cros
|
||||
PRINT_CROS_OUS_AND_CHILDREN = 'print_cros_ous_and_children'
|
||||
# Number of seconds to wait for batch/csv processes to complete
|
||||
PROCESS_WAIT_LIMIT = 'process_wait_limit'
|
||||
# Use quick method to move Chromebooks to OU
|
||||
QUICK_CROS_MOVE = 'quick_cros_move'
|
||||
# Quick info user: nogroups nolicenses noschemas
|
||||
QUICK_INFO_USER = 'quick_info_user'
|
||||
# resellerId from gam.cfg or retrieved from Google
|
||||
RESELLER_ID = 'reseller_id'
|
||||
# Retry service not available errors on API calls
|
||||
RETRY_API_SERVICE_NOT_AVAILABLE = 'retry_api_service_not_available'
|
||||
# Default section to use for processing
|
||||
SECTION = 'section'
|
||||
# Show API calls retry data
|
||||
SHOW_API_CALLS_RETRY_DATA = 'show_api_calls_retry_data'
|
||||
# Show commands when processing batch/csv/loop
|
||||
SHOW_COMMANDS = 'show_commands'
|
||||
# Convert newlines in text fields to "\n" in show commands
|
||||
SHOW_CONVERT_CR_NL = 'show_convert_cr_nl'
|
||||
# Add (n/m) to end of messages if number of items to be processed exceeds this number
|
||||
SHOW_COUNTS_MIN = 'show_counts_min'
|
||||
# Enable/disable "Getting ... " messages
|
||||
SHOW_GETTINGS = 'show_gettings'
|
||||
# Enable/disable NL at end of "Got ..." messages
|
||||
SHOW_GETTINGS_GOT_NL = 'show_gettings_got_nl'
|
||||
# Enable/disable showing multiprocess info in redirected stdout/stderr
|
||||
SHOW_MULTIPROCESS_INFO = 'show_multiprocess_info'
|
||||
# SMTP fqdn
|
||||
SMTP_FQDN = 'smtp_fqdn'
|
||||
# SMTP host
|
||||
SMTP_HOST = 'smtp_host'
|
||||
# SMTP username
|
||||
SMTP_USERNAME = 'smtp_username'
|
||||
# SMTP password
|
||||
SMTP_PASSWORD = 'smtp_password'
|
||||
## Minimum TLS Version required for HTTPS connections
|
||||
TLS_MIN_VERSION = 'tls_min_version'
|
||||
## Maximum TLS Version used for HTTPS connections
|
||||
TLS_MAX_VERSION = 'tls_max_version'
|
||||
# Time Zone
|
||||
TIMEZONE = 'timezone'
|
||||
# Clear basic filter when updating an existing sheet
|
||||
TODRIVE_CLEARFILTER = 'todrive_clearfilter'
|
||||
# Use client access for todrive
|
||||
TODRIVE_CLIENTACCESS = 'todrive_clientaccess'
|
||||
# Enable conversion to Google Sheets when uploading todrive files
|
||||
TODRIVE_CONVERSION = 'todrive_conversion'
|
||||
# Save local copy of CSV file
|
||||
TODRIVE_LOCALCOPY = 'todrive_localcopy'
|
||||
# Specify locale for Google Sheets
|
||||
TODRIVE_LOCALE = 'todrive_locale'
|
||||
# Suppress opening browser on todrive upload
|
||||
TODRIVE_NOBROWSER = 'todrive_nobrowser'
|
||||
# Suppress sending email on todrive upload
|
||||
TODRIVE_NOEMAIL = 'todrive_noemail'
|
||||
# No escape character in CSV output file
|
||||
TODRIVE_NO_ESCAPE_CHAR = 'todrive_no_escape_char'
|
||||
# ID/Name of parent folder for todrive files
|
||||
TODRIVE_PARENT = 'todrive_parent'
|
||||
# Append timestamp to todrive sheet name
|
||||
TODRIVE_SHEET_TIMESTAMP = 'todrive_sheet_timestamp'
|
||||
# Sheet timestamp format, empty defalts to ISOFormat
|
||||
TODRIVE_SHEET_TIMEFORMAT = 'todrive_sheet_timeformat'
|
||||
# Append timestamp to todrive file name
|
||||
TODRIVE_TIMESTAMP = 'todrive_timestamp'
|
||||
# Timestamp format, empty defalts to ISOFormat
|
||||
TODRIVE_TIMEFORMAT = 'todrive_timeformat'
|
||||
# Specify timezone for Google Sheets
|
||||
TODRIVE_TIMEZONE = 'todrive_timezone'
|
||||
# Upload data files with no data
|
||||
TODRIVE_UPLOAD_NODATA = 'todrive_upload_nodata'
|
||||
# User for todrive files
|
||||
TODRIVE_USER = 'todrive_user'
|
||||
# Truncate Client ID
|
||||
TRUNCATE_CLIENT_ID = 'truncate_client_id'
|
||||
# Update CrOS org unit with orgUnitId
|
||||
UPDATE_CROS_OU_WITH_ID = 'update_cros_ou_with_id'
|
||||
# Use admin access for chat where possible
|
||||
USE_CHAT_ADMIN_ACCESS = 'use_chat_admin_access'
|
||||
# Use course owner for course access
|
||||
USE_COURSE_OWNER_ACCESS = 'use_course_owner_access'
|
||||
# Use Project ID as Project Name and App Name
|
||||
USE_PROJECTID_AS_NAME = 'use_projectid_as_name'
|
||||
# When retrieving lists of Users from API, how many should be retrieved in each chunk
|
||||
USER_MAX_RESULTS = 'user_max_results'
|
||||
# User service account access only, no client access
|
||||
USER_SERVICE_ACCOUNT_ACCESS_ONLY = 'user_service_account_access_only'
|
||||
|
||||
CSV_INPUT_ROW_FILTER_ITEMS = {CSV_INPUT_ROW_FILTER, CSV_INPUT_ROW_FILTER_MODE,
|
||||
CSV_INPUT_ROW_DROP_FILTER, CSV_INPUT_ROW_DROP_FILTER_MODE,
|
||||
CSV_INPUT_ROW_LIMIT}
|
||||
|
||||
CSV_OUTPUT_ROW_FILTER_ITEMS = {CSV_OUTPUT_HEADER_FILTER, CSV_OUTPUT_HEADER_DROP_FILTER,
|
||||
CSV_OUTPUT_HEADER_FORCE, CSV_OUTPUT_HEADER_ORDER,
|
||||
CSV_OUTPUT_ROW_FILTER, CSV_OUTPUT_ROW_FILTER_MODE,
|
||||
CSV_OUTPUT_ROW_DROP_FILTER, CSV_OUTPUT_ROW_DROP_FILTER_MODE,
|
||||
CSV_OUTPUT_ROW_LIMIT}
|
||||
|
||||
Defaults = {
|
||||
ACTIVITY_MAX_RESULTS: '100',
|
||||
ADMIN_EMAIL: '',
|
||||
API_CALLS_RATE_CHECK: FALSE,
|
||||
API_CALLS_RATE_LIMIT: '100',
|
||||
API_CALLS_TRIES_LIMIT: '10',
|
||||
AUTO_BATCH_MIN: '0',
|
||||
BAIL_ON_INTERNAL_ERROR_TRIES: '2',
|
||||
BATCH_SIZE: '50',
|
||||
CACERTS_PEM: '',
|
||||
CACHE_DIR: '',
|
||||
CACHE_DISCOVERY_ONLY: TRUE,
|
||||
CHARSET: DEFAULT_CHARSET,
|
||||
CHANNEL_CUSTOMER_ID: '',
|
||||
CLASSROOM_MAX_RESULTS: '0',
|
||||
CLIENT_SECRETS_JSON: FN_CLIENT_SECRETS_JSON,
|
||||
CLOCK_SKEW_IN_SECONDS: '10',
|
||||
CMDLOG: '',
|
||||
CMDLOG_MAX_BACKUPS: 5,
|
||||
CMDLOG_MAX_KILO_BYTES: 1000,
|
||||
CONFIG_DIR: '',
|
||||
CONTACT_MAX_RESULTS: '100',
|
||||
CSV_INPUT_COLUMN_DELIMITER: ',',
|
||||
CSV_INPUT_NO_ESCAPE_CHAR: TRUE,
|
||||
CSV_INPUT_QUOTE_CHAR: '\'"\'',
|
||||
CSV_INPUT_ROW_FILTER: '',
|
||||
CSV_INPUT_ROW_FILTER_MODE: 'allmatch',
|
||||
CSV_INPUT_ROW_DROP_FILTER: '',
|
||||
CSV_INPUT_ROW_DROP_FILTER_MODE: 'anymatch',
|
||||
CSV_INPUT_ROW_LIMIT: '0',
|
||||
CSV_OUTPUT_COLUMN_DELIMITER: ',',
|
||||
CSV_OUTPUT_CONVERT_CR_NL: FALSE,
|
||||
CSV_OUTPUT_NO_ESCAPE_CHAR: FALSE,
|
||||
CSV_OUTPUT_FIELD_DELIMITER: "' '",
|
||||
CSV_OUTPUT_HEADER_FILTER: '',
|
||||
CSV_OUTPUT_HEADER_DROP_FILTER: '',
|
||||
CSV_OUTPUT_HEADER_FORCE: '',
|
||||
CSV_OUTPUT_HEADER_ORDER: '',
|
||||
CSV_OUTPUT_LINE_TERMINATOR: 'lf',
|
||||
CSV_OUTPUT_QUOTE_CHAR: '\'"\'',
|
||||
CSV_OUTPUT_ROW_FILTER: '',
|
||||
CSV_OUTPUT_ROW_FILTER_MODE: 'allmatch',
|
||||
CSV_OUTPUT_ROW_DROP_FILTER: '',
|
||||
CSV_OUTPUT_ROW_DROP_FILTER_MODE: 'anymatch',
|
||||
CSV_OUTPUT_ROW_LIMIT: '0',
|
||||
CSV_OUTPUT_SORT_HEADERS: '',
|
||||
CSV_OUTPUT_SUBFIELD_DELIMITER: '.',
|
||||
CSV_OUTPUT_TIMESTAMP_COLUMN: '',
|
||||
CSV_OUTPUT_USERS_AUDIT: FALSE,
|
||||
CUSTOMER_ID: MY_CUSTOMER,
|
||||
DEBUG_LEVEL: '0',
|
||||
DEVICE_MAX_RESULTS: '200',
|
||||
DOMAIN: '',
|
||||
DRIVE_DIR: '',
|
||||
DRIVE_MAX_RESULTS: '1000',
|
||||
DRIVE_V3_BETA: FALSE,
|
||||
DRIVE_V3_NATIVE_NAMES: TRUE,
|
||||
EMAIL_BATCH_SIZE: '50',
|
||||
ENABLE_DASA: FALSE,
|
||||
ENABLE_GCLOUD_REAUTH: FALSE,
|
||||
EVENT_MAX_RESULTS: '250',
|
||||
EXTRA_ARGS: '',
|
||||
GMAIL_CSE_INCERT_DIR: '',
|
||||
GMAIL_CSE_INKEY_DIR: '',
|
||||
INTER_BATCH_WAIT: '0',
|
||||
LICENSE_MAX_RESULTS: '100',
|
||||
LICENSE_SKUS: '',
|
||||
MEMBER_MAX_RESULTS: '200',
|
||||
MESSAGE_BATCH_SIZE: '50',
|
||||
MESSAGE_MAX_RESULTS: '500',
|
||||
MOBILE_MAX_RESULTS: '100',
|
||||
MULTIPROCESS_POOL_LIMIT: '0',
|
||||
NEVER_TIME: NEVER,
|
||||
NO_BROWSER: FALSE,
|
||||
NO_CACHE: FALSE,
|
||||
NO_SHORT_URLS: TRUE,
|
||||
NO_UPDATE_CHECK: TRUE,
|
||||
NO_VERIFY_SSL: FALSE,
|
||||
NUM_TBATCH_THREADS: '2',
|
||||
NUM_THREADS: '5',
|
||||
OAUTH2_TXT: FN_OAUTH2_TXT,
|
||||
OAUTH2SERVICE_JSON: FN_OAUTH2SERVICE_JSON,
|
||||
OUTPUT_DATEFORMAT: '',
|
||||
OUTPUT_TIMEFORMAT: '',
|
||||
PEOPLE_MAX_RESULTS: '100',
|
||||
PRINT_AGU_DOMAINS: '',
|
||||
PRINT_CROS_OUS: '',
|
||||
PRINT_CROS_OUS_AND_CHILDREN: '',
|
||||
PROCESS_WAIT_LIMIT: '0',
|
||||
QUICK_CROS_MOVE: FALSE,
|
||||
QUICK_INFO_USER: FALSE,
|
||||
RESELLER_ID: '',
|
||||
RETRY_API_SERVICE_NOT_AVAILABLE: FALSE,
|
||||
SECTION: '',
|
||||
SHOW_API_CALLS_RETRY_DATA: FALSE,
|
||||
SHOW_COMMANDS: FALSE,
|
||||
SHOW_CONVERT_CR_NL: FALSE,
|
||||
SHOW_COUNTS_MIN: '1',
|
||||
SHOW_GETTINGS: TRUE,
|
||||
SHOW_GETTINGS_GOT_NL: FALSE,
|
||||
SHOW_MULTIPROCESS_INFO: FALSE,
|
||||
SMTP_FQDN: '',
|
||||
SMTP_HOST: '',
|
||||
SMTP_USERNAME: '',
|
||||
SMTP_PASSWORD: '',
|
||||
TLS_MIN_VERSION: 'TLSv1_3',
|
||||
TLS_MAX_VERSION: '',
|
||||
TIMEZONE: 'utc',
|
||||
TODRIVE_CLEARFILTER: FALSE,
|
||||
TODRIVE_CLIENTACCESS: FALSE,
|
||||
TODRIVE_CONVERSION: TRUE,
|
||||
TODRIVE_LOCALCOPY: FALSE,
|
||||
TODRIVE_LOCALE: '',
|
||||
TODRIVE_NOBROWSER: '',
|
||||
TODRIVE_NOEMAIL: '',
|
||||
TODRIVE_NO_ESCAPE_CHAR: TRUE,
|
||||
TODRIVE_PARENT: 'root',
|
||||
TODRIVE_SHEET_TIMESTAMP: 'copy', # copy from TODRIVE_TIMESTAMP
|
||||
TODRIVE_SHEET_TIMEFORMAT: 'copy', # copy from TODRIVE_TIMEFORMAT
|
||||
TODRIVE_TIMESTAMP: FALSE,
|
||||
TODRIVE_TIMEFORMAT: '',
|
||||
TODRIVE_TIMEZONE: '',
|
||||
TODRIVE_UPLOAD_NODATA: TRUE,
|
||||
TODRIVE_USER: '',
|
||||
TRUNCATE_CLIENT_ID: FALSE,
|
||||
UPDATE_CROS_OU_WITH_ID: FALSE,
|
||||
USE_CHAT_ADMIN_ACCESS: FALSE,
|
||||
USE_COURSE_OWNER_ACCESS: FALSE,
|
||||
USE_PROJECTID_AS_NAME: FALSE,
|
||||
USER_MAX_RESULTS: '500',
|
||||
USER_SERVICE_ACCOUNT_ACCESS_ONLY: FALSE,
|
||||
}
|
||||
|
||||
Values = {DEBUG_LEVEL: 0}
|
||||
|
||||
TYPE_BOOLEAN = 'bool'
|
||||
TYPE_CHARACTER = 'char'
|
||||
TYPE_CHOICE = 'choi'
|
||||
TYPE_CHOICE_LIST = 'chol'
|
||||
TYPE_DATETIME = 'datm'
|
||||
TYPE_DIRECTORY = 'dire'
|
||||
TYPE_EMAIL = 'emai'
|
||||
TYPE_EMAIL_OPTIONAL = 'emao'
|
||||
TYPE_FILE = 'file'
|
||||
TYPE_FLOAT = 'floa'
|
||||
TYPE_HEADERFILTER = 'heaf'
|
||||
TYPE_HEADERFORCE = 'hefo'
|
||||
TYPE_HEADERORDER = 'heor'
|
||||
TYPE_INTEGER = 'inte'
|
||||
TYPE_LANGUAGE = 'lang'
|
||||
TYPE_LOCALE = 'locl'
|
||||
TYPE_PASSWORD = 'pass'
|
||||
TYPE_ROWFILTER = 'rowf'
|
||||
TYPE_STRING = 'stri'
|
||||
TYPE_STRINGLIST = 'strl'
|
||||
TYPE_TIMEZONE = 'tmzn'
|
||||
|
||||
VAR_TYPE = 'type'
|
||||
VAR_ENVVAR = 'enva'
|
||||
VAR_CHOICES = 'chod'
|
||||
VAR_LIMITS = 'lmit'
|
||||
VAR_SFFT = 'sfft'
|
||||
VAR_SIGFILE = 'sigf'
|
||||
VAR_ACCESS = 'aces'
|
||||
|
||||
VAR_INFO = {
|
||||
ACTIVITY_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 500)},
|
||||
ADMIN_EMAIL: {VAR_TYPE: TYPE_STRING, VAR_ENVVAR: 'GA_ADMIN_EMAIL', VAR_LIMITS: (0, None)},
|
||||
API_CALLS_RATE_CHECK: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
API_CALLS_RATE_LIMIT: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (50, None)},
|
||||
API_CALLS_TRIES_LIMIT: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (3, 30)},
|
||||
AUTO_BATCH_MIN: {VAR_TYPE: TYPE_INTEGER, VAR_ENVVAR: 'GAM_AUTOBATCH', VAR_LIMITS: (0, 100)},
|
||||
BAIL_ON_INTERNAL_ERROR_TRIES: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 10)},
|
||||
BATCH_SIZE: {VAR_TYPE: TYPE_INTEGER, VAR_ENVVAR: 'GAM_BATCH_SIZE', VAR_LIMITS: (1, 1000)},
|
||||
CACERTS_PEM: {VAR_TYPE: TYPE_FILE, VAR_ENVVAR: 'GAM_CA_FILE', VAR_ACCESS: os.R_OK},
|
||||
CACHE_DIR: {VAR_TYPE: TYPE_DIRECTORY, VAR_ENVVAR: 'GAMCACHEDIR'},
|
||||
CACHE_DISCOVERY_ONLY: {VAR_TYPE: TYPE_BOOLEAN, VAR_SIGFILE: 'allcache.txt', VAR_SFFT: (TRUE, FALSE)},
|
||||
CHARSET: {VAR_TYPE: TYPE_STRING, VAR_ENVVAR: 'GAM_CHARSET', VAR_LIMITS: (1, None)},
|
||||
CHANNEL_CUSTOMER_ID: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
CLASSROOM_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (0, 1000)},
|
||||
CLIENT_SECRETS_JSON: {VAR_TYPE: TYPE_FILE, VAR_ENVVAR: 'CLIENTSECRETS', VAR_ACCESS: os.R_OK},
|
||||
CLOCK_SKEW_IN_SECONDS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (10, 3600)},
|
||||
CMDLOG: {VAR_TYPE: TYPE_FILE, VAR_ACCESS: os.W_OK},
|
||||
CMDLOG_MAX_BACKUPS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 10)},
|
||||
CMDLOG_MAX_KILO_BYTES: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (100, 10000)},
|
||||
CONFIG_DIR: {VAR_TYPE: TYPE_DIRECTORY, VAR_ENVVAR: 'GAMUSERCONFIGDIR'},
|
||||
CONTACT_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 10000)},
|
||||
CSV_INPUT_COLUMN_DELIMITER: {VAR_TYPE: TYPE_CHARACTER},
|
||||
CSV_INPUT_NO_ESCAPE_CHAR: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
CSV_INPUT_QUOTE_CHAR: {VAR_TYPE: TYPE_CHARACTER},
|
||||
CSV_INPUT_ROW_FILTER: {VAR_TYPE: TYPE_ROWFILTER},
|
||||
CSV_INPUT_ROW_FILTER_MODE: {VAR_TYPE: TYPE_CHOICE, VAR_CHOICES: {'allmatch': True, 'anymatch': False}},
|
||||
CSV_INPUT_ROW_DROP_FILTER: {VAR_TYPE: TYPE_ROWFILTER},
|
||||
CSV_INPUT_ROW_DROP_FILTER_MODE: {VAR_TYPE: TYPE_CHOICE, VAR_CHOICES: {'allmatch': True, 'anymatch': False}},
|
||||
CSV_INPUT_ROW_LIMIT: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (0, None)},
|
||||
CSV_OUTPUT_COLUMN_DELIMITER: {VAR_TYPE: TYPE_CHARACTER},
|
||||
CSV_OUTPUT_CONVERT_CR_NL: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
CSV_OUTPUT_NO_ESCAPE_CHAR: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
CSV_OUTPUT_FIELD_DELIMITER: {VAR_TYPE: TYPE_CHARACTER},
|
||||
CSV_OUTPUT_HEADER_FILTER: {VAR_TYPE: TYPE_HEADERFILTER},
|
||||
CSV_OUTPUT_HEADER_DROP_FILTER: {VAR_TYPE: TYPE_HEADERFILTER},
|
||||
CSV_OUTPUT_HEADER_FORCE: {VAR_TYPE: TYPE_HEADERFORCE},
|
||||
CSV_OUTPUT_HEADER_ORDER: {VAR_TYPE: TYPE_HEADERORDER},
|
||||
CSV_OUTPUT_LINE_TERMINATOR: {VAR_TYPE: TYPE_CHOICE, VAR_CHOICES: {'cr': '\r', 'lf': '\n', 'crlf': '\r\n'}},
|
||||
CSV_OUTPUT_QUOTE_CHAR: {VAR_TYPE: TYPE_CHARACTER},
|
||||
CSV_OUTPUT_ROW_FILTER: {VAR_TYPE: TYPE_ROWFILTER},
|
||||
CSV_OUTPUT_ROW_FILTER_MODE: {VAR_TYPE: TYPE_CHOICE, VAR_CHOICES: {'allmatch': True, 'anymatch': False}},
|
||||
CSV_OUTPUT_ROW_DROP_FILTER: {VAR_TYPE: TYPE_ROWFILTER},
|
||||
CSV_OUTPUT_ROW_DROP_FILTER_MODE: {VAR_TYPE: TYPE_CHOICE, VAR_CHOICES: {'allmatch': True, 'anymatch': False}},
|
||||
CSV_OUTPUT_ROW_LIMIT: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (0, None)},
|
||||
CSV_OUTPUT_SORT_HEADERS: {VAR_TYPE: TYPE_STRINGLIST},
|
||||
CSV_OUTPUT_SUBFIELD_DELIMITER: {VAR_TYPE: TYPE_CHARACTER},
|
||||
CSV_OUTPUT_TIMESTAMP_COLUMN: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
CSV_OUTPUT_USERS_AUDIT: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
CUSTOMER_ID: {VAR_TYPE: TYPE_STRING, VAR_ENVVAR: 'CUSTOMER_ID', VAR_LIMITS: (0, None)},
|
||||
DEBUG_LEVEL: {VAR_TYPE: TYPE_INTEGER, VAR_SIGFILE: 'debug.gam', VAR_LIMITS: (0, None), VAR_SFFT: ('0', '4')},
|
||||
DEVICE_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 200)},
|
||||
DOMAIN: {VAR_TYPE: TYPE_STRING, VAR_ENVVAR: 'GA_DOMAIN', VAR_LIMITS: (0, None)},
|
||||
DRIVE_DIR: {VAR_TYPE: TYPE_DIRECTORY, VAR_ENVVAR: 'GAMDRIVEDIR'},
|
||||
DRIVE_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 1000)},
|
||||
DRIVE_V3_BETA: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
DRIVE_V3_NATIVE_NAMES: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
EMAIL_BATCH_SIZE: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 100)},
|
||||
ENABLE_DASA: {VAR_TYPE: TYPE_BOOLEAN, VAR_SIGFILE: 'enabledasa.txt', VAR_SFFT: (FALSE, TRUE)},
|
||||
ENABLE_GCLOUD_REAUTH: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
EVENT_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 2500)},
|
||||
EXTRA_ARGS: {VAR_TYPE: TYPE_FILE, VAR_SIGFILE: FN_EXTRA_ARGS_TXT, VAR_SFFT: ('', FN_EXTRA_ARGS_TXT), VAR_ACCESS: os.R_OK},
|
||||
GMAIL_CSE_INCERT_DIR: {VAR_TYPE: TYPE_DIRECTORY},
|
||||
GMAIL_CSE_INKEY_DIR: {VAR_TYPE: TYPE_DIRECTORY},
|
||||
INTER_BATCH_WAIT: {VAR_TYPE: TYPE_FLOAT, VAR_LIMITS: (0.0, 60.0)},
|
||||
LICENSE_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (10, 1000)},
|
||||
LICENSE_SKUS: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
MEMBER_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 200)},
|
||||
MESSAGE_BATCH_SIZE: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 1000)},
|
||||
MESSAGE_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 10000)},
|
||||
MOBILE_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 100)},
|
||||
MULTIPROCESS_POOL_LIMIT: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (-1, None)},
|
||||
NEVER_TIME: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
NO_BROWSER: {VAR_TYPE: TYPE_BOOLEAN, VAR_SIGFILE: 'nobrowser.txt', VAR_SFFT: (FALSE, TRUE)},
|
||||
NO_CACHE: {VAR_TYPE: TYPE_BOOLEAN, VAR_SIGFILE: 'nocache.txt', VAR_SFFT: (FALSE, TRUE)},
|
||||
NO_SHORT_URLS: {VAR_TYPE: TYPE_BOOLEAN, VAR_SIGFILE: 'noshorturls.txt', VAR_SFFT: (FALSE, TRUE)},
|
||||
NO_UPDATE_CHECK: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
NO_VERIFY_SSL: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
NUM_TBATCH_THREADS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 1000)},
|
||||
NUM_THREADS: {VAR_TYPE: TYPE_INTEGER, VAR_ENVVAR: 'GAM_THREADS', VAR_LIMITS: (1, 1000)},
|
||||
OAUTH2_TXT: {VAR_TYPE: TYPE_FILE, VAR_ENVVAR: 'OAUTHFILE', VAR_ACCESS: os.R_OK | os.W_OK},
|
||||
OAUTH2SERVICE_JSON: {VAR_TYPE: TYPE_FILE, VAR_ENVVAR: 'OAUTHSERVICEFILE', VAR_ACCESS: os.R_OK | os.W_OK},
|
||||
OUTPUT_DATEFORMAT: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
OUTPUT_TIMEFORMAT: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
PEOPLE_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (0, 1000)},
|
||||
PRINT_AGU_DOMAINS: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
PRINT_CROS_OUS: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
PRINT_CROS_OUS_AND_CHILDREN: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
PROCESS_WAIT_LIMIT: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (0, None)},
|
||||
QUICK_CROS_MOVE: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
QUICK_INFO_USER: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
RESELLER_ID: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
RETRY_API_SERVICE_NOT_AVAILABLE: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
SECTION: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
SHOW_API_CALLS_RETRY_DATA: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
SHOW_COMMANDS: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
SHOW_CONVERT_CR_NL: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
SHOW_COUNTS_MIN: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (0, 100)},
|
||||
SHOW_GETTINGS: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
SHOW_GETTINGS_GOT_NL: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
SHOW_MULTIPROCESS_INFO: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
SMTP_FQDN: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
SMTP_HOST: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
SMTP_USERNAME: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
SMTP_PASSWORD: {VAR_TYPE: TYPE_PASSWORD, VAR_LIMITS: (0, None)},
|
||||
TLS_MIN_VERSION: {VAR_TYPE: TYPE_CHOICE, VAR_ENVVAR: 'GAM_TLS_MIN_VERSION', VAR_CHOICES: TLS_CHOICE_MAP},
|
||||
TLS_MAX_VERSION: {VAR_TYPE: TYPE_CHOICE, VAR_ENVVAR: 'GAM_TLS_MAX_VERSION', VAR_CHOICES: TLS_CHOICE_MAP},
|
||||
TIMEZONE: {VAR_TYPE: TYPE_TIMEZONE},
|
||||
TODRIVE_CLEARFILTER: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
TODRIVE_CLIENTACCESS: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
TODRIVE_CONVERSION: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
TODRIVE_LOCALCOPY: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
TODRIVE_LOCALE: {VAR_TYPE: TYPE_LOCALE},
|
||||
TODRIVE_NOBROWSER: {VAR_TYPE: TYPE_BOOLEAN, VAR_SIGFILE: 'nobrowser.txt', VAR_SFFT: (FALSE, TRUE)},
|
||||
TODRIVE_NOEMAIL: {VAR_TYPE: TYPE_BOOLEAN, VAR_SIGFILE: 'notdemail.txt', VAR_SFFT: (FALSE, TRUE)},
|
||||
TODRIVE_NO_ESCAPE_CHAR: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
TODRIVE_PARENT: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
TODRIVE_SHEET_TIMESTAMP: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
TODRIVE_SHEET_TIMEFORMAT: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
TODRIVE_TIMESTAMP: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
TODRIVE_TIMEFORMAT: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
TODRIVE_TIMEZONE: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
TODRIVE_UPLOAD_NODATA: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
TODRIVE_USER: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
|
||||
TRUNCATE_CLIENT_ID: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
UPDATE_CROS_OU_WITH_ID: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
USE_CHAT_ADMIN_ACCESS: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
USE_COURSE_OWNER_ACCESS: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
USE_PROJECTID_AS_NAME: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
USER_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 500)},
|
||||
USER_SERVICE_ACCOUNT_ACCESS_ONLY: {VAR_TYPE: TYPE_BOOLEAN},
|
||||
}
|
||||
1184
src/gam/gamlib/glclargs.py
Normal file
1184
src/gam/gamlib/glclargs.py
Normal file
File diff suppressed because it is too large
Load Diff
831
src/gam/gamlib/glentity.py
Normal file
831
src/gam/gamlib/glentity.py
Normal file
@@ -0,0 +1,831 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2024 Ross Scroggs All Rights Reserved.
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""GAM entity processing
|
||||
|
||||
"""
|
||||
|
||||
class GamEntity():
|
||||
|
||||
ROLE_MANAGER = 'MANAGER'
|
||||
ROLE_MEMBER = 'MEMBER'
|
||||
ROLE_OWNER = 'OWNER'
|
||||
ROLE_LIST = [ROLE_MANAGER, ROLE_MEMBER, ROLE_OWNER]
|
||||
ROLE_USER = 'USER'
|
||||
ROLE_MANAGER_MEMBER = ','.join([ROLE_MANAGER, ROLE_MEMBER])
|
||||
ROLE_MANAGER_OWNER = ','.join([ROLE_MANAGER, ROLE_OWNER])
|
||||
ROLE_MEMBER_OWNER = ','.join([ROLE_MEMBER, ROLE_OWNER])
|
||||
ROLE_MANAGER_MEMBER_OWNER = ','.join(ROLE_LIST)
|
||||
ROLE_PUBLIC = 'PUBLIC'
|
||||
ROLE_ALL = ROLE_MANAGER_MEMBER_OWNER
|
||||
|
||||
TYPE_CBCM_BROWSER = 'CBCM_BROWSER'
|
||||
TYPE_CUSTOMER = 'CUSTOMER'
|
||||
TYPE_EXTERNAL = 'EXTERNAL'
|
||||
TYPE_OTHER = 'OTHER'
|
||||
TYPE_GROUP = 'GROUP'
|
||||
TYPE_SERVICE_ACCOUNT = 'SERVICE_ACCOUNT'
|
||||
TYPE_USER = 'USER'
|
||||
|
||||
# Keys into NAMES; arbitrary values but must be unique
|
||||
ACCESS_TOKEN = 'atok'
|
||||
ACCOUNT = 'acct'
|
||||
ACTION = 'actn'
|
||||
ACTIVITY = 'actv'
|
||||
ADMINISTRATOR = 'admi'
|
||||
ADMIN_ROLE = 'adro'
|
||||
ADMIN_ROLE_ASSIGNMENT = 'adra'
|
||||
ALERT = 'alrt'
|
||||
ALERT_ID = 'alri'
|
||||
ALERT_FEEDBACK = 'alfb'
|
||||
ALERT_FEEDBACK_ID = 'alfi'
|
||||
ALIAS = 'alia'
|
||||
ALIAS_EMAIL = 'alie'
|
||||
ALIAS_TARGET = 'alit'
|
||||
ANALYTIC_ACCOUNT = 'anac'
|
||||
ANALYTIC_ACCOUNT_SUMMARY = 'anas'
|
||||
ANALYTIC_DATASTREAM = 'anad'
|
||||
ANALYTIC_PROPERTY = 'anap'
|
||||
ANALYTIC_UA_PROPERTY = 'anau'
|
||||
API = 'api '
|
||||
APP_ACCESS_SETTINGS = 'apps'
|
||||
APP_ID = 'appi'
|
||||
APP_NAME = 'appn'
|
||||
APPLICATION_SPECIFIC_PASSWORD = 'aspa'
|
||||
ARROWS_ENABLED = 'arro'
|
||||
ATTACHMENT = 'atta'
|
||||
ATTENDEE = 'atnd'
|
||||
AUDIT_ACTIVITY_REQUEST = 'auda'
|
||||
AUDIT_EXPORT_REQUEST = 'audx'
|
||||
AUDIT_MONITOR_REQUEST = 'audm'
|
||||
BACKUP_VERIFICATION_CODES = 'buvc'
|
||||
BUILDING = 'bldg'
|
||||
BUILDING_ID = 'bldi'
|
||||
CAA_LEVEL = 'calv'
|
||||
CALENDAR = 'cale'
|
||||
CALENDAR_ACL = 'cacl'
|
||||
CALENDAR_SETTINGS = 'cset'
|
||||
CHANNEL_CUSTOMER = 'chcu'
|
||||
CHANNEL_CUSTOMER_ENTITLEMENT = 'chce'
|
||||
CHANNEL_OFFER = 'chof'
|
||||
CHANNEL_PRODUCT = 'chpr'
|
||||
CHANNEL_SKU = 'chsk'
|
||||
CHAT_BOT = 'chbo'
|
||||
CHAT_ADMIN = 'chad'
|
||||
CHAT_EVENT = 'chev'
|
||||
CHAT_MANAGER_USER = 'chgu'
|
||||
CHAT_MEMBER = 'chme'
|
||||
CHAT_MEMBER_GROUP = 'chmg'
|
||||
CHAT_MEMBER_USER = 'chmu'
|
||||
CHAT_MESSAGE = 'chms'
|
||||
CHAT_MESSAGE_ID = 'chmi'
|
||||
CHAT_SPACE = 'chsp'
|
||||
CHAT_THREAD = 'chth'
|
||||
CHILD_ORGANIZATIONAL_UNIT = 'corg'
|
||||
CHROME_APP = 'capp'
|
||||
CHROME_APP_DEVICE = 'capd'
|
||||
CHROME_BROWSER = 'chbr'
|
||||
CHROME_BROWSER_ENROLLMENT_TOKEN = 'cbet'
|
||||
CHROME_CHANNEL = 'chan'
|
||||
CHROME_DEVICE = 'chdv'
|
||||
CHROME_MODEL = 'chmo'
|
||||
CHROME_NETWORK_ID = 'chni'
|
||||
CHROME_NETWORK_NAME = 'chnn'
|
||||
CHROME_PLATFORM = 'cpla'
|
||||
CHROME_POLICY = 'cpol'
|
||||
CHROME_POLICY_IMAGE = 'cpim'
|
||||
CHROME_POLICY_SCHEMA = 'cpsc'
|
||||
CHROME_PROFILE = 'cpro'
|
||||
CHROME_RELEASE = 'crel'
|
||||
CHROME_VERSION = 'cver'
|
||||
CLASSIFICATION_LABEL = 'dlab'
|
||||
CLASSIFICATION_LABEL_FIELD_ID = 'dlfi'
|
||||
CLASSIFICATION_LABEL_ID = 'dlid'
|
||||
CLASSIFICATION_LABEL_NAME = 'dlna'
|
||||
CLASSIFICATION_LABEL_PERMISSION = 'dlpe'
|
||||
CLASSIFICATION_LABEL_PERMISSION_NAME = 'dlpn'
|
||||
CLASSROOM_INVITATION = 'clai'
|
||||
CLASSROOM_INVITATION_OWNER = 'clio'
|
||||
CLASSROOM_INVITATION_STUDENT = 'clis'
|
||||
CLASSROOM_INVITATION_TEACHER = 'clit'
|
||||
CLASSROOM_OAUTH2_TXT_FILE = 'coa'
|
||||
CLASSROOM_USER_PROFILE = 'clup'
|
||||
CLIENT_ID = 'clid'
|
||||
CLIENT_SECRETS_JSON_FILE = 'csjf'
|
||||
CLOUD_IDENTITY_GROUP = 'cidg'
|
||||
CLOUD_STORAGE_BUCKET = 'clsb'
|
||||
CLOUD_STORAGE_FILE = 'clsf'
|
||||
COLLABORATOR = 'cola'
|
||||
COMMAND_ID = 'cmdi'
|
||||
COMPANY_DEVICE = 'codv'
|
||||
CONFIG_FILE = 'conf'
|
||||
CONTACT = 'cont'
|
||||
CONTACT_DELEGATE = 'cond'
|
||||
CONTACT_GROUP = 'cogr'
|
||||
CONTACT_GROUP_NAME = 'cogn'
|
||||
COPYFROM_COURSE = 'cfco'
|
||||
COPYFROM_GROUP = 'cfgr'
|
||||
COURSE = 'cour'
|
||||
COURSE_ALIAS = 'coal'
|
||||
COURSE_ANNOUNCEMENT = 'cann'
|
||||
COURSE_ANNOUNCEMENT_ID = 'caid'
|
||||
COURSE_ANNOUNCEMENT_STATE = 'cast'
|
||||
COURSE_MATERIAL_DRIVEFILE = 'comd'
|
||||
COURSE_MATERIAL_FORM = 'comf'
|
||||
COURSE_MATERIAL = 'cmtl'
|
||||
COURSE_MATERIAL_ID = 'cmid'
|
||||
COURSE_MATERIAL_STATE = 'cmst'
|
||||
COURSE_NAME = 'cona'
|
||||
COURSE_STATE = 'cost'
|
||||
COURSE_SUBMISSION_ID = 'csid'
|
||||
COURSE_SUBMISSION_STATE = 'csst'
|
||||
COURSE_TOPIC = 'ctop'
|
||||
COURSE_TOPIC_ID = 'ctoi'
|
||||
COURSE_WORK = 'cwrk'
|
||||
COURSE_WORK_ID = 'cwid'
|
||||
COURSE_WORK_STATE = 'cwst'
|
||||
CREATOR_ID = 'crid'
|
||||
CREDENTIALS = 'cred'
|
||||
CRITERIA = 'crit'
|
||||
CROS_DEVICE = 'cros'
|
||||
CROS_SERIAL_NUMBER = 'crsn'
|
||||
CSE_IDENTITY = 'csei'
|
||||
CSE_KEYPAIR = 'csek'
|
||||
CUSTOMER_DOMAIN = 'cudo'
|
||||
CUSTOMER_ID = 'cuid'
|
||||
DATE = 'date'
|
||||
DEFAULT_LANGUAGE = 'dfla'
|
||||
DELEGATE = 'dele'
|
||||
DELETED_USER = 'delu'
|
||||
DELIVERY = 'deli'
|
||||
DEVICE = 'devi'
|
||||
DEVICE_FILE = 'devf'
|
||||
DIRECTORY = 'drct'
|
||||
DEVICE_USER = 'devu'
|
||||
DEVICE_USER_CLIENT_STATE = 'ducs'
|
||||
DISCOVERY_JSON_FILE = 'disc'
|
||||
DOCUMENT = 'docu'
|
||||
DOMAIN = 'doma'
|
||||
DOMAIN_ALIAS = 'doal'
|
||||
DOMAIN_CONTACT = 'doco'
|
||||
DOMAIN_PEOPLE_CONTACT = 'dopc'
|
||||
DOMAIN_PROFILE = 'dopr'
|
||||
DRIVE_DISK_USAGE = 'drdu'
|
||||
DRIVE_FILE = 'dfil'
|
||||
DRIVE_FILE_COMMENT = 'filc'
|
||||
DRIVE_FILE_ID = 'fili'
|
||||
DRIVE_FILE_NAME = 'filn'
|
||||
DRIVE_FILE_RENAMED = 'firn'
|
||||
DRIVE_FILE_REVISION = 'filr'
|
||||
DRIVE_FILE_SHORTCUT = 'fils'
|
||||
DRIVE_FILE_OR_FOLDER = 'fifo'
|
||||
DRIVE_FILE_OR_FOLDER_ACL = 'fiac'
|
||||
DRIVE_FILE_OR_FOLDER_ID = 'fifi'
|
||||
DRIVE_FOLDER = 'fold'
|
||||
DRIVE_FOLDER_ID = 'foli'
|
||||
DRIVE_FOLDER_NAME = 'foln'
|
||||
DRIVE_FOLDER_PATH = 'folp'
|
||||
DRIVE_FOLDER_RENAMED = 'forn'
|
||||
DRIVE_FOLDER_SHORTCUT = 'fols'
|
||||
DRIVE_ORPHAN_FILE_OR_FOLDER = 'orph'
|
||||
DRIVE_PARENT_FOLDER = 'fipf'
|
||||
DRIVE_PARENT_FOLDER_ID = 'fipi'
|
||||
DRIVE_PARENT_FOLDER_REFERENCE = 'pfrf'
|
||||
DRIVE_PATH = 'drvp'
|
||||
DRIVE_SETTINGS = 'drvs'
|
||||
DRIVE_SHORTCUT = 'drsc'
|
||||
DRIVE_SHORTCUT_ID = 'dsci'
|
||||
DRIVE_3PSHORTCUT = 'dr3s'
|
||||
DRIVE_TRASH = 'drvt'
|
||||
EMAIL = 'emai'
|
||||
EMAIL_ALIAS = 'emal'
|
||||
EMAIL_SETTINGS = 'emse'
|
||||
END_TIME = 'endt'
|
||||
ENTITY = 'enti'
|
||||
EVENT = 'evnt'
|
||||
EVENT_BIRTHDAY = 'evbd'
|
||||
EVENT_FOCUSTIME = 'evft'
|
||||
EVENT_OUTOFOFFICE = 'evoo'
|
||||
EVENT_WORKINGLOCATION = 'evwl'
|
||||
FEATURE = 'feat'
|
||||
FIELD = 'fiel'
|
||||
FILE = 'file'
|
||||
FILE_PARENT_TREE = 'fptr'
|
||||
FILTER = 'filt'
|
||||
FORM = 'form'
|
||||
FORM_RESPONSE = 'frmr'
|
||||
FORWARD_ENABLED = 'fwde'
|
||||
FORWARDING_ADDRESS = 'fwda'
|
||||
GCP_FOLDER = 'gcpf'
|
||||
GCP_FOLDER_NAME = 'gcpn'
|
||||
GMAIL_PROFILE = 'gmpr'
|
||||
GROUP = 'grou'
|
||||
GROUP_ALIAS = 'gali'
|
||||
GROUP_EMAIL = 'gale'
|
||||
GROUP_MEMBERSHIP = 'gmem'
|
||||
GROUP_MEMBERSHIP_TREE = 'gmtr'
|
||||
GROUP_SETTINGS = 'gset'
|
||||
GROUP_TREE = 'gtre'
|
||||
GUARDIAN = 'guar'
|
||||
GUARDIAN_INVITATION = 'gari'
|
||||
GUARDIAN_AND_INVITATION = 'gani'
|
||||
IAM_POLICY = 'iamp'
|
||||
IMAP_ENABLED = 'imap'
|
||||
INBOUND_SSO_ASSIGNMENT = 'insa'
|
||||
INBOUND_SSO_CREDENTIALS = 'insc'
|
||||
INBOUND_SSO_PROFILE = 'insp'
|
||||
INSTANCE = 'inst'
|
||||
ITEM = 'item'
|
||||
ISSUER_CN = 'iscn'
|
||||
KEYBOARD_SHORTCUTS_ENABLED = 'kbsc'
|
||||
LABEL = 'labe'
|
||||
LABEL_ID = 'labi'
|
||||
LANGUAGE = 'lang'
|
||||
LICENSE = 'lice'
|
||||
LOCATION = 'loca'
|
||||
LOOKERSTUDIO_ASSET = 'lsas'
|
||||
LOOKERSTUDIO_ASSET_DATASOURCE = 'lsad'
|
||||
LOOKERSTUDIO_ASSETID = 'lsai'
|
||||
LOOKERSTUDIO_ASSET_REPORT = 'lsar'
|
||||
LOOKERSTUDIO_PERMISSION = 'lspe'
|
||||
MD5HASH = 'md5h'
|
||||
MEET_SPACE = 'mesp'
|
||||
MEET_CONFERENCE = 'msco'
|
||||
MEET_PARTICIPANT = 'msps'
|
||||
MEET_RECORDING = 'msre'
|
||||
MEET_TRANSCRIPT = 'mstr'
|
||||
MEMBER = 'memb'
|
||||
MEMBER_NOT_ARCHIVED = 'mena'
|
||||
MEMBER_ARCHIVED = 'mear'
|
||||
MEMBER_NOT_SUSPENDED = 'mens'
|
||||
MEMBER_SUSPENDED = 'mesu'
|
||||
MEMBER_NOT_SUSPENDED_NOT_ARCHIVED = 'nsna'
|
||||
MEMBER_SUSPENDED_ARCHIVED = 'suar'
|
||||
MEMBER_RESTRICTION = 'memr'
|
||||
MEMBER_URI = 'memu'
|
||||
MEMBERSHIP_TREE = 'metr'
|
||||
MESSAGE = 'mesg'
|
||||
MIMETYPE = 'mime'
|
||||
MOBILE_DEVICE = 'mobi'
|
||||
NAME = 'name'
|
||||
NOTE = 'note'
|
||||
NOTE_ACL = 'nota'
|
||||
NOTES_ACLS = 'naac'
|
||||
NONEDITABLE_ALIAS = 'neal'
|
||||
OAUTH2_TXT_FILE = 'oaut'
|
||||
OAUTH2SERVICE_JSON_FILE = 'oau2'
|
||||
ORGANIZATIONAL_UNIT = 'orgu'
|
||||
OTHER_CONTACT = 'otco'
|
||||
OWNER = 'ownr'
|
||||
OWNER_ID = 'owid'
|
||||
PAGE_SIZE = 'page'
|
||||
PARENT_ORGANIZATIONAL_UNIT = 'porg'
|
||||
PARTICIPANT = 'part'
|
||||
PEOPLE_CONTACT = 'peco'
|
||||
PEOPLE_CONTACT_GROUP = 'pecg'
|
||||
PEOPLE_PHOTO = 'peph'
|
||||
PEOPLE_PROFILE = 'pepr'
|
||||
PERMISSION = 'perm'
|
||||
PERMISSION_ID = 'peid'
|
||||
PERMITTEE = 'prmt'
|
||||
PERSONAL_DEVICE = 'pedv'
|
||||
PHOTO = 'phot'
|
||||
POLICY = 'poli'
|
||||
POP_ENABLED = 'popa'
|
||||
PRESENTATION = 'pres'
|
||||
PRINTER = 'prin'
|
||||
PRINTER_ID = 'prid'
|
||||
PRINTER_MODEL = 'prmd'
|
||||
PRIVILEGE = 'priv'
|
||||
PRODUCT = 'prod'
|
||||
PROFILE_SHARING_ENABLED = 'prof'
|
||||
PROJECT = 'proj'
|
||||
PROJECT_FOLDER = 'prjf'
|
||||
PROJECT_ID = 'prji'
|
||||
PUBLIC_KEY = 'pubk'
|
||||
QUERY = 'quer'
|
||||
RECIPIENT = 'recp'
|
||||
RECIPIENT_BCC = 'rebc'
|
||||
RECIPIENT_CC = 'recc'
|
||||
REPORT = 'rept'
|
||||
REQUEST_ID = 'reqi'
|
||||
RESOURCE_CALENDAR = 'resc'
|
||||
RESOURCE_ID = 'resi'
|
||||
ROLE = 'role'
|
||||
ROW = 'row '
|
||||
SCOPE = 'scop'
|
||||
SECTION = 'sect'
|
||||
SENDAS_ADDRESS = 'sasa'
|
||||
SENDER = 'send'
|
||||
SERVICE = 'serv'
|
||||
SHAREDDRIVE = 'tdrv'
|
||||
SHAREDDRIVE_ACL = 'tdac'
|
||||
SHAREDDRIVE_FOLDER = 'tdfo'
|
||||
SHAREDDRIVE_ID = 'tdid'
|
||||
SHAREDDRIVE_NAME = 'tdna'
|
||||
SHAREDDRIVE_THEME = 'tdth'
|
||||
SHEET = 'shet'
|
||||
SHEET_ID = 'shti'
|
||||
SIGNATURE = 'sign'
|
||||
SITE = 'site'
|
||||
SITE_ACL = 'sacl'
|
||||
SIZE = 'size'
|
||||
SKU = 'sku '
|
||||
SMIME_ID = 'smid'
|
||||
SNIPPETS_ENABLED = 'snip'
|
||||
SSO_KEY = 'ssok'
|
||||
SSO_SETTINGS = 'ssos'
|
||||
SOURCE_USER = 'src'
|
||||
SPREADSHEET = 'sprd'
|
||||
SPREADSHEET_RANGE = 'ssrn'
|
||||
START_TIME = 'strt'
|
||||
STATUS = 'stat'
|
||||
STUDENT = 'stud'
|
||||
SUBSCRIPTION = 'subs'
|
||||
SVCACCT = 'svac'
|
||||
SVCACCT_KEY = 'svky'
|
||||
TARGET_USER = 'tgt'
|
||||
TASK = 'task'
|
||||
TASKLIST = 'tali'
|
||||
TEACHER = 'teac'
|
||||
THREAD = 'thre'
|
||||
TRANSFER_APPLICATION = 'trap'
|
||||
TRANSFER_ID = 'trid'
|
||||
TRANSFER_REQUEST = 'trnr'
|
||||
TRASHED_EVENT = 'trev'
|
||||
TRUSTED_APPLICATION = 'trus'
|
||||
TYPE = 'type'
|
||||
UNICODE_ENCODING_ENABLED = 'unic'
|
||||
UNIQUE_ID = 'uniq'
|
||||
URL = 'url '
|
||||
USER = 'user'
|
||||
USER_ALIAS = 'uali'
|
||||
USER_EMAIL = 'uema'
|
||||
USER_INVITATION = 'uinv'
|
||||
USER_NOT_SUSPENDED = 'uns'
|
||||
USER_SCHEMA = 'usch'
|
||||
USER_SUSPENDED = 'usup'
|
||||
VACATION = 'vaca'
|
||||
VACATION_ENABLED = 'vace'
|
||||
VALUE = 'val'
|
||||
VAULT_EXPORT = 'vlte'
|
||||
VAULT_HOLD = 'vlth'
|
||||
VAULT_MATTER = 'vltm'
|
||||
VAULT_MATTER_ARTIFACT = 'vlma'
|
||||
VAULT_MATTER_ID = 'vlmi'
|
||||
VAULT_OPERATION = 'vlto'
|
||||
VAULT_QUERY = 'vltq'
|
||||
WEBCLIPS_ENABLED = 'webc'
|
||||
YOUTUBE_CHANNEL = 'ytch'
|
||||
# _NAMES[0] is plural, _NAMES[1] is singular unless the item name is explicitly plural (Calendar Settings)
|
||||
# For items with Boolean values, both entries are singular (Forward, POP)
|
||||
# These values can be translated into other languages
|
||||
_NAMES = {
|
||||
ACCESS_TOKEN: ['Access Tokens', 'Access Token'],
|
||||
ACCOUNT: ['Google Workspace Accounts', 'Google Workspace Account'],
|
||||
ACTION: ['Actions', 'Action'],
|
||||
ACTIVITY: ['Activities', 'Activity'],
|
||||
ADMINISTRATOR: ['Administrators', 'Administrator'],
|
||||
ADMIN_ROLE: ['Admin Roles', 'Admin Role'],
|
||||
ADMIN_ROLE_ASSIGNMENT: ['Admin Role Assignments', 'Admin Role Assignment'],
|
||||
ALERT: ['Alerts', 'Alert'],
|
||||
ALERT_ID: ['Alert IDs', 'Alert ID'],
|
||||
ALERT_FEEDBACK: ['Alert Feedbacks', 'Alert Feedback'],
|
||||
ALERT_FEEDBACK_ID: ['Alert Feedback IDs', 'Alert Feedback ID'],
|
||||
ALIAS: ['Aliases', 'Alias'],
|
||||
ALIAS_EMAIL: ['Alias Emails', 'Alias Email'],
|
||||
ALIAS_TARGET: ['Alias Targets', 'Alias Target'],
|
||||
ANALYTIC_ACCOUNT: ['Analytic Accounts', 'Analytic Account'],
|
||||
ANALYTIC_ACCOUNT_SUMMARY: ['Analytic Account Summaries', 'Analytic Account Summary'],
|
||||
ANALYTIC_DATASTREAM: ['Analytic Datastreams', 'Analytic Datastream'],
|
||||
ANALYTIC_PROPERTY: ['Analytic GA4 Properties', 'Analytic GA4 Property'],
|
||||
ANALYTIC_UA_PROPERTY: ['Analytic UA Properties', 'Analytic UA Property'],
|
||||
API: ['APIs', 'API'],
|
||||
APP_ACCESS_SETTINGS: ['Application Access Settings', 'Application Access Settings'],
|
||||
APP_ID: ['Application IDs', 'Application ID'],
|
||||
APP_NAME: ['Application Names', 'Application Name'],
|
||||
APPLICATION_SPECIFIC_PASSWORD: ['Application Specific Password IDs', 'Application Specific Password ID'],
|
||||
ARROWS_ENABLED: ['Personal Indicator Arrows Enabled', 'Personal Indicator Arrows Enabled'],
|
||||
ATTACHMENT: ['Attachments', 'Attachment'],
|
||||
ATTENDEE: ['Attendees', 'Attendee'],
|
||||
AUDIT_ACTIVITY_REQUEST: ['Audit Activity Requests', 'Audit Activity Request'],
|
||||
AUDIT_EXPORT_REQUEST: ['Audit Export Requests', 'Audit Export Request'],
|
||||
AUDIT_MONITOR_REQUEST: ['Audit Monitor Requests', 'Audit Monitor Request'],
|
||||
BACKUP_VERIFICATION_CODES: ['Backup Verification Codes', 'Backup Verification Codes'],
|
||||
BUILDING: ['Buildings', 'Building'],
|
||||
BUILDING_ID: ['Building IDs', 'Building ID'],
|
||||
CAA_LEVEL: ['CAA Levels', 'CAA Level'],
|
||||
CALENDAR: ['Calendars', 'Calendar'],
|
||||
CALENDAR_ACL: ['Calendar ACLs', 'Calendar ACL'],
|
||||
CALENDAR_SETTINGS: ['Calendar Settings', 'Calendar Settings'],
|
||||
CHANNEL_CUSTOMER: ['Channel Customers', 'Channel Customer'],
|
||||
CHANNEL_CUSTOMER_ENTITLEMENT: ['Channel Customer Entitlements', 'Channel Customer Entitlement'],
|
||||
CHANNEL_OFFER: ['Channel Offers', 'Channel Offer'],
|
||||
CHANNEL_PRODUCT: ['Channel Products', 'Channel Product'],
|
||||
CHANNEL_SKU: ['Channel SKUs', 'Channel SKU'],
|
||||
CHAT_BOT: ['Chat BOTs', 'Chat BOT'],
|
||||
CHAT_ADMIN: ['Chat Admins', 'Chat Admin'],
|
||||
CHAT_EVENT: ['Chat Events', 'Chat Event'],
|
||||
CHAT_MANAGER_USER: ['Chat User Managers', 'Chat User Manager'],
|
||||
CHAT_MESSAGE: ['Chat Messages', 'Chat Message'],
|
||||
CHAT_MESSAGE_ID: ['Chat Message IDs', 'Chat Message ID'],
|
||||
CHAT_MEMBER: ['Chat Members', 'Chat Member'],
|
||||
CHAT_MEMBER_GROUP: ['Chat Group Members', 'Chat Group Member'],
|
||||
CHAT_MEMBER_USER: ['Chat User Members', 'Chat User Member'],
|
||||
CHAT_SPACE: ['Chat Spaces', 'Chat Space'],
|
||||
CHAT_THREAD: ['Chat Threads', 'Chat Thread'],
|
||||
CHILD_ORGANIZATIONAL_UNIT: ['Child Organizational Units', 'Child Organizational Unit'],
|
||||
CHROME_APP: ['Chrome Applications', 'Chrome Application'],
|
||||
CHROME_APP_DEVICE: ['Chrome Application Devices', 'Chrome Application Device'],
|
||||
CHROME_BROWSER: ['Chrome Browsers', 'Chrome Browser'],
|
||||
CHROME_BROWSER_ENROLLMENT_TOKEN: ['Chrome Browser Enrollment Tokens', 'Chrome Browser Enrollment Token'],
|
||||
CHROME_CHANNEL: ['Chrome Channels', 'Chrome Channel'],
|
||||
CHROME_DEVICE: ['Chrome Devices', 'Chrome Device'],
|
||||
CHROME_MODEL: ['Chrome Models', 'Chrome Model'],
|
||||
CHROME_NETWORK_ID: ['Chrome Network IDs', 'Chrome Network ID'],
|
||||
CHROME_NETWORK_NAME: ['Chrome Network Names', 'Chrome Network Name'],
|
||||
CHROME_PLATFORM: ['Chrome Platforms', 'Chrome Platform'],
|
||||
CHROME_POLICY: ['Chrome Policies', 'Chrome Policy'],
|
||||
CHROME_POLICY_IMAGE: ['Chrome Policy Images', 'Chrome Policy Image'],
|
||||
CHROME_POLICY_SCHEMA: ['Chrome Policy Schemas', 'Chrome Policy Schema'],
|
||||
CHROME_PROFILE: ['Chrome Profiles', 'Chrome Profile'],
|
||||
CHROME_RELEASE: ['Chrome Releases', 'Chrome Release'],
|
||||
CHROME_VERSION: ['Chrome Versions', 'Chrome Version'],
|
||||
CLASSIFICATION_LABEL: ['Classification Labels', 'Classification Label'],
|
||||
CLASSIFICATION_LABEL_FIELD_ID: ['Classification Label Field IDs', 'Classification Label Field ID'],
|
||||
CLASSIFICATION_LABEL_ID: ['Classification Label IDs', 'Classification Label ID'],
|
||||
CLASSIFICATION_LABEL_NAME: ['Classification Label Names', 'Classification Label Name'],
|
||||
CLASSIFICATION_LABEL_PERMISSION: ['Classification Label Permissions', 'Classification Label Permission'],
|
||||
CLASSIFICATION_LABEL_PERMISSION_NAME: ['Classification Label Permission Names', 'Classification Label Permission Name'],
|
||||
CLASSROOM_INVITATION: ['Classroom Invitations', 'Classroom Invitation'],
|
||||
CLASSROOM_INVITATION_OWNER: ['Classroom Owner Invitations', 'Classroom Owner Invitation'],
|
||||
CLASSROOM_INVITATION_STUDENT: ['Classroom Student Invitations', 'Classroom Student Invitation'],
|
||||
CLASSROOM_INVITATION_TEACHER: ['Classroom Teacher Invitations', 'Classroom Teacher Invitation'],
|
||||
CLASSROOM_OAUTH2_TXT_FILE: ['Classroom OAuth2 File', 'Classroom OAuth2 File'],
|
||||
CLASSROOM_USER_PROFILE: ['Classroom User Profile', 'Classroom User Profile'],
|
||||
CLIENT_ID: ['Client IDs', 'Client ID'],
|
||||
CLIENT_SECRETS_JSON_FILE: ['Client Secrets File', 'Client Secrets File'],
|
||||
CLOUD_IDENTITY_GROUP: ['Cloud Identity Groups', 'Cloud Identity Group'],
|
||||
CLOUD_STORAGE_BUCKET: ['Cloud Storage Buckets', 'Cloud Storage Bucket'],
|
||||
CLOUD_STORAGE_FILE: ['Cloud Storage Files', 'Cloud Storage File'],
|
||||
COLLABORATOR: ['Collaborators', 'Collaborator'],
|
||||
COMMAND_ID: ['Command IDs', 'Command ID'],
|
||||
COMPANY_DEVICE: ['Company Devices', 'Company Device'],
|
||||
CONFIG_FILE: ['Config File', 'Config File'],
|
||||
CONTACT: ['Contacts', 'Contact'],
|
||||
CONTACT_DELEGATE: ['Contact Delegates', 'Contact Delegate'],
|
||||
CONTACT_GROUP: ['Contact Groups', 'Contact Group'],
|
||||
CONTACT_GROUP_NAME: ['Contact Group Names', 'Contact Group Name'],
|
||||
COPYFROM_COURSE: ['Copy From Courses', 'CopyFrom Course'],
|
||||
COPYFROM_GROUP: ['Copy From Groups', 'CopyFrom Group'],
|
||||
COURSE: ['Courses', 'Course'],
|
||||
COURSE_ALIAS: ['Course Aliases', 'Course Alias'],
|
||||
COURSE_ANNOUNCEMENT: ['Course Announcements', 'Course Announcement'],
|
||||
COURSE_ANNOUNCEMENT_ID: ['Course Announcement IDs', 'Course Announcement ID'],
|
||||
COURSE_ANNOUNCEMENT_STATE: ['Course Announcement States', 'Course Announcement State'],
|
||||
COURSE_MATERIAL_DRIVEFILE: ['Course Material Drive Files', 'Course Material Drive File'],
|
||||
COURSE_MATERIAL_FORM: ['Course Material Forms', 'Course Material Form'],
|
||||
COURSE_MATERIAL: ['Course Materials', 'Course Material'],
|
||||
COURSE_MATERIAL_ID: ['Course Material IDs', 'Course Material ID'],
|
||||
COURSE_MATERIAL_STATE: ['Course Material States', 'Course Material State'],
|
||||
COURSE_NAME: ['Course Names', 'Course Name'],
|
||||
COURSE_STATE: ['Course States', 'Course State'],
|
||||
COURSE_SUBMISSION_ID: ['Course Submission IDs', 'Course Submission ID'],
|
||||
COURSE_SUBMISSION_STATE: ['Course Submission States', 'Course Submission State'],
|
||||
COURSE_TOPIC: ['Course Topics', 'Course Topic'],
|
||||
COURSE_TOPIC_ID: ['Course Topic IDs', 'Course Topic ID'],
|
||||
COURSE_WORK: ['Course Works', 'Course Work'],
|
||||
COURSE_WORK_ID: ['Course Work IDs', 'Course Work ID'],
|
||||
COURSE_WORK_STATE: ['Course Work States', 'Course Work State'],
|
||||
CREATOR_ID: ['Creator IDs', 'Creator ID'],
|
||||
CREDENTIALS: ['Credentials', 'Credentials'],
|
||||
CRITERIA: ['Criteria', 'Criteria'],
|
||||
CROS_DEVICE: ['CrOS Devices', 'CrOS Device'],
|
||||
CROS_SERIAL_NUMBER: ['CrOS Serial Numbers', 'CrOS Serial Numbers'],
|
||||
CSE_IDENTITY: ['CSE Identities', 'CSE Identity'],
|
||||
CSE_KEYPAIR: ['CSE KeyPairs', 'CSE KeyPair'],
|
||||
CUSTOMER_DOMAIN: ['Customer Domains', 'Customer Domain'],
|
||||
CUSTOMER_ID: ['Customer IDs', 'Customer ID'],
|
||||
DATE: ['Dates', 'Date'],
|
||||
DEFAULT_LANGUAGE: ['Default Language', 'Default Language'],
|
||||
DELEGATE: ['Delegates', 'Delegate'],
|
||||
DELETED_USER: ['Deleted Users', 'Deleted User'],
|
||||
DELIVERY: ['Delivery', 'Delivery'],
|
||||
DEVICE: ['Devices', 'Device'],
|
||||
DEVICE_FILE: ['Device Files', 'Device File'],
|
||||
DEVICE_USER: ['Device Users', 'Device User'],
|
||||
DEVICE_USER_CLIENT_STATE: ['Device Users Client States', 'Device User Client State'],
|
||||
DIRECTORY: ['Directories', 'Directory'],
|
||||
DISCOVERY_JSON_FILE: ['Discovery File', 'Discovery File'],
|
||||
DOCUMENT: ['Documents', 'Document'],
|
||||
DOMAIN: ['Domains', 'Domain'],
|
||||
DOMAIN_ALIAS: ['Domain Aliases', 'Domain Alias'],
|
||||
DOMAIN_CONTACT: ['Domain Contacts', 'Domain Contact'],
|
||||
DOMAIN_PEOPLE_CONTACT: ['Domain People Contacts', 'Domain People Contact'],
|
||||
DOMAIN_PROFILE: ['Domain Profiles', 'Domain Profile'],
|
||||
DRIVE_DISK_USAGE: ['Drive Disk Usages', 'Drive Disk Usage'],
|
||||
DRIVE_FILE: ['Drive Files', 'Drive File'],
|
||||
DRIVE_FILE_COMMENT: ['Drive File Comments', 'Drive File Comment'],
|
||||
DRIVE_FILE_ID: ['Drive File IDs', 'Drive File ID'],
|
||||
DRIVE_FILE_NAME: ['Drive File Names', 'Drive File Name'],
|
||||
DRIVE_FILE_REVISION: ['Drive File Revisions', 'Drive File Revision'],
|
||||
DRIVE_FILE_RENAMED: ['Drive Files Renamed', 'Drive File Renamed'],
|
||||
DRIVE_FILE_SHORTCUT: ['Drive File Shortcuts', 'Drive File Shortcut'],
|
||||
DRIVE_FILE_OR_FOLDER: ['Drive Files/Folders', 'Drive File/Folder'],
|
||||
DRIVE_FILE_OR_FOLDER_ACL: ['Drive File/Folder ACLs', 'Drive File/Folder ACL'],
|
||||
DRIVE_FILE_OR_FOLDER_ID: ['Drive File/Folder IDs', 'Drive File/Folder ID'],
|
||||
DRIVE_FOLDER: ['Drive Folders', 'Drive Folder'],
|
||||
DRIVE_FOLDER_ID: ['Drive Folder IDs', 'Drive Folder ID'],
|
||||
DRIVE_FOLDER_NAME: ['Drive Folder Names', 'Drive Folder Name'],
|
||||
DRIVE_FOLDER_PATH: ['Drive Folder Paths', 'Drive Folder Path'],
|
||||
DRIVE_FOLDER_RENAMED: ['Drive Folders Renamed', 'Drive Folder Renamed'],
|
||||
DRIVE_FOLDER_SHORTCUT: ['Drive Folder Shortcuts', 'Drive Folder Shortcut'],
|
||||
DRIVE_ORPHAN_FILE_OR_FOLDER: ['Drive Orphan Files/Folders', 'Drive Orphan File/Folder'],
|
||||
DRIVE_PARENT_FOLDER: ['Drive Parent Folders', 'Drive Parent Folder'],
|
||||
DRIVE_PARENT_FOLDER_ID: ['Drive Parent Folder IDs', 'Drive Parent Folder ID'],
|
||||
DRIVE_PARENT_FOLDER_REFERENCE: ['Drive Parent Folder References', 'Drive Parent Folder Reference'],
|
||||
DRIVE_PATH: ['Drive Paths', 'Drive Path'],
|
||||
DRIVE_SETTINGS: ['Drive Settings', 'Drive Settings'],
|
||||
DRIVE_SHORTCUT: ['Drive Shortcuts', 'Drive Shortcut'],
|
||||
DRIVE_SHORTCUT_ID: ['Drive Shortcut IDs', 'Drive Shortcut ID'],
|
||||
DRIVE_3PSHORTCUT: ['Drive 3rd Party Shortcuts', 'Drive 3rd Party Shortcut'],
|
||||
DRIVE_TRASH: ['Drive Trash', 'Drive Trash'],
|
||||
EMAIL: ['Email Addresses', 'Email Address'],
|
||||
EMAIL_ALIAS: ['Email Aliases', 'Email Alias'],
|
||||
EMAIL_SETTINGS: ['Email Settings', 'Email Settings'],
|
||||
END_TIME: ['End Times', 'End Time'],
|
||||
ENTITY: ['Entities', 'Entity'],
|
||||
EVENT: ['Events', 'Event'],
|
||||
EVENT_BIRTHDAY: ['Borthday Events', 'Birthday Event'],
|
||||
EVENT_FOCUSTIME: ['Focus Time Events', 'Focus Time Event'],
|
||||
EVENT_OUTOFOFFICE: ['Out of Office Events', 'Out of Office Event'],
|
||||
EVENT_WORKINGLOCATION: ['Working Location Events', 'Working Location Event'],
|
||||
FEATURE: ['Features', 'Feature'],
|
||||
FIELD: ['Fields', 'Field'],
|
||||
FILE: ['Files', 'File'],
|
||||
FILE_PARENT_TREE: ['File Parent Trees', 'File Parent Tree'],
|
||||
FILTER: ['Filters', 'Filter'],
|
||||
FORM: ['Forms', 'Form'],
|
||||
FORM_RESPONSE: ['Form Responses', 'Form Response'],
|
||||
FORWARD_ENABLED: ['Forward Enabled', 'Forward Enabled'],
|
||||
FORWARDING_ADDRESS: ['Forwarding Addresses', 'Forwarding Address'],
|
||||
GCP_FOLDER: ['GCP Folders', 'GCP Folder'],
|
||||
GCP_FOLDER_NAME: ['GCP Folder Names', 'GCP Folder Name'],
|
||||
GMAIL_PROFILE: ['Gmail Profile', 'Gmail Profile'],
|
||||
GROUP: ['Groups', 'Group'],
|
||||
GROUP_ALIAS: ['Group Aliases', 'Group Alias'],
|
||||
GROUP_EMAIL: ['Group Emails', 'Group Email'],
|
||||
GROUP_MEMBERSHIP: ['Group Memberships', 'Group Membership'],
|
||||
GROUP_MEMBERSHIP_TREE: ['Group Membership Trees', 'Group Membership Tree'],
|
||||
GROUP_SETTINGS: ['Group Settings', 'Group Settings'],
|
||||
GROUP_TREE: ['Group Trees', 'Group Tree'],
|
||||
GUARDIAN: ['Guardians', 'Guardian'],
|
||||
GUARDIAN_INVITATION: ['Guardian Invitations', 'Guardian Invitation'],
|
||||
GUARDIAN_AND_INVITATION: ['Guardians and Invitations', 'Guardian and Invitation'],
|
||||
IAM_POLICY: ['IAM Policies', 'IAM Policy'],
|
||||
IMAP_ENABLED: ['IMAP Enabled', 'IMAP Enabled'],
|
||||
INBOUND_SSO_ASSIGNMENT: ['Inbound SSO Assignments', 'Inbound SSO Assignment'],
|
||||
INBOUND_SSO_CREDENTIALS: ['Inbound SSO Credentials', 'Inbound SSO Credential'],
|
||||
INBOUND_SSO_PROFILE: ['Inbound SSO Profiles', 'Inbound SSO Profile'],
|
||||
INSTANCE: ['Instances', 'Instance'],
|
||||
ISSUER_CN: ['Issuer CNs', 'Issuer CN'],
|
||||
ITEM: ['Items', 'Item'],
|
||||
KEYBOARD_SHORTCUTS_ENABLED: ['Keyboard Shortcuts Enabled', 'Keyboard Shortcuts Enabled'],
|
||||
LABEL: ['Labels', 'Label'],
|
||||
LABEL_ID: ['Label IDs', 'Label ID'],
|
||||
LANGUAGE: ['Languages', 'Language'],
|
||||
LICENSE: ['Licenses', 'License'],
|
||||
LOCATION: ['Locations', 'Location'],
|
||||
LOOKERSTUDIO_ASSET: ['Looker Studio Assets', 'Looker Studio Asset'],
|
||||
LOOKERSTUDIO_ASSET_DATASOURCE: ['Looker Studio DATA_SOURCE Assets', 'Looker Studio DATA_SOURCE Asset'],
|
||||
LOOKERSTUDIO_ASSETID: ['Looker Studio Asset IDs', 'Looker Studio Asset ID'],
|
||||
LOOKERSTUDIO_ASSET_REPORT: ['Looker Studio REPORT Assets', 'Looker Studio REPORT Asset'],
|
||||
LOOKERSTUDIO_PERMISSION: ['Looker Studio Permissions', 'Looker Studio Permission'],
|
||||
MD5HASH: ['MD5 hash', 'MD5 Hash'],
|
||||
MEET_SPACE: ['Meet Spaces', 'Meet Space'],
|
||||
MEET_CONFERENCE: ['Meet Conferences', 'Meet Conference'],
|
||||
MEET_PARTICIPANT: ['Meet Participants', 'Meet Participant'],
|
||||
MEET_RECORDING: ['Meet Recordings', 'Meet Recording'],
|
||||
MEET_TRANSCRIPT: ['Meet Transcripts', 'Meet Transcript'],
|
||||
MEMBER: ['Members', 'Member'],
|
||||
MEMBER_NOT_ARCHIVED: ['Members (Not Archived)', 'Member (Not Archived)'],
|
||||
MEMBER_ARCHIVED: ['Members (Archived)', 'Member (Archived)'],
|
||||
MEMBER_NOT_SUSPENDED: ['Members (Not Suspended)', 'Member (Not Suspended)'],
|
||||
MEMBER_SUSPENDED: ['Members (Suspended)', 'Member (Suspended)'],
|
||||
MEMBER_NOT_SUSPENDED_NOT_ARCHIVED: ['Members (Not Suspended & Not Archived)', 'Member (Not Suspended & Not Archived)'],
|
||||
MEMBER_SUSPENDED_ARCHIVED: ['Members (Suspended & Archived)', 'Member (Suspended & Archived)'],
|
||||
MEMBER_RESTRICTION: ['Member Restrictions', 'Member Restriction'],
|
||||
MEMBER_URI: ['Member URIs', 'Member URI'],
|
||||
MEMBERSHIP_TREE: ['Membership Trees', 'Membership Tree'],
|
||||
MESSAGE: ['Messages', 'Message'],
|
||||
MIMETYPE: ['MIME Types', 'MIME Type'],
|
||||
MOBILE_DEVICE: ['Mobile Devices', 'Mobile Device'],
|
||||
NAME: ['Names', 'Name'],
|
||||
NOTE: ['Notes', 'Note'],
|
||||
NOTE_ACL: ['Note ACLs', 'Note ACL'],
|
||||
NOTES_ACLS: ["'Note's ACLs", "Note's ACLs"],
|
||||
NONEDITABLE_ALIAS: ['Non-Editable Aliases', 'Non-Editable Alias'],
|
||||
OAUTH2_TXT_FILE: ['Client OAuth2 File', 'Client OAuth2 File'],
|
||||
OAUTH2SERVICE_JSON_FILE: ['Service Account OAuth2 File', 'Service Account OAuth2 File'],
|
||||
ORGANIZATIONAL_UNIT: ['Organizational Units', 'Organizational Unit'],
|
||||
OTHER_CONTACT: ['Other Contacts', 'Other Contact'],
|
||||
OWNER: ['Owners', 'Owner'],
|
||||
OWNER_ID: ['Owner IDs', 'Owner ID'],
|
||||
PAGE_SIZE: ['Page Size', 'Page Size'],
|
||||
PARENT_ORGANIZATIONAL_UNIT: ['Parent Organizational Units', 'Parent Organizational Unit'],
|
||||
PARTICIPANT: ['Participants', 'Participant'],
|
||||
PEOPLE_CONTACT: ['People Contacts', 'Person Contact'],
|
||||
PEOPLE_CONTACT_GROUP: ['People Contact Groups', 'People Contact Group'],
|
||||
PEOPLE_PHOTO: ['People Photos', 'Person Photo'],
|
||||
PEOPLE_PROFILE: ['People Profiles', 'People Profile'],
|
||||
PERMISSION: ['Permissions', 'Permission'],
|
||||
PERMISSION_ID: ['Permission IDs', 'Permission ID'],
|
||||
PERMITTEE: ['Permittees', 'Permittee'],
|
||||
PERSONAL_DEVICE: ['Personal Devices', 'Personal Device'],
|
||||
PHOTO: ['Photos', 'Photo'],
|
||||
POLICY: ['Policies', 'Policy'],
|
||||
POP_ENABLED: ['POP Enabled', 'POP Enabled'],
|
||||
PRESENTATION: ['Presentations', 'Presentation'],
|
||||
PRINTER: ['Printers', 'Printer'],
|
||||
PRINTER_ID: ['Printer IDs', 'Printer ID'],
|
||||
PRINTER_MODEL: ['Printer Models', 'Printer Model'],
|
||||
PRIVILEGE: ['Privileges', 'Privilege'],
|
||||
PRODUCT: ['Products', 'Product'],
|
||||
PROFILE_SHARING_ENABLED: ['Profile Sharing Enabled', 'Profile Sharing Enabled'],
|
||||
PROJECT: ['Projects', 'Project'],
|
||||
PROJECT_FOLDER: ['Project Folders', 'Project Folder'],
|
||||
PROJECT_ID: ['Project IDs', 'Project ID'],
|
||||
PUBLIC_KEY: ['Public Key', 'Public Key'],
|
||||
QUERY: ['Queries', 'Query'],
|
||||
RECIPIENT: ['Recipients', 'Recipient'],
|
||||
RECIPIENT_BCC: ['Recipients (BCC)', 'Recipient (BCC)'],
|
||||
RECIPIENT_CC: ['Recipients (CC)', 'Recipient (CC)'],
|
||||
REPORT: ['Reports', 'Report'],
|
||||
REQUEST_ID: ['Request IDs', 'Request ID'],
|
||||
RESOURCE_CALENDAR: ['Resource Calendars', 'Resource Calendar'],
|
||||
RESOURCE_ID: ['Resource IDs', 'Resource ID'],
|
||||
ROLE: ['Roles', 'Role'],
|
||||
ROW: ['Rows', 'Row'],
|
||||
SCOPE: ['Scopes', 'Scope'],
|
||||
SECTION: ['Sections', 'Section'],
|
||||
SENDAS_ADDRESS: ['SendAs Addresses', 'SendAs Address'],
|
||||
SENDER: ['Senders', 'Sender'],
|
||||
SERVICE: ['Services', 'Service'],
|
||||
SHAREDDRIVE: ['Shared Drives', 'Shared Drive'],
|
||||
SHAREDDRIVE_ACL: ['Shared Drive ACLs', 'Shared Drive ACL'],
|
||||
SHAREDDRIVE_FOLDER: ['Shared Drive Folders', 'Shared Drive Folder'],
|
||||
SHAREDDRIVE_ID: ['Shared Drive IDs', 'Shared Drive ID'],
|
||||
SHAREDDRIVE_NAME: ['Shared Drive Names', 'Shared Drive Name'],
|
||||
SHAREDDRIVE_THEME: ['Shared Drive Themes', 'Shared Drive Theme'],
|
||||
SHEET: ['Sheets', 'Sheet'],
|
||||
SHEET_ID: ['Sheet IDs', 'Sheet ID'],
|
||||
SIGNATURE: ['Signatures', 'Signature'],
|
||||
SITE: ['Sites', 'Site'],
|
||||
SITE_ACL: ['Site ACLs', 'Site ACL'],
|
||||
SIZE: ['Sizes', 'Size'],
|
||||
SKU: ['SKUs', 'SKU'],
|
||||
SMIME_ID: ['S/MIME Certificate IDs', 'S/MIME Certificate ID'],
|
||||
SNIPPETS_ENABLED: ['Preview Snippets Enabled', 'Preview Snippets Enabled'],
|
||||
SSO_KEY: ['SSO Key', 'SSO Key'],
|
||||
SSO_SETTINGS: ['SSO Settings', 'SSO Settings'],
|
||||
SOURCE_USER: ['Source Users', 'Source User'],
|
||||
SPREADSHEET: ['Spreadsheets', 'Spreadsheet'],
|
||||
SPREADSHEET_RANGE: ['Spreadsheet Ranges', 'Spreadsheet Range'],
|
||||
START_TIME: ['Start Times', 'Start Time'],
|
||||
STATUS: ['Status', 'Status'],
|
||||
STUDENT: ['Students', 'Student'],
|
||||
SUBSCRIPTION: ['Subscriptions', 'Subscription'],
|
||||
SVCACCT: ['Service Accounts', 'Service Account'],
|
||||
SVCACCT_KEY: ['Service Account Keys', 'Service Account Key'],
|
||||
TARGET_USER: ['Target Users', 'Target User'],
|
||||
TASK: ['Tasks', 'Task'],
|
||||
TASKLIST: ['Tasklists', 'Tasklist'],
|
||||
TEACHER: ['Teachers', 'Teacher'],
|
||||
THREAD: ['Threads', 'Thread'],
|
||||
TRANSFER_APPLICATION: ['Transfer Applications', 'Transfer Application'],
|
||||
TRANSFER_ID: ['Transfer IDs', 'Transfer ID'],
|
||||
TRANSFER_REQUEST: ['Transfer Requests', 'Transfer Request'],
|
||||
TRASHED_EVENT: ['Trashed Events', 'Trashed Event'],
|
||||
TRUSTED_APPLICATION: ['Trusted Applications', 'Trusted Application'],
|
||||
TYPE: ['Types', 'Type'],
|
||||
UNICODE_ENCODING_ENABLED: ['UTF-8 Encoding Enabled', 'UTF-8 Encoding Enabled'],
|
||||
UNIQUE_ID: ['Unique IDs', 'Unique ID'],
|
||||
URL: ['URLs', 'URL'],
|
||||
USER: ['Users', 'User'],
|
||||
USER_ALIAS: ['User Aliases', 'User Alias'],
|
||||
USER_EMAIL: ['User Emails', 'User Email'],
|
||||
USER_INVITATION: ['User Invitations', 'User Invitation'],
|
||||
USER_NOT_SUSPENDED: ['Users (Not suspended)', 'User (Not suspended)'],
|
||||
USER_SCHEMA: ['Schemas', 'Schema'],
|
||||
USER_SUSPENDED: ['Users (Suspended)', 'User (Suspended)'],
|
||||
VACATION: ['Vacation', 'Vacation'],
|
||||
VACATION_ENABLED: ['Vacation Enabled', 'Vacation Enabled'],
|
||||
VALUE: ['Values', 'Value'],
|
||||
VAULT_EXPORT: ['Vault Exports', 'Vault Export'],
|
||||
VAULT_HOLD: ['Vault Holds', 'Vault Hold'],
|
||||
VAULT_MATTER: ['Vault Matters', 'Vault Matter'],
|
||||
VAULT_MATTER_ARTIFACT: ['Vault Matter Artifacts', 'Vault Matter Artifact'],
|
||||
VAULT_MATTER_ID: ['Vault Matter IDs', 'Vault Matter ID'],
|
||||
VAULT_OPERATION: ['Vault Operations', 'Vault Operation'],
|
||||
VAULT_QUERY: ['Vault Queries', 'Vault Query'],
|
||||
WEBCLIPS_ENABLED: ['Web Clips Enabled', 'Web Clips Enabled'],
|
||||
YOUTUBE_CHANNEL: ['YouTube Channels', 'YouTube Channel'],
|
||||
ROLE_MANAGER: ['Managers', 'Manager'],
|
||||
ROLE_MEMBER: ['Members', 'Member'],
|
||||
ROLE_OWNER: ['Owners', 'Owner'],
|
||||
ROLE_ALL: ['Members, Managers, Owners', 'Member, Manager, Owner'],
|
||||
ROLE_USER: ['Users', 'User'],
|
||||
ROLE_MANAGER_MEMBER: ['Members, Managers', 'Member, Manager'],
|
||||
ROLE_MANAGER_OWNER: ['Managers, Owners', 'Manager, Owner'],
|
||||
ROLE_MEMBER_OWNER: ['Members, Owners', 'Member, Owner'],
|
||||
ROLE_MANAGER_MEMBER_OWNER: ['Members, Managers, Owners', 'Member, Manager, Owner'],
|
||||
ROLE_PUBLIC: ['Public', 'Public'],
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.entityType = None
|
||||
self.forWhom = None
|
||||
self.preQualifier = ''
|
||||
self.postQualifier = ''
|
||||
|
||||
def SetGetting(self, entityType):
|
||||
self.entityType = entityType
|
||||
self.preQualifier = self.postQualifier = ''
|
||||
|
||||
def SetGettingQuery(self, entityType, query):
|
||||
self.entityType = entityType
|
||||
self.preQualifier = f' that match query ({query})'
|
||||
self.postQualifier = f' that matched query ({query})'
|
||||
|
||||
def SetGettingQualifier(self, entityType, qualifier):
|
||||
self.entityType = entityType
|
||||
self.preQualifier = self.postQualifier = qualifier
|
||||
|
||||
def Getting(self):
|
||||
return self.entityType
|
||||
|
||||
def GettingPreQualifier(self):
|
||||
return self.preQualifier
|
||||
|
||||
def GettingPostQualifier(self):
|
||||
return self.postQualifier
|
||||
|
||||
def SetGettingForWhom(self, forWhom):
|
||||
self.forWhom = forWhom
|
||||
|
||||
def GettingForWhom(self):
|
||||
return self.forWhom
|
||||
|
||||
def Choose(self, entityType, count):
|
||||
return self._NAMES[entityType][[0, 1][count == 1]]
|
||||
|
||||
def ChooseGetting(self, count):
|
||||
return self._NAMES[self.entityType][[0, 1][count == 1]]
|
||||
|
||||
def Plural(self, entityType):
|
||||
return self._NAMES[entityType][0]
|
||||
|
||||
def PluralGetting(self):
|
||||
return self._NAMES[self.entityType][0]
|
||||
|
||||
def Singular(self, entityType):
|
||||
return self._NAMES[entityType][1]
|
||||
|
||||
def SingularGetting(self):
|
||||
return self._NAMES[self.entityType][1]
|
||||
|
||||
def MayTakeTime(self, entityType):
|
||||
if entityType:
|
||||
return f', may take some time on a large {self.Singular(entityType)}...'
|
||||
return ''
|
||||
|
||||
def FormatEntityValueList(self, entityValueList):
|
||||
evList = []
|
||||
for j in range(0, len(entityValueList), 2):
|
||||
evList.append(self.Singular(entityValueList[j]))
|
||||
evList.append(entityValueList[j+1])
|
||||
return evList
|
||||
|
||||
def TypeMessage(self, entityType, message):
|
||||
return f'{self.Singular(entityType)}: {message}'
|
||||
|
||||
def TypeName(self, entityType, entityName):
|
||||
return f'{self.Singular(entityType)}: {entityName}'
|
||||
|
||||
def TypeNameMessage(self, entityType, entityName, message):
|
||||
return f'{self.Singular(entityType)}: {entityName} {message}'
|
||||
821
src/gam/gamlib/glgapi.py
Normal file
821
src/gam/gamlib/glgapi.py
Normal file
@@ -0,0 +1,821 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2024 Ross Scroggs All Rights Reserved.
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""GAM GAPI resources
|
||||
|
||||
"""
|
||||
# callGAPI throw reasons
|
||||
ABORTED = 'aborted'
|
||||
ABUSIVE_CONTENT_RESTRICTION = 'abusiveContentRestriction'
|
||||
ACCESS_NOT_CONFIGURED = 'accessNotConfigured'
|
||||
ALREADY_EXISTS = 'alreadyExists'
|
||||
APPLY_LABEL_FORBIDDEN = 'applyLabelForbidden'
|
||||
AUTH_ERROR = 'authError'
|
||||
BACKEND_ERROR = 'backendError'
|
||||
BAD_GATEWAY = 'badGateway'
|
||||
BAD_REQUEST = 'badRequest'
|
||||
CANNOT_ADD_PARENT = 'cannotAddParent'
|
||||
CANNOT_CHANGE_ORGANIZER = 'cannotChangeOrganizer'
|
||||
CANNOT_CHANGE_ORGANIZER_OF_INSTANCE = 'cannotChangeOrganizerOfInstance'
|
||||
CANNOT_CHANGE_OWN_ACL = 'cannotChangeOwnAcl'
|
||||
CANNOT_CHANGE_OWNER_ACL = 'cannotChangeOwnerAcl'
|
||||
CANNOT_CHANGE_OWN_PRIMARY_SUBSCRIPTION = 'cannotChangeOwnPrimarySubscription'
|
||||
CANNOT_COPY_FILE = 'cannotCopyFile'
|
||||
CANNOT_DELETE_ONLY_REVISION = 'cannotDeleteOnlyRevision'
|
||||
CANNOT_DELETE_PRIMARY_CALENDAR = 'cannotDeletePrimaryCalendar'
|
||||
CANNOT_DELETE_PRIMARY_SENDAS = 'cannotDeletePrimarySendAs'
|
||||
CANNOT_DELETE_RESOURCE_WITH_CHILDREN = 'cannotDeleteResourceWithChildren'
|
||||
CANNOT_MODIFY_INHERITED_TEAMDRIVE_PERMISSION = 'cannotModifyInheritedTeamDrivePermission'
|
||||
CANNOT_MODIFY_RESTRICTED_LABEL = 'cannotModifyRestrictedLabel'
|
||||
CANNOT_MODIFY_VIEWERS_CAN_COPY_CONTENT = 'cannotModifyViewersCanCopyContent'
|
||||
CANNOT_MOVE_TRASHED_ITEM_INTO_TEAMDRIVE = 'cannotMoveTrashedItemIntoTeamDrive'
|
||||
CANNOT_MOVE_TRASHED_ITEM_OUT_OF_TEAMDRIVE = 'cannotMoveTrashedItemOutOfTeamDrive'
|
||||
CANNOT_REMOVE_OWNER = 'cannotRemoveOwner'
|
||||
CANNOT_SET_EXPIRATION = 'cannotSetExpiration'
|
||||
CANNOT_SHARE_GROUPS_WITHLINK = 'cannotShareGroupsWithLink'
|
||||
CANNOT_SHARE_USERS_WITHLINK = 'cannotShareUsersWithLink'
|
||||
CANNOT_SHARE_TEAMDRIVE_TOPFOLDER_WITH_ANYONEORDOMAINS = 'cannotShareTeamDriveTopFolderWithAnyoneOrDomains'
|
||||
CANNOT_SHARE_TEAMDRIVE_WITH_NONGOOGLE_ACCOUNTS = 'cannotShareTeamDriveWithNonGoogleAccounts'
|
||||
CANNOT_UPDATE_PERMISSION = 'cannotUpdatePermission'
|
||||
CONDITION_NOT_MET = 'conditionNotMet'
|
||||
CONFLICT = 'conflict'
|
||||
CONTENT_OWNER_ACCOUNT_NOT_FOUND = 'contentOwnerAccountNotFound'
|
||||
CROSS_DOMAIN_MOVE_RESTRICTION = 'crossDomainMoveRestriction'
|
||||
CUSTOMER_EXCEEDED_ROLE_ASSIGNMENTS_LIMIT = 'CUSTOMER_EXCEEDED_ROLE_ASSIGNMENTS_LIMIT'
|
||||
CUSTOMER_NOT_FOUND = 'customerNotFound'
|
||||
CYCLIC_MEMBERSHIPS_NOT_ALLOWED = 'cyclicMembershipsNotAllowed'
|
||||
DAILY_LIMIT_EXCEEDED = 'dailyLimitExceeded'
|
||||
DELETED = 'deleted'
|
||||
DELETED_USER_NOT_FOUND = 'deletedUserNotFound'
|
||||
DOMAIN_ALIAS_NOT_FOUND = 'domainAliasNotFound'
|
||||
DOMAIN_CANNOT_USE_APIS = 'domainCannotUseApis'
|
||||
DOMAIN_NOT_FOUND = 'domainNotFound'
|
||||
DOMAIN_NOT_VERIFIED_SECONDARY = 'domainNotVerifiedSecondary'
|
||||
DOMAIN_POLICY = 'domainPolicy'
|
||||
DOWNLOAD_QUOTA_EXCEEDED = 'downloadQuotaExceeded'
|
||||
DUPLICATE = 'duplicate'
|
||||
EVENT_DURATION_EXCEEDS_LIMIT = 'eventDurationExceedsLimit'
|
||||
EXPIRATION_DATE_NOT_ALLOWED_FOR_SHARED_DRIVE_MEMBERS = 'expirationDateNotAllowedForSharedDriveMembers'
|
||||
FAILED_PRECONDITION = 'failedPrecondition'
|
||||
FIELD_IN_USE = 'fieldInUse'
|
||||
FIELD_NOT_WRITABLE = 'fieldNotWritable'
|
||||
FILE_NEVER_WRITABLE = 'fileNeverWritable'
|
||||
FILE_NOT_FOUND = 'fileNotFound'
|
||||
FILE_ORGANIZER_NOT_YET_ENABLED_FOR_THIS_TEAMDRIVE = 'fileOrganizerNotYetEnabledForThisTeamDrive'
|
||||
FILE_ORGANIZER_ON_FOLDERS_IN_SHARED_DRIVE_ONLY = 'fileOrganizerOnFoldersInSharedDriveOnly'
|
||||
FILE_ORGANIZER_ON_NON_TEAMDRIVE_NOT_SUPPORTED = 'fileOrganizerOnNonTeamDriveNotSupported'
|
||||
FILE_OWNER_NOT_MEMBER_OF_TEAMDRIVE = 'fileOwnerNotMemberOfTeamDrive'
|
||||
FILE_OWNER_NOT_MEMBER_OF_WRITER_DOMAIN = 'fileOwnerNotMemberOfWriterDomain'
|
||||
FILE_WRITER_TEAMDRIVE_MOVE_IN_DISABLED = 'fileWriterTeamDriveMoveInDisabled'
|
||||
FORBIDDEN = 'forbidden'
|
||||
GATEWAY_TIMEOUT = 'gatewayTimeout'
|
||||
GROUP_NOT_FOUND = 'groupNotFound'
|
||||
ILLEGAL_ACCESS_ROLE_FOR_DEFAULT = 'illegalAccessRoleForDefault'
|
||||
INSUFFICIENT_ADMINISTRATOR_PRIVILEGES = 'insufficientAdministratorPrivileges'
|
||||
INSUFFICIENT_ARCHIVED_USER_LICENSES = 'insufficientArchivedUserLicenses'
|
||||
INSUFFICIENT_FILE_PERMISSIONS = 'insufficientFilePermissions'
|
||||
INSUFFICIENT_PARENT_PERMISSIONS = 'insufficientParentPermissions'
|
||||
INSUFFICIENT_PERMISSIONS = 'insufficientPermissions'
|
||||
INTERNAL_ERROR = 'internalError'
|
||||
INVALID = 'invalid'
|
||||
INVALID_ARGUMENT = 'invalidArgument'
|
||||
INVALID_ATTRIBUTE_VALUE = 'invalidAttributeValue'
|
||||
INVALID_CUSTOMER_ID = 'invalidCustomerId'
|
||||
INVALID_INPUT = 'invalidInput'
|
||||
INVALID_LINK_VISIBILITY = 'invalidLinkVisibility'
|
||||
INVALID_MEMBER = 'invalidMember'
|
||||
INVALID_MESSAGE_ID = 'invalidMessageId'
|
||||
INVALID_ORGUNIT = 'invalidOrgunit'
|
||||
INVALID_ORGUNIT_NAME = 'invalidOrgunitName'
|
||||
INVALID_OWNERSHIP_TRANSFER = 'invalidOwnershipTransfer'
|
||||
INVALID_PARAMETER = 'invalidParameter'
|
||||
INVALID_PARENT_ORGUNIT = 'invalidParentOrgunit'
|
||||
INVALID_QUERY = 'invalidQuery'
|
||||
INVALID_RESOURCE = 'invalidResource'
|
||||
INVALID_SCHEMA_VALUE = 'invalidSchemaValue'
|
||||
INVALID_SCOPE_VALUE = 'invalidScopeValue'
|
||||
INVALID_SHARING_REQUEST = 'invalidSharingRequest'
|
||||
LABEL_MULTIPLE_VALUES_FOR_SINGULAR_FIELD = 'labelMultipleValuesForSingularField'
|
||||
LABEL_MUTATION_FORBIDDEN = 'labelMutationForbidden'
|
||||
LABEL_MUTATION_ILLEGAL_SELECTION = 'labelMutationIllegalSelection'
|
||||
LABEL_MUTATION_UNKNOWN_FIELD = 'labelMutationUnknownField'
|
||||
LIMIT_EXCEEDED = 'limitExceeded'
|
||||
LOGIN_REQUIRED = 'loginRequired'
|
||||
MAIL_SERVICE_NOT_ENABLED = 'mailServiceNotEnabled'
|
||||
MALFORMED_WORKING_LOCATION_EVENT = 'malformedWorkingLocationEvent'
|
||||
MEMBER_NOT_FOUND = 'memberNotFound'
|
||||
MYDRIVE_HIERARCHY_DEPTH_LIMIT_EXCEEDED = 'myDriveHierarchyDepthLimitExceeded'
|
||||
NO_LIST_TEAMDRIVES_ADMINISTRATOR_PRIVILEGE = 'noListTeamDrivesAdministratorPrivilege'
|
||||
NO_MANAGE_TEAMDRIVE_ADMINISTRATOR_PRIVILEGE = 'noManageTeamDriveAdministratorPrivilege'
|
||||
NOT_A_CALENDAR_USER = 'notACalendarUser'
|
||||
NOT_FOUND = 'notFound'
|
||||
NOT_IMPLEMENTED = 'notImplemented'
|
||||
NUM_CHILDREN_IN_NON_ROOT_LIMIT_EXCEEDED = 'numChildrenInNonRootLimitExceeded'
|
||||
OPERATION_NOT_SUPPORTED = 'operationNotSupported'
|
||||
ORGANIZER_ON_NON_TEAMDRIVE_NOT_SUPPORTED = 'organizerOnNonTeamDriveNotSupported'
|
||||
ORGANIZER_ON_NON_TEAMDRIVE_ITEM_NOT_SUPPORTED = 'organizerOnNonTeamDriveItemNotSupported'
|
||||
ORGUNIT_NOT_FOUND = 'orgunitNotFound'
|
||||
OWNER_ON_TEAMDRIVE_ITEM_NOT_SUPPORTED = 'ownerOnTeamDriveItemNotSupported'
|
||||
OWNERSHIP_CHANGE_ACROSS_DOMAIN_NOT_PERMITTED = 'ownershipChangeAcrossDomainNotPermitted'
|
||||
PARTICIPANT_IS_NEITHER_ORGANIZER_NOR_ATTENDEE = 'participantIsNeitherOrganizerNorAttendee'
|
||||
PERMISSION_DENIED = 'permissionDenied'
|
||||
PERMISSION_NOT_FOUND = 'permissionNotFound'
|
||||
PHOTO_NOT_FOUND = 'photoNotFound'
|
||||
PUBLISH_OUT_NOT_PERMITTED = 'publishOutNotPermitted'
|
||||
QUERY_REQUIRES_ADMIN_CREDENTIALS = 'queryRequiresAdminCredentials'
|
||||
QUOTA_EXCEEDED = 'quotaExceeded'
|
||||
RATE_LIMIT_EXCEEDED = 'rateLimitExceeded'
|
||||
REQUIRED = 'required'
|
||||
REQUIRED_ACCESS_LEVEL = 'requiredAccessLevel'
|
||||
RESOURCE_EXHAUSTED = 'resourceExhausted'
|
||||
RESOURCE_ID_NOT_FOUND = 'resourceIdNotFound'
|
||||
RESOURCE_NOT_FOUND = 'resourceNotFound'
|
||||
RESPONSE_PREPARATION_FAILURE = 'responsePreparationFailure'
|
||||
REVISION_DELETION_NOT_SUPPORTED = 'revisionDeletionNotSupported'
|
||||
REVISION_NOT_FOUND = 'revisionNotFound'
|
||||
REVISIONS_NOT_SUPPORTED = 'revisionsNotSupported'
|
||||
SERVICE_LIMIT = 'serviceLimit'
|
||||
SERVICE_NOT_AVAILABLE = 'serviceNotAvailable'
|
||||
SHARE_IN_NOT_PERMITTED = 'shareInNotPermitted'
|
||||
SHARE_OUT_NOT_PERMITTED = 'shareOutNotPermitted'
|
||||
SHARE_OUT_NOT_PERMITTED_TO_USER = 'shareOutNotPermittedToUser'
|
||||
SHARING_RATE_LIMIT_EXCEEDED = 'sharingRateLimitExceeded'
|
||||
SHORTCUT_TARGET_INVALID = 'shortcutTargetInvalid'
|
||||
STORAGE_QUOTA_EXCEEDED = 'storageQuotaExceeded'
|
||||
SYSTEM_ERROR = 'systemError'
|
||||
TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION = 'targetUserRoleLimitedByLicenseRestriction'
|
||||
TEAMDRIVE_ALREADY_EXISTS = 'teamDriveAlreadyExists'
|
||||
TEAMDRIVE_DOMAIN_USERS_ONLY_RESTRICTION = 'teamDriveDomainUsersOnlyRestriction'
|
||||
TEAMDRIVE_TEAM_MEMBERS_ONLY_RESTRICTION = 'teamDriveTeamMembersOnlyRestriction'
|
||||
TEAMDRIVE_FILE_LIMIT_EXCEEDED = 'teamDriveFileLimitExceeded'
|
||||
TEAMDRIVE_HIERARCHY_TOO_DEEP = 'teamDriveHierarchyTooDeep'
|
||||
TEAMDRIVE_MEMBERSHIP_REQUIRED = 'teamDriveMembershipRequired'
|
||||
TEAMDRIVES_FOLDER_MOVE_IN_NOT_SUPPORTED = 'teamDrivesFolderMoveInNotSupported'
|
||||
TEAMDRIVES_FOLDER_SHARING_NOT_SUPPORTED = 'teamDrivesFolderSharingNotSupported'
|
||||
TEAMDRIVES_PARENT_LIMIT = 'teamDrivesParentLimit'
|
||||
TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED = 'teamDrivesSharingRestrictionNotAllowed'
|
||||
TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED = 'teamDrivesShortcutFileNotSupported'
|
||||
TIME_RANGE_EMPTY = 'timeRangeEmpty'
|
||||
TRANSIENT_ERROR = 'transientError'
|
||||
UNKNOWN_ERROR = 'unknownError'
|
||||
UNSUPPORTED_LANGUAGE_CODE = 'unsupportedLanguageCode'
|
||||
UNSUPPORTED_SUPERVISED_ACCOUNT = 'unsupportedSupervisedAccount'
|
||||
UPLOAD_TOO_LARGE = 'uploadTooLarge'
|
||||
USER_CANNOT_CREATE_TEAMDRIVES = 'userCannotCreateTeamDrives'
|
||||
USER_ACCESS = 'userAccess'
|
||||
USER_NOT_FOUND = 'userNotFound'
|
||||
USER_RATE_LIMIT_EXCEEDED = 'userRateLimitExceeded'
|
||||
#
|
||||
DEFAULT_RETRY_REASONS = [QUOTA_EXCEEDED, RATE_LIMIT_EXCEEDED, SHARING_RATE_LIMIT_EXCEEDED, USER_RATE_LIMIT_EXCEEDED,
|
||||
BACKEND_ERROR, BAD_GATEWAY, GATEWAY_TIMEOUT, INTERNAL_ERROR, TRANSIENT_ERROR]
|
||||
SERVICE_NOT_AVAILABLE_RETRY_REASONS = [SERVICE_NOT_AVAILABLE]
|
||||
ACTIVITY_THROW_REASONS = [SERVICE_NOT_AVAILABLE, BAD_REQUEST]
|
||||
ALERT_THROW_REASONS = [SERVICE_NOT_AVAILABLE, AUTH_ERROR]
|
||||
CALENDAR_THROW_REASONS = [SERVICE_NOT_AVAILABLE, AUTH_ERROR, NOT_A_CALENDAR_USER]
|
||||
CIGROUP_CREATE_THROW_REASONS = [SERVICE_NOT_AVAILABLE, ALREADY_EXISTS, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, INVALID, INVALID_ARGUMENT, PERMISSION_DENIED, FAILED_PRECONDITION]
|
||||
CIGROUP_GET_THROW_REASONS = [SERVICE_NOT_AVAILABLE, NOT_FOUND, GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, BAD_REQUEST, INVALID, SYSTEM_ERROR, PERMISSION_DENIED]
|
||||
CIGROUP_LIST_THROW_REASONS = [SERVICE_NOT_AVAILABLE, RESOURCE_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, BAD_REQUEST, INVALID, INVALID_ARGUMENT, SYSTEM_ERROR, PERMISSION_DENIED]
|
||||
CIGROUP_LIST_USERKEY_THROW_REASONS = CIGROUP_LIST_THROW_REASONS+[INVALID_ARGUMENT]
|
||||
CIGROUP_UPDATE_THROW_REASONS = [SERVICE_NOT_AVAILABLE, NOT_FOUND, GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS,
|
||||
FORBIDDEN, BAD_REQUEST, INVALID, INVALID_INPUT, INVALID_ARGUMENT,
|
||||
SYSTEM_ERROR, PERMISSION_DENIED, FAILED_PRECONDITION]
|
||||
CIGROUP_RETRY_REASONS = [INVALID, SYSTEM_ERROR, SERVICE_NOT_AVAILABLE]
|
||||
CIMEMBERS_THROW_REASONS = [SERVICE_NOT_AVAILABLE, MEMBER_NOT_FOUND, INVALID_MEMBER]
|
||||
CISSO_CREATE_THROW_REASONS = [SERVICE_NOT_AVAILABLE, FAILED_PRECONDITION, NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, INVALID, INVALID_ARGUMENT, PERMISSION_DENIED, INTERNAL_ERROR]
|
||||
CISSO_GET_THROW_REASONS = [SERVICE_NOT_AVAILABLE, NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, BAD_REQUEST, INVALID, SYSTEM_ERROR, PERMISSION_DENIED, INTERNAL_ERROR]
|
||||
CISSO_LIST_THROW_REASONS = [SERVICE_NOT_AVAILABLE, NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, BAD_REQUEST, INVALID, SYSTEM_ERROR, PERMISSION_DENIED, INTERNAL_ERROR]
|
||||
CISSO_UPDATE_THROW_REASONS = [SERVICE_NOT_AVAILABLE, NOT_FOUND, FAILED_PRECONDITION, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS,
|
||||
FORBIDDEN, BAD_REQUEST, INVALID, INVALID_INPUT, INVALID_ARGUMENT,
|
||||
SYSTEM_ERROR, PERMISSION_DENIED, INTERNAL_ERROR]
|
||||
CONTACT_DELEGATE_THROW_REASONS = [SERVICE_NOT_AVAILABLE, BAD_REQUEST, FAILED_PRECONDITION, PERMISSION_DENIED, FORBIDDEN, INVALID_ARGUMENT]
|
||||
COURSE_ACCESS_THROW_REASONS = [NOT_FOUND, INSUFFICIENT_PERMISSIONS, PERMISSION_DENIED, FORBIDDEN, INVALID_ARGUMENT]
|
||||
DRIVE_USER_THROW_REASONS = [SERVICE_NOT_AVAILABLE, AUTH_ERROR, DOMAIN_POLICY]
|
||||
DRIVE_ACCESS_THROW_REASONS = DRIVE_USER_THROW_REASONS+[FILE_NOT_FOUND, FORBIDDEN, INTERNAL_ERROR, INSUFFICIENT_FILE_PERMISSIONS, UNKNOWN_ERROR, INVALID]
|
||||
DRIVE_COPY_THROW_REASONS = DRIVE_ACCESS_THROW_REASONS+[CANNOT_COPY_FILE, BAD_REQUEST, RESPONSE_PREPARATION_FAILURE, TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
|
||||
FIELD_NOT_WRITABLE, RATE_LIMIT_EXCEEDED, USER_RATE_LIMIT_EXCEEDED,
|
||||
STORAGE_QUOTA_EXCEEDED, TEAMDRIVE_FILE_LIMIT_EXCEEDED, TEAMDRIVE_HIERARCHY_TOO_DEEP]
|
||||
DRIVE_GET_THROW_REASONS = DRIVE_USER_THROW_REASONS+[FILE_NOT_FOUND, DOWNLOAD_QUOTA_EXCEEDED]
|
||||
DRIVE3_CREATE_ACL_THROW_REASONS = [BAD_REQUEST, INVALID, INVALID_SHARING_REQUEST, OWNERSHIP_CHANGE_ACROSS_DOMAIN_NOT_PERMITTED,
|
||||
CANNOT_SET_EXPIRATION, EXPIRATION_DATE_NOT_ALLOWED_FOR_SHARED_DRIVE_MEMBERS,
|
||||
NOT_FOUND, TEAMDRIVE_DOMAIN_USERS_ONLY_RESTRICTION, TEAMDRIVE_TEAM_MEMBERS_ONLY_RESTRICTION,
|
||||
TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION, INSUFFICIENT_ADMINISTRATOR_PRIVILEGES, SHARING_RATE_LIMIT_EXCEEDED,
|
||||
PUBLISH_OUT_NOT_PERMITTED, SHARE_IN_NOT_PERMITTED, SHARE_OUT_NOT_PERMITTED, SHARE_OUT_NOT_PERMITTED_TO_USER,
|
||||
CANNOT_SHARE_TEAMDRIVE_TOPFOLDER_WITH_ANYONEORDOMAINS,
|
||||
CANNOT_SHARE_TEAMDRIVE_WITH_NONGOOGLE_ACCOUNTS,
|
||||
OWNER_ON_TEAMDRIVE_ITEM_NOT_SUPPORTED,
|
||||
ORGANIZER_ON_NON_TEAMDRIVE_NOT_SUPPORTED,
|
||||
ORGANIZER_ON_NON_TEAMDRIVE_ITEM_NOT_SUPPORTED,
|
||||
FILE_ORGANIZER_NOT_YET_ENABLED_FOR_THIS_TEAMDRIVE,
|
||||
FILE_ORGANIZER_ON_FOLDERS_IN_SHARED_DRIVE_ONLY,
|
||||
FILE_ORGANIZER_ON_NON_TEAMDRIVE_NOT_SUPPORTED,
|
||||
TEAMDRIVES_FOLDER_SHARING_NOT_SUPPORTED, INVALID_LINK_VISIBILITY, ABUSIVE_CONTENT_RESTRICTION]
|
||||
DRIVE3_GET_ACL_REASONS = DRIVE_USER_THROW_REASONS+[FILE_NOT_FOUND, FORBIDDEN, INTERNAL_ERROR,
|
||||
INSUFFICIENT_ADMINISTRATOR_PRIVILEGES, INSUFFICIENT_FILE_PERMISSIONS,
|
||||
UNKNOWN_ERROR, INVALID]
|
||||
DRIVE3_UPDATE_ACL_THROW_REASONS = [BAD_REQUEST, INVALID_OWNERSHIP_TRANSFER, CANNOT_REMOVE_OWNER,
|
||||
CANNOT_SET_EXPIRATION, EXPIRATION_DATE_NOT_ALLOWED_FOR_SHARED_DRIVE_MEMBERS,
|
||||
OWNERSHIP_CHANGE_ACROSS_DOMAIN_NOT_PERMITTED,
|
||||
NOT_FOUND, TEAMDRIVE_DOMAIN_USERS_ONLY_RESTRICTION, TEAMDRIVE_TEAM_MEMBERS_ONLY_RESTRICTION,
|
||||
TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION, INSUFFICIENT_ADMINISTRATOR_PRIVILEGES, SHARING_RATE_LIMIT_EXCEEDED,
|
||||
PUBLISH_OUT_NOT_PERMITTED, SHARE_IN_NOT_PERMITTED, SHARE_OUT_NOT_PERMITTED, SHARE_OUT_NOT_PERMITTED_TO_USER,
|
||||
CANNOT_SHARE_TEAMDRIVE_TOPFOLDER_WITH_ANYONEORDOMAINS,
|
||||
CANNOT_SHARE_TEAMDRIVE_WITH_NONGOOGLE_ACCOUNTS,
|
||||
OWNER_ON_TEAMDRIVE_ITEM_NOT_SUPPORTED,
|
||||
ORGANIZER_ON_NON_TEAMDRIVE_NOT_SUPPORTED,
|
||||
ORGANIZER_ON_NON_TEAMDRIVE_ITEM_NOT_SUPPORTED,
|
||||
FILE_ORGANIZER_NOT_YET_ENABLED_FOR_THIS_TEAMDRIVE,
|
||||
FILE_ORGANIZER_ON_FOLDERS_IN_SHARED_DRIVE_ONLY,
|
||||
FILE_ORGANIZER_ON_NON_TEAMDRIVE_NOT_SUPPORTED,
|
||||
CANNOT_UPDATE_PERMISSION,
|
||||
CANNOT_MODIFY_INHERITED_TEAMDRIVE_PERMISSION,
|
||||
FIELD_NOT_WRITABLE, PERMISSION_NOT_FOUND]
|
||||
DRIVE3_DELETE_ACL_THROW_REASONS = [BAD_REQUEST, CANNOT_REMOVE_OWNER,
|
||||
CANNOT_MODIFY_INHERITED_TEAMDRIVE_PERMISSION,
|
||||
INSUFFICIENT_ADMINISTRATOR_PRIVILEGES, SHARING_RATE_LIMIT_EXCEEDED,
|
||||
NOT_FOUND, PERMISSION_NOT_FOUND]
|
||||
DRIVE3_MODIFY_LABEL_THROW_REASONS = DRIVE_USER_THROW_REASONS+[FILE_NOT_FOUND, NOT_FOUND, FORBIDDEN, INTERNAL_ERROR,
|
||||
FILE_NEVER_WRITABLE, APPLY_LABEL_FORBIDDEN,
|
||||
INSUFFICIENT_ADMINISTRATOR_PRIVILEGES, INSUFFICIENT_FILE_PERMISSIONS,
|
||||
UNKNOWN_ERROR, INVALID_INPUT, BAD_REQUEST,
|
||||
LABEL_MULTIPLE_VALUES_FOR_SINGULAR_FIELD, LABEL_MUTATION_FORBIDDEN,
|
||||
LABEL_MUTATION_ILLEGAL_SELECTION, LABEL_MUTATION_UNKNOWN_FIELD]
|
||||
DOCS_ACCESS_THROW_REASONS = DRIVE_USER_THROW_REASONS+[NOT_FOUND, PERMISSION_DENIED, FORBIDDEN, INTERNAL_ERROR, INSUFFICIENT_FILE_PERMISSIONS,
|
||||
BAD_REQUEST, INVALID, INVALID_ARGUMENT, FAILED_PRECONDITION]
|
||||
GMAIL_THROW_REASONS = [SERVICE_NOT_AVAILABLE, BAD_REQUEST]
|
||||
GMAIL_LIST_THROW_REASONS = [FAILED_PRECONDITION, PERMISSION_DENIED, INVALID, INVALID_ARGUMENT]
|
||||
GMAIL_SMIME_THROW_REASONS = [SERVICE_NOT_AVAILABLE, BAD_REQUEST, INVALID_ARGUMENT, FORBIDDEN, NOT_FOUND, PERMISSION_DENIED]
|
||||
GROUP_GET_THROW_REASONS = [GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, BAD_REQUEST, INVALID, SYSTEM_ERROR]
|
||||
GROUP_GET_RETRY_REASONS = [INVALID, SYSTEM_ERROR, SERVICE_NOT_AVAILABLE]
|
||||
GROUP_CREATE_THROW_REASONS = [DUPLICATE, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, INVALID, INVALID_INPUT]
|
||||
GROUP_UPDATE_THROW_REASONS = [GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, INVALID, INVALID_INPUT]
|
||||
GROUP_SETTINGS_THROW_REASONS = [NOT_FOUND, GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, SYSTEM_ERROR, PERMISSION_DENIED,
|
||||
INVALID, INVALID_ARGUMENT, INVALID_PARAMETER, INVALID_ATTRIBUTE_VALUE, INVALID_INPUT,
|
||||
SERVICE_LIMIT, SERVICE_NOT_AVAILABLE, AUTH_ERROR, REQUIRED]
|
||||
GROUP_SETTINGS_RETRY_REASONS = [INVALID, SERVICE_LIMIT, SERVICE_NOT_AVAILABLE]
|
||||
GROUP_LIST_THROW_REASONS = [RESOURCE_NOT_FOUND, DOMAIN_NOT_FOUND, FORBIDDEN, BAD_REQUEST]
|
||||
GROUP_LIST_USERKEY_THROW_REASONS = GROUP_LIST_THROW_REASONS+[INVALID_MEMBER, INVALID_INPUT]
|
||||
KEEP_THROW_REASONS = [SERVICE_NOT_AVAILABLE, BAD_REQUEST, PERMISSION_DENIED, INVALID_ARGUMENT, NOT_FOUND]
|
||||
LOOKERSTUDIO_THROW_REASONS = [INVALID_ARGUMENT, SERVICE_NOT_AVAILABLE, BAD_REQUEST, NOT_FOUND, PERMISSION_DENIED, INTERNAL_ERROR]
|
||||
MEMBERS_THROW_REASONS = [GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, INVALID, FORBIDDEN, SERVICE_NOT_AVAILABLE]
|
||||
MEMBERS_RETRY_REASONS = [SYSTEM_ERROR, SERVICE_NOT_AVAILABLE]
|
||||
ORGUNIT_GET_THROW_REASONS = [INVALID_ORGUNIT, ORGUNIT_NOT_FOUND, BACKEND_ERROR, BAD_REQUEST, INVALID_CUSTOMER_ID, LOGIN_REQUIRED]
|
||||
PEOPLE_ACCESS_THROW_REASONS = [SERVICE_NOT_AVAILABLE, FORBIDDEN, PERMISSION_DENIED]
|
||||
RESELLER_THROW_REASONS = [BAD_REQUEST, RESOURCE_NOT_FOUND, FORBIDDEN, INVALID]
|
||||
SHEETS_ACCESS_THROW_REASONS = DRIVE_USER_THROW_REASONS+[NOT_FOUND, PERMISSION_DENIED, FORBIDDEN, INTERNAL_ERROR, INSUFFICIENT_FILE_PERMISSIONS,
|
||||
BAD_REQUEST, INVALID, INVALID_ARGUMENT, FAILED_PRECONDITION]
|
||||
TASK_THROW_REASONS = [SERVICE_NOT_AVAILABLE, BAD_REQUEST, PERMISSION_DENIED, INVALID, NOT_FOUND, ACCESS_NOT_CONFIGURED]
|
||||
TASKLIST_THROW_REASONS = [SERVICE_NOT_AVAILABLE, BAD_REQUEST, PERMISSION_DENIED, INVALID, NOT_FOUND, ACCESS_NOT_CONFIGURED]
|
||||
USER_GET_THROW_REASONS = [USER_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, BAD_REQUEST, SYSTEM_ERROR]
|
||||
YOUTUBE_THROW_REASONS = [SERVICE_NOT_AVAILABLE, AUTH_ERROR, UNSUPPORTED_SUPERVISED_ACCOUNT, UNSUPPORTED_LANGUAGE_CODE, CONTENT_OWNER_ACCOUNT_NOT_FOUND]
|
||||
|
||||
REASON_MESSAGE_MAP = {
|
||||
ABORTED: [
|
||||
('Label name exists or conflicts', DUPLICATE),
|
||||
('The operation was aborted', ABORTED),
|
||||
],
|
||||
CONDITION_NOT_MET: [
|
||||
('Cyclic memberships not allowed', CYCLIC_MEMBERSHIPS_NOT_ALLOWED),
|
||||
('undelete', DELETED_USER_NOT_FOUND),
|
||||
],
|
||||
FAILED_PRECONDITION: [
|
||||
('Bad Request', BAD_REQUEST),
|
||||
('Mail service not enabled', SERVICE_NOT_AVAILABLE),
|
||||
],
|
||||
INVALID: [
|
||||
('userId', USER_NOT_FOUND),
|
||||
('memberKey', INVALID_MEMBER),
|
||||
('A system error has occurred', SYSTEM_ERROR),
|
||||
('Invalid attribute value', INVALID_ATTRIBUTE_VALUE),
|
||||
('Invalid Customer Id', INVALID_CUSTOMER_ID),
|
||||
('Invalid Input: INVALID_OU_ID', INVALID_ORGUNIT),
|
||||
('Invalid Input: custom_schema', INVALID_SCHEMA_VALUE),
|
||||
('Invalid Input: groupKey', INVALID_INPUT),
|
||||
('Invalid Input: resource', INVALID_RESOURCE),
|
||||
('Invalid Input:', INVALID_INPUT),
|
||||
('Invalid Input', INVALID_INPUT),
|
||||
('Invalid Org Unit', INVALID_ORGUNIT),
|
||||
('Invalid Ou Id', INVALID_ORGUNIT),
|
||||
('Invalid Ou Name', INVALID_ORGUNIT_NAME),
|
||||
('Invalid Parent Orgunit Id', INVALID_PARENT_ORGUNIT),
|
||||
('Invalid query', INVALID_QUERY),
|
||||
('Invalid scope value', INVALID_SCOPE_VALUE),
|
||||
('Invalid value', INVALID_INPUT),
|
||||
('New domain name is not a verified secondary domain', DOMAIN_NOT_VERIFIED_SECONDARY),
|
||||
('PermissionDenied', PERMISSION_DENIED),
|
||||
],
|
||||
INVALID_ARGUMENT: [
|
||||
('Cannot delete primary send-as', CANNOT_DELETE_PRIMARY_SENDAS),
|
||||
('Invalid id value', INVALID_MESSAGE_ID),
|
||||
('Invalid ids value', INVALID_MESSAGE_ID),
|
||||
],
|
||||
NOT_FOUND: [
|
||||
('userKey', USER_NOT_FOUND),
|
||||
('groupKey', GROUP_NOT_FOUND),
|
||||
('memberKey', MEMBER_NOT_FOUND),
|
||||
('photo', PHOTO_NOT_FOUND),
|
||||
('resource_id', RESOURCE_ID_NOT_FOUND),
|
||||
('resourceId', RESOURCE_ID_NOT_FOUND),
|
||||
('Customer doesn\'t exist', CUSTOMER_NOT_FOUND),
|
||||
('Domain alias does not exist', DOMAIN_ALIAS_NOT_FOUND),
|
||||
('Domain not found', DOMAIN_NOT_FOUND),
|
||||
('domain', DOMAIN_NOT_FOUND),
|
||||
('File not found', FILE_NOT_FOUND),
|
||||
('Org unit not found', ORGUNIT_NOT_FOUND),
|
||||
('Permission not found', PERMISSION_NOT_FOUND),
|
||||
('Resource Not Found', RESOURCE_NOT_FOUND),
|
||||
('Revision not found', REVISION_NOT_FOUND),
|
||||
('Shared Drive not found', NOT_FOUND),
|
||||
('Not Found', NOT_FOUND),
|
||||
],
|
||||
REQUIRED: [
|
||||
('Login Required', LOGIN_REQUIRED),
|
||||
('memberKey', MEMBER_NOT_FOUND),
|
||||
],
|
||||
RESOURCE_NOT_FOUND: [
|
||||
('resourceId', RESOURCE_ID_NOT_FOUND),
|
||||
],
|
||||
}
|
||||
|
||||
class aborted(Exception):
|
||||
pass
|
||||
class abusiveContentRestriction(Exception):
|
||||
pass
|
||||
class accessNotConfigured(Exception):
|
||||
pass
|
||||
class alreadyExists(Exception):
|
||||
pass
|
||||
class applyLabelForbidden(Exception):
|
||||
pass
|
||||
class authError(Exception):
|
||||
pass
|
||||
class backendError(Exception):
|
||||
pass
|
||||
class badRequest(Exception):
|
||||
pass
|
||||
class cannotAddParent(Exception):
|
||||
pass
|
||||
class cannotChangeOrganizer(Exception):
|
||||
pass
|
||||
class cannotChangeOrganizerOfInstance(Exception):
|
||||
pass
|
||||
class cannotChangeOwnAcl(Exception):
|
||||
pass
|
||||
class cannotChangeOwnerAcl(Exception):
|
||||
pass
|
||||
class cannotChangeOwnPrimarySubscription(Exception):
|
||||
pass
|
||||
class cannotCopyFile(Exception):
|
||||
pass
|
||||
class cannotDeleteOnlyRevision(Exception):
|
||||
pass
|
||||
class cannotDeletePrimaryCalendar(Exception):
|
||||
pass
|
||||
class cannotDeletePrimarySendAs(Exception):
|
||||
pass
|
||||
class cannotDeleteResourceWithChildren(Exception):
|
||||
pass
|
||||
class cannotModifyInheritedTeamDrivePermission(Exception):
|
||||
pass
|
||||
class cannotModifyRestrictedLabel(Exception):
|
||||
pass
|
||||
class cannotModifyViewersCanCopyContent(Exception):
|
||||
pass
|
||||
class cannotMoveTrashedItemIntoTeamDrive(Exception):
|
||||
pass
|
||||
class cannotMoveTrashedItemOutOfTeamDrive(Exception):
|
||||
pass
|
||||
class cannotRemoveOwner(Exception):
|
||||
pass
|
||||
class cannotSetExpiration(Exception):
|
||||
pass
|
||||
class cannotShareGroupsWithLink(Exception):
|
||||
pass
|
||||
class cannotShareUsersWithLink(Exception):
|
||||
pass
|
||||
class cannotShareTeamDriveTopFolderWithAnyoneOrDomains(Exception):
|
||||
pass
|
||||
class cannotShareTeamDriveWithNonGoogleAccounts(Exception):
|
||||
pass
|
||||
class cannotUpdatePermission(Exception):
|
||||
pass
|
||||
class conditionNotMet(Exception):
|
||||
pass
|
||||
class conflict(Exception):
|
||||
pass
|
||||
class contentOwnerAccountNotFound(Exception):
|
||||
pass
|
||||
class crossDomainMoveRestriction(Exception):
|
||||
pass
|
||||
class customerExceededRoleAssignmentsLimit(Exception):
|
||||
pass
|
||||
class customerNotFound(Exception):
|
||||
pass
|
||||
class cyclicMembershipsNotAllowed(Exception):
|
||||
pass
|
||||
class deleted(Exception):
|
||||
pass
|
||||
class deletedUserNotFound(Exception):
|
||||
pass
|
||||
class domainAliasNotFound(Exception):
|
||||
pass
|
||||
class domainCannotUseApis(Exception):
|
||||
pass
|
||||
class domainNotFound(Exception):
|
||||
pass
|
||||
class domainNotVerifiedSecondary(Exception):
|
||||
pass
|
||||
class domainPolicy(Exception):
|
||||
pass
|
||||
class downloadQuotaExceeded(Exception):
|
||||
pass
|
||||
class duplicate(Exception):
|
||||
pass
|
||||
class eventDurationExceedsLimit(Exception):
|
||||
pass
|
||||
class expirationDateNotAllowedForSharedDriveMembers(Exception):
|
||||
pass
|
||||
class failedPrecondition(Exception):
|
||||
pass
|
||||
class fieldInUse(Exception):
|
||||
pass
|
||||
class fieldNotWritable(Exception):
|
||||
pass
|
||||
class fileNeverWritable(Exception):
|
||||
pass
|
||||
class fileNotFound(Exception):
|
||||
pass
|
||||
class fileOrganizerNotYetEnabledForThisTeamDrive(Exception):
|
||||
pass
|
||||
class fileOrganizerOnFoldersInSharedDriveOnly(Exception):
|
||||
pass
|
||||
class fileOrganizerOnNonTeamDriveNotSupported(Exception):
|
||||
pass
|
||||
class fileOwnerNotMemberOfTeamDrive(Exception):
|
||||
pass
|
||||
class fileOwnerNotMemberOfWriterDomain(Exception):
|
||||
pass
|
||||
class fileWriterTeamDriveMoveInDisabled(Exception):
|
||||
pass
|
||||
class forbidden(Exception):
|
||||
pass
|
||||
class groupNotFound(Exception):
|
||||
pass
|
||||
class illegalAccessRoleForDefault(Exception):
|
||||
pass
|
||||
class insufficientAdministratorPrivileges(Exception):
|
||||
pass
|
||||
class insufficientArchivedUserLicenses(Exception):
|
||||
pass
|
||||
class insufficientFilePermissions(Exception):
|
||||
pass
|
||||
class insufficientParentPermissions(Exception):
|
||||
pass
|
||||
class insufficientPermissions(Exception):
|
||||
pass
|
||||
class internalError(Exception):
|
||||
pass
|
||||
class invalid(Exception):
|
||||
pass
|
||||
class invalidArgument(Exception):
|
||||
pass
|
||||
class invalidAttributeValue(Exception):
|
||||
pass
|
||||
class invalidCustomerId(Exception):
|
||||
pass
|
||||
class invalidInput(Exception):
|
||||
pass
|
||||
class invalidLinkVisibility(Exception):
|
||||
pass
|
||||
class invalidMember(Exception):
|
||||
pass
|
||||
class invalidMessageId(Exception):
|
||||
pass
|
||||
class invalidOrgunit(Exception):
|
||||
pass
|
||||
class invalidOrgunitName(Exception):
|
||||
pass
|
||||
class invalidOwnershipTransfer(Exception):
|
||||
pass
|
||||
class invalidParameter(Exception):
|
||||
pass
|
||||
class invalidParentOrgunit(Exception):
|
||||
pass
|
||||
class invalidQuery(Exception):
|
||||
pass
|
||||
class invalidResource(Exception):
|
||||
pass
|
||||
class invalidSchemaValue(Exception):
|
||||
pass
|
||||
class invalidScopeValue(Exception):
|
||||
pass
|
||||
class invalidSharingRequest(Exception):
|
||||
pass
|
||||
class labelMultipleValuesForSingularField(Exception):
|
||||
pass
|
||||
class labelMutationForbidden(Exception):
|
||||
pass
|
||||
class labelMutationIllegalSelection(Exception):
|
||||
pass
|
||||
class labelMutationUnknownField(Exception):
|
||||
pass
|
||||
class limitExceeded(Exception):
|
||||
pass
|
||||
class loginRequired(Exception):
|
||||
pass
|
||||
class mailServiceNotEnabled(Exception):
|
||||
pass
|
||||
class malformedWorkingLocationEvent(Exception):
|
||||
pass
|
||||
class memberNotFound(Exception):
|
||||
pass
|
||||
class noListTeamDrivesAdministratorPrivilege(Exception):
|
||||
pass
|
||||
class noManageTeamDriveAdministratorPrivilege(Exception):
|
||||
pass
|
||||
class notACalendarUser(Exception):
|
||||
pass
|
||||
class notFound(Exception):
|
||||
pass
|
||||
class notImplemented(Exception):
|
||||
pass
|
||||
class operationNotSupported(Exception):
|
||||
pass
|
||||
class organizerOnNonTeamDriveNotSupported(Exception):
|
||||
pass
|
||||
class organizerOnNonTeamDriveItemNotSupported(Exception):
|
||||
pass
|
||||
class orgunitNotFound(Exception):
|
||||
pass
|
||||
class ownerOnTeamDriveItemNotSupported(Exception):
|
||||
pass
|
||||
class ownershipChangeAcrossDomainNotPermitted(Exception):
|
||||
pass
|
||||
class participantIsNeitherOrganizerNorAttendee(Exception):
|
||||
pass
|
||||
class permissionDenied(Exception):
|
||||
pass
|
||||
class permissionNotFound(Exception):
|
||||
pass
|
||||
class photoNotFound(Exception):
|
||||
pass
|
||||
class publishOutNotPermitted(Exception):
|
||||
pass
|
||||
class queryRequiresAdminCredentials(Exception):
|
||||
pass
|
||||
class quotaExceeded(Exception):
|
||||
pass
|
||||
class rateLimitExceeded(Exception):
|
||||
pass
|
||||
class required(Exception):
|
||||
pass
|
||||
class requiredAccessLevel(Exception):
|
||||
pass
|
||||
class resourceExhausted(Exception):
|
||||
pass
|
||||
class resourceIdNotFound(Exception):
|
||||
pass
|
||||
class resourceNotFound(Exception):
|
||||
pass
|
||||
class responsePreparationFailure(Exception):
|
||||
pass
|
||||
class revisionDeletionNotSupported(Exception):
|
||||
pass
|
||||
class revisionNotFound(Exception):
|
||||
pass
|
||||
class revisionsNotSupported(Exception):
|
||||
pass
|
||||
class serviceLimit(Exception):
|
||||
pass
|
||||
class serviceNotAvailable(Exception):
|
||||
pass
|
||||
class shareInNotPermitted(Exception):
|
||||
pass
|
||||
class shareOutNotPermitted(Exception):
|
||||
pass
|
||||
class shareOutNotPermittedToUser(Exception):
|
||||
pass
|
||||
class sharingRateLimitExceeded(Exception):
|
||||
pass
|
||||
class shortcutTargetInvalid(Exception):
|
||||
pass
|
||||
class storageQuotaExceeded(Exception):
|
||||
pass
|
||||
class systemError(Exception):
|
||||
pass
|
||||
class targetUserRoleLimitedByLicenseRestriction(Exception):
|
||||
pass
|
||||
class teamDriveAlreadyExists(Exception):
|
||||
pass
|
||||
class teamDriveDomainUsersOnlyRestriction(Exception):
|
||||
pass
|
||||
class teamDriveTeamMembersOnlyRestriction(Exception):
|
||||
pass
|
||||
class teamDriveFileLimitExceeded(Exception):
|
||||
pass
|
||||
class teamDriveHierarchyTooDeep(Exception):
|
||||
pass
|
||||
class teamDriveMembershipRequired(Exception):
|
||||
pass
|
||||
class teamDrivesFolderMoveInNotSupported(Exception):
|
||||
pass
|
||||
class teamDrivesFolderSharingNotSupported(Exception):
|
||||
pass
|
||||
class teamDrivesParentLimit(Exception):
|
||||
pass
|
||||
class teamDrivesSharingRestrictionNotAllowed(Exception):
|
||||
pass
|
||||
class teamDrivesShortcutFileNotSupported(Exception):
|
||||
pass
|
||||
class timeRangeEmpty(Exception):
|
||||
pass
|
||||
class transientError(Exception):
|
||||
pass
|
||||
class unknownError(Exception):
|
||||
pass
|
||||
class unsupportedLanguageCode(Exception):
|
||||
pass
|
||||
class unsupportedSupervisedAccount(Exception):
|
||||
pass
|
||||
class uploadTooLarge(Exception):
|
||||
pass
|
||||
class userCannotCreateTeamDrives(Exception):
|
||||
pass
|
||||
class userAccess(Exception):
|
||||
pass
|
||||
class userNotFound(Exception):
|
||||
pass
|
||||
class userRateLimitExceeded(Exception):
|
||||
pass
|
||||
|
||||
REASON_EXCEPTION_MAP = {
|
||||
ABORTED: aborted,
|
||||
ABUSIVE_CONTENT_RESTRICTION: abusiveContentRestriction,
|
||||
ACCESS_NOT_CONFIGURED: accessNotConfigured,
|
||||
ALREADY_EXISTS: alreadyExists,
|
||||
APPLY_LABEL_FORBIDDEN: applyLabelForbidden,
|
||||
AUTH_ERROR: authError,
|
||||
BACKEND_ERROR: backendError,
|
||||
BAD_REQUEST: badRequest,
|
||||
CANNOT_ADD_PARENT: cannotAddParent,
|
||||
CANNOT_CHANGE_ORGANIZER: cannotChangeOrganizer,
|
||||
CANNOT_CHANGE_ORGANIZER_OF_INSTANCE: cannotChangeOrganizerOfInstance,
|
||||
CANNOT_CHANGE_OWN_ACL: cannotChangeOwnAcl,
|
||||
CANNOT_CHANGE_OWNER_ACL: cannotChangeOwnerAcl,
|
||||
CANNOT_CHANGE_OWN_PRIMARY_SUBSCRIPTION: cannotChangeOwnPrimarySubscription,
|
||||
CANNOT_COPY_FILE: cannotCopyFile,
|
||||
CANNOT_DELETE_ONLY_REVISION: cannotDeleteOnlyRevision,
|
||||
CANNOT_DELETE_PRIMARY_CALENDAR: cannotDeletePrimaryCalendar,
|
||||
CANNOT_DELETE_PRIMARY_SENDAS: cannotDeletePrimarySendAs,
|
||||
CANNOT_DELETE_RESOURCE_WITH_CHILDREN: cannotDeleteResourceWithChildren,
|
||||
CANNOT_MODIFY_INHERITED_TEAMDRIVE_PERMISSION: cannotModifyInheritedTeamDrivePermission,
|
||||
CANNOT_MODIFY_RESTRICTED_LABEL: cannotModifyRestrictedLabel,
|
||||
CANNOT_MODIFY_VIEWERS_CAN_COPY_CONTENT: cannotModifyViewersCanCopyContent,
|
||||
CANNOT_MOVE_TRASHED_ITEM_INTO_TEAMDRIVE: cannotMoveTrashedItemIntoTeamDrive,
|
||||
CANNOT_MOVE_TRASHED_ITEM_OUT_OF_TEAMDRIVE: cannotMoveTrashedItemOutOfTeamDrive,
|
||||
CANNOT_REMOVE_OWNER: cannotRemoveOwner,
|
||||
CANNOT_SET_EXPIRATION: cannotSetExpiration,
|
||||
CANNOT_SHARE_GROUPS_WITHLINK: cannotShareGroupsWithLink,
|
||||
CANNOT_SHARE_USERS_WITHLINK: cannotShareUsersWithLink,
|
||||
CANNOT_SHARE_TEAMDRIVE_TOPFOLDER_WITH_ANYONEORDOMAINS: cannotShareTeamDriveTopFolderWithAnyoneOrDomains,
|
||||
CANNOT_SHARE_TEAMDRIVE_WITH_NONGOOGLE_ACCOUNTS: cannotShareTeamDriveWithNonGoogleAccounts,
|
||||
CANNOT_UPDATE_PERMISSION: cannotUpdatePermission,
|
||||
CONDITION_NOT_MET: conditionNotMet,
|
||||
CONFLICT: conflict,
|
||||
CONTENT_OWNER_ACCOUNT_NOT_FOUND: contentOwnerAccountNotFound,
|
||||
CROSS_DOMAIN_MOVE_RESTRICTION: crossDomainMoveRestriction,
|
||||
CUSTOMER_EXCEEDED_ROLE_ASSIGNMENTS_LIMIT: customerExceededRoleAssignmentsLimit,
|
||||
CUSTOMER_NOT_FOUND: customerNotFound,
|
||||
CYCLIC_MEMBERSHIPS_NOT_ALLOWED: cyclicMembershipsNotAllowed,
|
||||
DELETED: deleted,
|
||||
DELETED_USER_NOT_FOUND: deletedUserNotFound,
|
||||
DOMAIN_ALIAS_NOT_FOUND: domainAliasNotFound,
|
||||
DOMAIN_CANNOT_USE_APIS: domainCannotUseApis,
|
||||
DOMAIN_NOT_FOUND: domainNotFound,
|
||||
DOMAIN_NOT_VERIFIED_SECONDARY: domainNotVerifiedSecondary,
|
||||
DOMAIN_POLICY: domainPolicy,
|
||||
DOWNLOAD_QUOTA_EXCEEDED: downloadQuotaExceeded,
|
||||
DUPLICATE: duplicate,
|
||||
EVENT_DURATION_EXCEEDS_LIMIT: eventDurationExceedsLimit,
|
||||
EXPIRATION_DATE_NOT_ALLOWED_FOR_SHARED_DRIVE_MEMBERS: expirationDateNotAllowedForSharedDriveMembers,
|
||||
FAILED_PRECONDITION: failedPrecondition,
|
||||
FIELD_IN_USE: fieldInUse,
|
||||
FIELD_NOT_WRITABLE: fieldNotWritable,
|
||||
FILE_NEVER_WRITABLE: fileNeverWritable,
|
||||
FILE_NOT_FOUND: fileNotFound,
|
||||
FILE_ORGANIZER_NOT_YET_ENABLED_FOR_THIS_TEAMDRIVE: fileOrganizerNotYetEnabledForThisTeamDrive,
|
||||
FILE_ORGANIZER_ON_FOLDERS_IN_SHARED_DRIVE_ONLY: fileOrganizerOnFoldersInSharedDriveOnly,
|
||||
FILE_ORGANIZER_ON_NON_TEAMDRIVE_NOT_SUPPORTED: fileOrganizerOnNonTeamDriveNotSupported,
|
||||
FILE_OWNER_NOT_MEMBER_OF_TEAMDRIVE: fileOwnerNotMemberOfTeamDrive,
|
||||
FILE_OWNER_NOT_MEMBER_OF_WRITER_DOMAIN: fileOwnerNotMemberOfWriterDomain,
|
||||
FILE_WRITER_TEAMDRIVE_MOVE_IN_DISABLED: fileWriterTeamDriveMoveInDisabled,
|
||||
FORBIDDEN: forbidden,
|
||||
GROUP_NOT_FOUND: groupNotFound,
|
||||
ILLEGAL_ACCESS_ROLE_FOR_DEFAULT: illegalAccessRoleForDefault,
|
||||
INSUFFICIENT_ADMINISTRATOR_PRIVILEGES: insufficientAdministratorPrivileges,
|
||||
INSUFFICIENT_ARCHIVED_USER_LICENSES: insufficientArchivedUserLicenses,
|
||||
INSUFFICIENT_FILE_PERMISSIONS: insufficientFilePermissions,
|
||||
INSUFFICIENT_PARENT_PERMISSIONS: insufficientParentPermissions,
|
||||
INSUFFICIENT_PERMISSIONS: insufficientPermissions,
|
||||
INTERNAL_ERROR: internalError,
|
||||
INVALID: invalid,
|
||||
INVALID_ARGUMENT: invalidArgument,
|
||||
INVALID_ATTRIBUTE_VALUE: invalidAttributeValue,
|
||||
INVALID_CUSTOMER_ID: invalidCustomerId,
|
||||
INVALID_INPUT: invalidInput,
|
||||
INVALID_LINK_VISIBILITY: invalidLinkVisibility,
|
||||
INVALID_MEMBER: invalidMember,
|
||||
INVALID_MESSAGE_ID: invalidMessageId,
|
||||
INVALID_ORGUNIT: invalidOrgunit,
|
||||
INVALID_ORGUNIT_NAME: invalidOrgunitName,
|
||||
INVALID_OWNERSHIP_TRANSFER: invalidOwnershipTransfer,
|
||||
INVALID_PARAMETER: invalidParameter,
|
||||
INVALID_PARENT_ORGUNIT: invalidParentOrgunit,
|
||||
INVALID_QUERY: invalidQuery,
|
||||
INVALID_RESOURCE: invalidResource,
|
||||
INVALID_SCHEMA_VALUE: invalidSchemaValue,
|
||||
INVALID_SCOPE_VALUE: invalidScopeValue,
|
||||
INVALID_SHARING_REQUEST: invalidSharingRequest,
|
||||
LABEL_MULTIPLE_VALUES_FOR_SINGULAR_FIELD: labelMultipleValuesForSingularField,
|
||||
LABEL_MUTATION_FORBIDDEN: labelMutationForbidden,
|
||||
LABEL_MUTATION_ILLEGAL_SELECTION: labelMutationIllegalSelection,
|
||||
LABEL_MUTATION_UNKNOWN_FIELD: labelMutationUnknownField,
|
||||
LIMIT_EXCEEDED: limitExceeded,
|
||||
LOGIN_REQUIRED: loginRequired,
|
||||
MAIL_SERVICE_NOT_ENABLED: mailServiceNotEnabled,
|
||||
MALFORMED_WORKING_LOCATION_EVENT: malformedWorkingLocationEvent,
|
||||
MEMBER_NOT_FOUND: memberNotFound,
|
||||
NO_LIST_TEAMDRIVES_ADMINISTRATOR_PRIVILEGE: noListTeamDrivesAdministratorPrivilege,
|
||||
NO_MANAGE_TEAMDRIVE_ADMINISTRATOR_PRIVILEGE: noManageTeamDriveAdministratorPrivilege,
|
||||
NOT_A_CALENDAR_USER: notACalendarUser,
|
||||
NOT_FOUND: notFound,
|
||||
NOT_IMPLEMENTED: notImplemented,
|
||||
OPERATION_NOT_SUPPORTED: operationNotSupported,
|
||||
ORGANIZER_ON_NON_TEAMDRIVE_NOT_SUPPORTED: organizerOnNonTeamDriveNotSupported,
|
||||
ORGANIZER_ON_NON_TEAMDRIVE_ITEM_NOT_SUPPORTED: organizerOnNonTeamDriveItemNotSupported,
|
||||
ORGUNIT_NOT_FOUND: orgunitNotFound,
|
||||
OWNER_ON_TEAMDRIVE_ITEM_NOT_SUPPORTED: ownerOnTeamDriveItemNotSupported,
|
||||
OWNERSHIP_CHANGE_ACROSS_DOMAIN_NOT_PERMITTED: ownershipChangeAcrossDomainNotPermitted,
|
||||
PARTICIPANT_IS_NEITHER_ORGANIZER_NOR_ATTENDEE: participantIsNeitherOrganizerNorAttendee,
|
||||
PERMISSION_DENIED: permissionDenied,
|
||||
PERMISSION_NOT_FOUND: permissionNotFound,
|
||||
PHOTO_NOT_FOUND: photoNotFound,
|
||||
PUBLISH_OUT_NOT_PERMITTED: publishOutNotPermitted,
|
||||
QUERY_REQUIRES_ADMIN_CREDENTIALS: queryRequiresAdminCredentials,
|
||||
QUOTA_EXCEEDED: quotaExceeded,
|
||||
RATE_LIMIT_EXCEEDED: rateLimitExceeded,
|
||||
REQUIRED: required,
|
||||
REQUIRED_ACCESS_LEVEL: requiredAccessLevel,
|
||||
RESOURCE_EXHAUSTED: resourceExhausted,
|
||||
RESOURCE_ID_NOT_FOUND: resourceIdNotFound,
|
||||
RESOURCE_NOT_FOUND: resourceNotFound,
|
||||
RESPONSE_PREPARATION_FAILURE: responsePreparationFailure,
|
||||
REVISION_DELETION_NOT_SUPPORTED: revisionDeletionNotSupported,
|
||||
REVISION_NOT_FOUND: revisionNotFound,
|
||||
REVISIONS_NOT_SUPPORTED: revisionsNotSupported,
|
||||
SERVICE_LIMIT: serviceLimit,
|
||||
SERVICE_NOT_AVAILABLE: serviceNotAvailable,
|
||||
SHARE_IN_NOT_PERMITTED: shareInNotPermitted,
|
||||
SHARE_OUT_NOT_PERMITTED: shareOutNotPermitted,
|
||||
SHARE_OUT_NOT_PERMITTED_TO_USER: shareOutNotPermittedToUser,
|
||||
SHARING_RATE_LIMIT_EXCEEDED: sharingRateLimitExceeded,
|
||||
SHORTCUT_TARGET_INVALID: shortcutTargetInvalid,
|
||||
STORAGE_QUOTA_EXCEEDED: storageQuotaExceeded,
|
||||
SYSTEM_ERROR: systemError,
|
||||
TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION: targetUserRoleLimitedByLicenseRestriction,
|
||||
TEAMDRIVE_ALREADY_EXISTS: teamDriveAlreadyExists,
|
||||
TEAMDRIVE_DOMAIN_USERS_ONLY_RESTRICTION: teamDriveDomainUsersOnlyRestriction,
|
||||
TEAMDRIVE_TEAM_MEMBERS_ONLY_RESTRICTION: teamDriveTeamMembersOnlyRestriction,
|
||||
TEAMDRIVE_FILE_LIMIT_EXCEEDED: teamDriveFileLimitExceeded,
|
||||
TEAMDRIVE_HIERARCHY_TOO_DEEP: teamDriveHierarchyTooDeep,
|
||||
TEAMDRIVE_MEMBERSHIP_REQUIRED: teamDriveMembershipRequired,
|
||||
TEAMDRIVES_FOLDER_MOVE_IN_NOT_SUPPORTED: teamDrivesFolderMoveInNotSupported,
|
||||
TEAMDRIVES_FOLDER_SHARING_NOT_SUPPORTED: teamDrivesFolderSharingNotSupported,
|
||||
TEAMDRIVES_PARENT_LIMIT: teamDrivesParentLimit,
|
||||
TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED: teamDrivesSharingRestrictionNotAllowed,
|
||||
TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED: teamDrivesShortcutFileNotSupported,
|
||||
TIME_RANGE_EMPTY: timeRangeEmpty,
|
||||
TRANSIENT_ERROR: transientError,
|
||||
UNKNOWN_ERROR: unknownError,
|
||||
UNSUPPORTED_LANGUAGE_CODE: unsupportedLanguageCode,
|
||||
UNSUPPORTED_SUPERVISED_ACCOUNT: unsupportedSupervisedAccount,
|
||||
UPLOAD_TOO_LARGE: uploadTooLarge,
|
||||
USER_CANNOT_CREATE_TEAMDRIVES: userCannotCreateTeamDrives,
|
||||
USER_ACCESS: userAccess,
|
||||
USER_NOT_FOUND: userNotFound,
|
||||
USER_RATE_LIMIT_EXCEEDED: userRateLimitExceeded,
|
||||
}
|
||||
98
src/gam/gamlib/glgdata.py
Normal file
98
src/gam/gamlib/glgdata.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2023 Ross Scroggs All Rights Reserved.
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""GAM GData resources
|
||||
|
||||
"""
|
||||
API_DEPRECATED_MSG = 'Contacts API is being deprecated.'
|
||||
|
||||
# callGData throw errors
|
||||
API_DEPRECATED = 612
|
||||
BAD_GATEWAY = 601
|
||||
BAD_REQUEST = 602
|
||||
DOES_NOT_EXIST = 1301
|
||||
ENTITY_EXISTS = 1300
|
||||
FORBIDDEN = 603
|
||||
GATEWAY_TIMEOUT = 612
|
||||
INSUFFICIENT_PERMISSIONS = 604
|
||||
INTERNAL_SERVER_ERROR = 1000
|
||||
INVALID_DOMAIN = 605
|
||||
INVALID_INPUT = 1317
|
||||
INVALID_VALUE = 1801
|
||||
NAME_NOT_VALID = 1303
|
||||
NOT_FOUND = 606
|
||||
NOT_IMPLEMENTED = 607
|
||||
PRECONDITION_FAILED = 608
|
||||
QUOTA_EXCEEDED = 609
|
||||
SERVICE_NOT_APPLICABLE = 1410
|
||||
SERVICE_UNAVAILABLE = 610
|
||||
TOKEN_EXPIRED = 611
|
||||
TOKEN_INVALID = 403
|
||||
UNKNOWN_ERROR = 600
|
||||
#
|
||||
NON_TERMINATING_ERRORS = [API_DEPRECATED, BAD_GATEWAY, GATEWAY_TIMEOUT, QUOTA_EXCEEDED, SERVICE_UNAVAILABLE, TOKEN_EXPIRED]
|
||||
EMAILSETTINGS_THROW_LIST = [INVALID_DOMAIN, DOES_NOT_EXIST, SERVICE_NOT_APPLICABLE, BAD_REQUEST, NAME_NOT_VALID, INTERNAL_SERVER_ERROR, INVALID_VALUE]
|
||||
#
|
||||
class apiDeprecated(Exception):
|
||||
pass
|
||||
class badRequest(Exception):
|
||||
pass
|
||||
class doesNotExist(Exception):
|
||||
pass
|
||||
class entityExists(Exception):
|
||||
pass
|
||||
class forbidden(Exception):
|
||||
pass
|
||||
class insufficientPermissions(Exception):
|
||||
pass
|
||||
class internalServerError(Exception):
|
||||
pass
|
||||
class invalidDomain(Exception):
|
||||
pass
|
||||
class invalidInput(Exception):
|
||||
pass
|
||||
class invalidValue(Exception):
|
||||
pass
|
||||
class nameNotValid(Exception):
|
||||
pass
|
||||
class notFound(Exception):
|
||||
pass
|
||||
class notImplemented(Exception):
|
||||
pass
|
||||
class preconditionFailed(Exception):
|
||||
pass
|
||||
class serviceNotApplicable(Exception):
|
||||
pass
|
||||
|
||||
ERROR_CODE_EXCEPTION_MAP = {
|
||||
API_DEPRECATED: apiDeprecated,
|
||||
BAD_REQUEST: badRequest,
|
||||
DOES_NOT_EXIST: doesNotExist,
|
||||
ENTITY_EXISTS: entityExists,
|
||||
FORBIDDEN: forbidden,
|
||||
INSUFFICIENT_PERMISSIONS: insufficientPermissions,
|
||||
INTERNAL_SERVER_ERROR: internalServerError,
|
||||
INVALID_DOMAIN: invalidDomain,
|
||||
INVALID_INPUT: invalidInput,
|
||||
INVALID_VALUE: invalidValue,
|
||||
NAME_NOT_VALID: nameNotValid,
|
||||
NOT_FOUND: notFound,
|
||||
NOT_IMPLEMENTED: notImplemented,
|
||||
PRECONDITION_FAILED: preconditionFailed,
|
||||
SERVICE_NOT_APPLICABLE: serviceNotApplicable,
|
||||
}
|
||||
307
src/gam/gamlib/glglobals.py
Normal file
307
src/gam/gamlib/glglobals.py
Normal file
@@ -0,0 +1,307 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2023 Ross Scroggs All Rights Reserved.
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""GAM global variables
|
||||
|
||||
"""
|
||||
|
||||
# The following GM_XXX constants are arbitrary but must be unique
|
||||
# Most errors print a message and bail out with a return code
|
||||
# Some commands want to set a non-zero return code but not bail
|
||||
# GAM admin user
|
||||
ADMIN = 'admin'
|
||||
# Number/length of API call retries
|
||||
API_CALLS_RETRY_DATA = 'rtry'
|
||||
# GAM cache directory. If no_cache is True, this variable will be set to None
|
||||
CACHE_DIR = 'gacd'
|
||||
# Reset GAM cache directory after discovery
|
||||
CACHE_DISCOVERY_ONLY = 'gcdo'
|
||||
# Classroom service not available
|
||||
CLASSROOM_SERVICE_NOT_AVAILABLE = 'csna'
|
||||
# Command logging
|
||||
CMDLOG_HANDLER = 'clha'
|
||||
CMDLOG_LOGGER = 'cllo'
|
||||
# Convert to local time
|
||||
CONVERT_TO_LOCAL_TIME = 'ctlt'
|
||||
# Credentials scopes
|
||||
CREDENTIALS_SCOPES = 'crsc'
|
||||
# csvfile keyfield <FieldName> [delimiter <Character>] (matchfield <FieldName> <MatchPattern>)* [datafield <FieldName>(:<FieldName>*) [delimiter <String>]]
|
||||
CSVFILE = 'csvf'
|
||||
# { key: [datafieldvalues]}
|
||||
CSV_DATA_DICT = 'csdd'
|
||||
CSV_KEY_FIELD = 'cskf'
|
||||
CSV_SUBKEY_FIELD = 'cssk'
|
||||
CSV_DATA_FIELD = 'csdf'
|
||||
# Filter for input column drop values
|
||||
CSV_INPUT_ROW_DROP_FILTER = 'cird'
|
||||
# Mode (and|or) for input column drop values
|
||||
CSV_INPUT_ROW_DROP_FILTER_MODE = 'cidm'
|
||||
# Filter for input column values
|
||||
CSV_INPUT_ROW_FILTER = 'cirf'
|
||||
# Mode (and|or) for input column values
|
||||
CSV_INPUT_ROW_FILTER_MODE = 'cirm'
|
||||
# Limit number of input rows
|
||||
CSV_INPUT_ROW_LIMIT = 'cirl'
|
||||
# Column delimiter in CSV output file
|
||||
CSV_OUTPUT_COLUMN_DELIMITER = 'codl'
|
||||
# Filter for output column headers to drop
|
||||
CSV_OUTPUT_HEADER_DROP_FILTER = 'cohd'
|
||||
# Filter for output column headers
|
||||
CSV_OUTPUT_HEADER_FILTER = 'cohf'
|
||||
# Force output column headers
|
||||
CSV_OUTPUT_HEADER_FORCE = 'cofh'
|
||||
# Order output column headers
|
||||
CSV_OUTPUT_HEADER_ORDER = 'coho'
|
||||
# No escape character in CSV output file
|
||||
CSV_OUTPUT_NO_ESCAPE_CHAR = 'cone'
|
||||
# Quote character in CSV output file
|
||||
CSV_OUTPUT_QUOTE_CHAR = 'coqc'
|
||||
# Filter for output column drop values
|
||||
CSV_OUTPUT_ROW_DROP_FILTER = 'cord'
|
||||
# Mode (and|or) for output column drop values
|
||||
CSV_OUTPUT_ROW_DROP_FILTER_MODE = 'codm'
|
||||
# Filter for output column values
|
||||
CSV_OUTPUT_ROW_FILTER = 'corf'
|
||||
# Mode (and|or) for output column values
|
||||
CSV_OUTPUT_ROW_FILTER_MODE = 'corm'
|
||||
# Limit number of output rows
|
||||
CSV_OUTPUT_ROW_LIMIT = 'corl'
|
||||
# Add timestamp column to CSV output file
|
||||
CSV_OUTPUT_TIMESTAMP_COLUMN = 'cotc'
|
||||
# Output sort headers
|
||||
CSV_OUTPUT_SORT_HEADERS = 'cosh'
|
||||
# CSV todrive options
|
||||
CSV_TODRIVE = 'todr'
|
||||
# Current API services
|
||||
CURRENT_API_SERVICES = 'caps'
|
||||
# Current Client API
|
||||
CURRENT_CLIENT_API = 'ccap'
|
||||
# Current Client API scopes
|
||||
CURRENT_CLIENT_API_SCOPES = 'ccas'
|
||||
# Current Service Account API
|
||||
CURRENT_SVCACCT_API = 'csap'
|
||||
# Current Service Account API scopes
|
||||
CURRENT_SVCACCT_API_SCOPES = 'csas'
|
||||
# Current Service Account user
|
||||
CURRENT_SVCACCT_USER = 'csa'
|
||||
# datetime.datetime.now
|
||||
DATETIME_NOW = 'dtno'
|
||||
# If debug_level > 0: extra_args['prettyPrint'] = True, httplib2.debuglevel = gam_debug_level, appsObj.debug = True
|
||||
DEBUG_LEVEL = 'dbgl'
|
||||
# Decoded ID token
|
||||
DECODED_ID_TOKEN = 'didt'
|
||||
# Index of start of <UserTypeEntity> in command line
|
||||
ENTITY_CL_DELAY_START = 'ecld'
|
||||
ENTITY_CL_START = 'ecls'
|
||||
# Extra arguments to pass to GAPI functions
|
||||
EXTRA_ARGS_LIST = 'exad'
|
||||
# gam.cfg file
|
||||
GAM_CFG_FILE = 'gcfi'
|
||||
GAM_CFG_PATH = 'gcpa'
|
||||
GAM_CFG_SECTION = 'gcse'
|
||||
GAM_CFG_SECTION_NAME = 'gcsn'
|
||||
# Path to gam
|
||||
GAM_PATH = 'gpth'
|
||||
# Python source, PyInstaller or StaticX?
|
||||
GAM_TYPE = 'gtyp'
|
||||
# Length of last Got message
|
||||
LAST_GOT_MSG_LEN = 'lgml'
|
||||
# License SKUs
|
||||
LICENSE_SKUS = 'lsku'
|
||||
# Make Building ID/Name map
|
||||
MAKE_BUILDING_ID_NAME_MAP = 'mkbm'
|
||||
# Dictionary mapping Building ID to Name
|
||||
MAP_BUILDING_ID_TO_NAME = 'bi2n'
|
||||
# Dictionary mapping Building Name to ID
|
||||
MAP_BUILDING_NAME_TO_ID = 'bn2i'
|
||||
# Dictionary mapping OrgUnit ID to Name
|
||||
MAP_ORGUNIT_ID_TO_NAME = 'oi2n'
|
||||
# Dictionary mapping Shared Drive ID to Name
|
||||
MAP_SHAREDDRIVE_ID_TO_NAME = 'si2n'
|
||||
# Make Role ID/Name map
|
||||
MAKE_ROLE_ID_NAME_MAP = 'mkrm'
|
||||
# Dictionary mapping Role ID to Name
|
||||
MAP_ROLE_ID_TO_NAME = 'ri2n'
|
||||
# Dictionary mapping Role Name to ID
|
||||
MAP_ROLE_NAME_TO_ID = 'rn2i'
|
||||
# Dictionary mapping User ID to Name
|
||||
MAP_USER_ID_TO_NAME = 'ui2n'
|
||||
# Multiprocess exit condition
|
||||
MULTIPROCESS_EXIT_CONDITION = 'mpec'
|
||||
# Multiprocess exit processing
|
||||
MULTIPROCESS_EXIT_PROCESSING = 'mpep'
|
||||
# Number of batch items
|
||||
NUM_BATCH_ITEMS = 'nbat'
|
||||
# Values retrieved from oauth2service.json
|
||||
OAUTH2SERVICE_CLIENT_ID = 'osci'
|
||||
OAUTH2SERVICE_JSON_DATA = 'osjd'
|
||||
# Values retrieved from oauth2.txt
|
||||
OAUTH2_CLIENT_ID = 'oaci'
|
||||
# oauth2.txt lock file
|
||||
OAUTH2_TXT_LOCK = 'oatl'
|
||||
# Output date format, empty defalts to ISOFormat
|
||||
OUTPUT_DATEFORMAT = 'oudf'
|
||||
# Output time format, empty defalts to ISOFormat
|
||||
OUTPUT_TIMEFORMAT = 'outf'
|
||||
# gam.cfg parser
|
||||
PARSER = 'pars'
|
||||
# Process ID
|
||||
PID = 'pid '
|
||||
# Domains for print alises|groups|users
|
||||
PRINT_AGU_DOMAINS = 'pagu'
|
||||
# OrgUnits for print cros
|
||||
PRINT_CROS_OUS = 'pcou'
|
||||
# OrgUnits and children for print cros
|
||||
PRINT_CROS_OUS_AND_CHILDREN = 'pcoc'
|
||||
# Check API calls rate
|
||||
RATE_CHECK_COUNT = 'rccn'
|
||||
RATE_CHECK_START = 'rcst'
|
||||
# Section name from outer gam, passed to inner gams
|
||||
SECTION = 'sect'
|
||||
# Enable/disable "Getting ... " messages
|
||||
SHOW_GETTINGS = 'shog'
|
||||
# Enable/disable NL at end of "Got ..." messages
|
||||
SHOW_GETTINGS_GOT_NL = 'shgn'
|
||||
# redirected files
|
||||
SAVED_STDOUT = 'svso'
|
||||
STDERR = 'stde'
|
||||
STDOUT = 'stdo'
|
||||
# Scopes values retrieved from oauth2service.json
|
||||
SVCACCT_SCOPES = 'sasc'
|
||||
# Were scopes values retrieved from oauth2service.json
|
||||
SVCACCT_SCOPES_DEFINED = 'sasd'
|
||||
# Most errors print a message and bail out with a return code
|
||||
# Some commands want to set a non-zero return code but not bail
|
||||
SYSEXITRC = 'sxrc'
|
||||
# Encodings
|
||||
SYS_ENCODING = 'syen'
|
||||
# Shared by threadBatchWorker and threadBatchGAMCommands
|
||||
TBATCH_QUEUE = 'batq'
|
||||
# redirected file fields: name, mode, encoding, write header, multiproces, queue
|
||||
REDIRECT_NAME = 'rdfn'
|
||||
REDIRECT_MODE = 'rdmo'
|
||||
REDIRECT_FD = 'rdfd'
|
||||
REDIRECT_MULTI_FD = 'rdmf'
|
||||
REDIRECT_STD = 'rdst'
|
||||
REDIRECT_ENCODING = 'rden'
|
||||
REDIRECT_WRITE_HEADER = 'rdwh'
|
||||
REDIRECT_MULTIPROCESS = 'rdmp'
|
||||
REDIRECT_QUEUE = 'rdq'
|
||||
REDIRECT_QUEUE_NAME = 'name'
|
||||
REDIRECT_QUEUE_TODRIVE = 'todrive'
|
||||
REDIRECT_QUEUE_CSVPF = 'csvpf'
|
||||
REDIRECT_QUEUE_DATA = 'rows'
|
||||
REDIRECT_QUEUE_ARGS = 'args'
|
||||
REDIRECT_QUEUE_GLOBALS = 'globals'
|
||||
REDIRECT_QUEUE_VALUES = 'values'
|
||||
REDIRECT_QUEUE_START = 'start'
|
||||
REDIRECT_QUEUE_END = 'end'
|
||||
REDIRECT_QUEUE_EOF = 'eof'
|
||||
#
|
||||
Globals = {
|
||||
ADMIN: None,
|
||||
API_CALLS_RETRY_DATA: {},
|
||||
CACHE_DIR: None,
|
||||
CACHE_DISCOVERY_ONLY: True,
|
||||
CLASSROOM_SERVICE_NOT_AVAILABLE: False,
|
||||
CMDLOG_HANDLER: None,
|
||||
CMDLOG_LOGGER: None,
|
||||
CONVERT_TO_LOCAL_TIME: False,
|
||||
CREDENTIALS_SCOPES: set(),
|
||||
CSVFILE: {},
|
||||
CSV_DATA_DICT: {},
|
||||
CSV_KEY_FIELD: None,
|
||||
CSV_SUBKEY_FIELD: None,
|
||||
CSV_DATA_FIELD: None,
|
||||
CSV_INPUT_ROW_DROP_FILTER: [],
|
||||
CSV_INPUT_ROW_DROP_FILTER_MODE: False,
|
||||
CSV_INPUT_ROW_FILTER: [],
|
||||
CSV_INPUT_ROW_FILTER_MODE: True,
|
||||
CSV_INPUT_ROW_LIMIT: 0,
|
||||
CSV_OUTPUT_COLUMN_DELIMITER: None,
|
||||
CSV_OUTPUT_HEADER_DROP_FILTER: [],
|
||||
CSV_OUTPUT_HEADER_FILTER: [],
|
||||
CSV_OUTPUT_HEADER_FORCE: [],
|
||||
CSV_OUTPUT_HEADER_ORDER: [],
|
||||
CSV_OUTPUT_NO_ESCAPE_CHAR: None,
|
||||
CSV_OUTPUT_QUOTE_CHAR: None,
|
||||
CSV_OUTPUT_ROW_DROP_FILTER: [],
|
||||
CSV_OUTPUT_ROW_DROP_FILTER_MODE: False,
|
||||
CSV_OUTPUT_ROW_FILTER: [],
|
||||
CSV_OUTPUT_ROW_FILTER_MODE: True,
|
||||
CSV_OUTPUT_ROW_LIMIT: 0,
|
||||
CSV_OUTPUT_SORT_HEADERS: [],
|
||||
CSV_OUTPUT_TIMESTAMP_COLUMN: None,
|
||||
CSV_TODRIVE: {},
|
||||
CURRENT_API_SERVICES: {},
|
||||
CURRENT_CLIENT_API: None,
|
||||
CURRENT_CLIENT_API_SCOPES: set(),
|
||||
CURRENT_SVCACCT_API: None,
|
||||
CURRENT_SVCACCT_API_SCOPES: set(),
|
||||
CURRENT_SVCACCT_USER: None,
|
||||
DATETIME_NOW: None,
|
||||
DEBUG_LEVEL: 0,
|
||||
DECODED_ID_TOKEN: None,
|
||||
ENTITY_CL_DELAY_START: 1,
|
||||
ENTITY_CL_START: 1,
|
||||
EXTRA_ARGS_LIST: [],
|
||||
GAM_CFG_FILE: '',
|
||||
GAM_CFG_PATH: '',
|
||||
GAM_CFG_SECTION: '',
|
||||
GAM_CFG_SECTION_NAME: '',
|
||||
GAM_PATH: '.',
|
||||
GAM_TYPE: '',
|
||||
LAST_GOT_MSG_LEN: 0,
|
||||
LICENSE_SKUS: [],
|
||||
MAKE_BUILDING_ID_NAME_MAP: True,
|
||||
MAKE_ROLE_ID_NAME_MAP: True,
|
||||
MAP_BUILDING_ID_TO_NAME: {},
|
||||
MAP_BUILDING_NAME_TO_ID: {},
|
||||
MAP_ORGUNIT_ID_TO_NAME: {},
|
||||
MAP_SHAREDDRIVE_ID_TO_NAME: {},
|
||||
MAP_ROLE_ID_TO_NAME: {},
|
||||
MAP_ROLE_NAME_TO_ID: {},
|
||||
MAP_USER_ID_TO_NAME: {},
|
||||
MULTIPROCESS_EXIT_CONDITION: None,
|
||||
MULTIPROCESS_EXIT_PROCESSING: False,
|
||||
NUM_BATCH_ITEMS: 0,
|
||||
OAUTH2SERVICE_CLIENT_ID: None,
|
||||
OAUTH2SERVICE_JSON_DATA: {},
|
||||
OAUTH2_CLIENT_ID: None,
|
||||
OAUTH2_TXT_LOCK: None,
|
||||
OUTPUT_DATEFORMAT: '',
|
||||
OUTPUT_TIMEFORMAT: '',
|
||||
PARSER: None,
|
||||
PID: 0,
|
||||
PRINT_AGU_DOMAINS: '',
|
||||
PRINT_CROS_OUS: '',
|
||||
PRINT_CROS_OUS_AND_CHILDREN: '',
|
||||
RATE_CHECK_COUNT: 0,
|
||||
RATE_CHECK_START: 0,
|
||||
SECTION: None,
|
||||
SHOW_GETTINGS: True,
|
||||
SHOW_GETTINGS_GOT_NL: False,
|
||||
SAVED_STDOUT: None,
|
||||
STDERR: {},
|
||||
STDOUT: {},
|
||||
SVCACCT_SCOPES: {},
|
||||
SVCACCT_SCOPES_DEFINED: False,
|
||||
SYSEXITRC: 0,
|
||||
SYS_ENCODING: 'utf-8',
|
||||
TBATCH_QUEUE: None
|
||||
}
|
||||
46
src/gam/gamlib/glindent.py
Normal file
46
src/gam/gamlib/glindent.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2023 Ross Scroggs All Rights Reserved.
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""GAM indent processing
|
||||
|
||||
"""
|
||||
|
||||
class GamIndent():
|
||||
|
||||
INDENT_SPACES_PER_LEVEL = ' '
|
||||
|
||||
def __init__(self):
|
||||
self.indent = 0
|
||||
|
||||
def Reset(self):
|
||||
self.indent = 0
|
||||
|
||||
def Increment(self):
|
||||
self.indent += 1
|
||||
|
||||
def Decrement(self):
|
||||
self.indent -= 1
|
||||
|
||||
def Spaces(self):
|
||||
return self.INDENT_SPACES_PER_LEVEL*self.indent
|
||||
|
||||
def SpacesSub1(self):
|
||||
return self.INDENT_SPACES_PER_LEVEL*(self.indent-1)
|
||||
|
||||
def MultiLineText(self, message, n=0):
|
||||
return message.replace('\n', f'\n{self.INDENT_SPACES_PER_LEVEL*(self.indent+n)}').rstrip()
|
||||
524
src/gam/gamlib/glmsgs.py
Normal file
524
src/gam/gamlib/glmsgs.py
Normal file
@@ -0,0 +1,524 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2024 Ross Scroggs All Rights Reserved.
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""GAM messages
|
||||
|
||||
"""
|
||||
|
||||
# These values can be translated into other languages
|
||||
# Project creation messages in order of appearance
|
||||
CREATING_PROJECT = 'Creating project "{0}"...\n'
|
||||
CHECK_INTERRUPTED = 'Check interrupted'
|
||||
CHECKING_PROJECT_CREATION_STATUS = 'Checking project creation status...\n'
|
||||
NO_RIGHTS_GOOGLE_CLOUD_ORGANIZATION = 'Looks like you have no rights to your Google Cloud Organization.\nAttempting to fix that...\n'
|
||||
YOUR_ORGANIZATION_NAME_IS = 'Your organization name is {0}\n'
|
||||
YOU_HAVE_NO_RIGHTS_TO_CREATE_PROJECTS_AND_YOU_ARE_NOT_A_SUPER_ADMIN = 'You have no rights to create projects for your organization and you don\'t seem to be a super admin! Sorry, there\'s nothing more I can do.'
|
||||
LOOKS_LIKE_NO_ONE_HAS_RIGHTS_TO_YOUR_GOOGLE_CLOUD_ORGANIZATION_ATTEMPTING_TO_GIVE_YOU_CREATE_RIGHTS = 'Looks like no one has rights to your Google Cloud Organization. Attempting to give you create rights...\n'
|
||||
THE_FOLLOWING_RIGHTS_SEEM_TO_EXIST = 'The following rights seem to exist:\n'
|
||||
GIVING_LOGIN_HINT_THE_CREATOR_ROLE = 'Giving {0} the role of {1}...\n'
|
||||
ACCEPT_CLOUD_TOS = '''
|
||||
Please go to:
|
||||
|
||||
https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project
|
||||
|
||||
sign in as {0} and accept the Terms of Service (ToS). As soon as you've accepted the ToS popup, you can return here and press enter.\n'''
|
||||
|
||||
PROJECT_STILL_BEING_CREATED_SLEEPING = 'Project still being created. Sleeping {0} seconds\n'
|
||||
FAILED_TO_CREATE_PROJECT = 'Failed to create project: {0}\n'
|
||||
SETTING_GAM_PROJECT_CONSENT_SCREEN = 'Setting GAM project consent screen...\n'
|
||||
CREATE_PROJECT_INSTRUCTIONS = '''
|
||||
Please go to:
|
||||
|
||||
{0}
|
||||
|
||||
1. Choose "Desktop App" or "Other" for "Application type".
|
||||
2. Enter "GAM" or another desired value for "Name".
|
||||
3. Click the blue "Create" button.
|
||||
4. Copy your "Client ID" value that shows on the next page.
|
||||
'''
|
||||
ENTER_YOUR_CLIENT_ID = '\nEnter your Client ID: '
|
||||
GO_BACK_TO_YOUR_BROWSER_AND_COPY_YOUR_CLIENT_SECRET_VALUE = '\n5. Go back to your browser and copy your "Client Secret" value.\n'
|
||||
ENTER_YOUR_CLIENT_SECRET = '\nEnter your Client Secret: '
|
||||
GO_BACK_TO_YOUR_BROWSER_AND_CLICK_OK_TO_CLOSE_THE_OAUTH_CLIENT_POPUP = '\n6. Go back to your browser and click OK to close the "OAuth client" popup if it\'s still open.\n'
|
||||
IS_NOT_A_VALID_CLIENT_ID = '''
|
||||
|
||||
{0}
|
||||
|
||||
Is not a valid Client ID.
|
||||
|
||||
Please make sure you are following the directions exactly and that there are no extra spaces in your Client ID.
|
||||
'''
|
||||
IS_NOT_A_VALID_CLIENT_SECRET = '''
|
||||
|
||||
{0}
|
||||
|
||||
Is not a valid Client Secret.
|
||||
|
||||
Please make sure you are following the directions exactly and that there are no extra spaces in your Client Secret.
|
||||
'''
|
||||
TRUST_GAM_CLIENT_ID = '''
|
||||
It's important to mark the {0} Client ID as trusted by your Workspace instance.
|
||||
|
||||
Please go to:
|
||||
|
||||
https://admin.google.com/ac/owl/list?tab=configuredApps
|
||||
|
||||
1. Click on: Configure new app > OAuth App Name Or Client ID.
|
||||
2. Enter the following Client ID value:
|
||||
|
||||
{1}
|
||||
|
||||
3. Press Search, select the {0} app, press Select, check the box and press Select.
|
||||
4. Keep the default scope or select a preferred scope that includes your GAM admin.
|
||||
5. Press Continue
|
||||
6. Select Trusted radio button, press Continue and Finish.
|
||||
7. Press Confirm if Confirm parental consent pops up
|
||||
8. Press enter here on the terminal once trust is complete.
|
||||
'''
|
||||
|
||||
ENABLE_SERVICE_ACCOUNT_PRIVATE_KEY_UPLOAD = '''
|
||||
Your workspace is configured to disable service account private key uploads.
|
||||
|
||||
Please go to:
|
||||
|
||||
https://github.com/taers232c/GAMADV-XTD3/wiki/Authorization#authorize-service-account-key-uploads
|
||||
|
||||
Follow the steps to allow a service account private key upload for the project ({0}) just created.
|
||||
Once those steps are completed, you can continue with your project authentication.
|
||||
'''
|
||||
|
||||
YOUR_GAM_PROJECT_IS_CREATED_AND_READY_TO_USE = '''
|
||||
That\'s it! Your GAM Project is created and ready to use.
|
||||
Proceed to the authentication steps.
|
||||
'''
|
||||
|
||||
# check|update service messages in order of appearance
|
||||
SYSTEM_TIME_STATUS = 'System time status'
|
||||
YOUR_SYSTEM_TIME_DIFFERS_FROM_GOOGLE = 'Your system time differs from {0} by {1}'
|
||||
PRESS_ENTER_ONCE_AUTHORIZATION_IS_COMPLETE = 'Press enter once authorization is complete.'
|
||||
SERVICE_ACCOUNT_API_DISABLED = '{0} not enabled. Please run "gam update project" and "gam user user@domain.com update serviceaccount"'
|
||||
SERVICE_ACCOUNT_PRIVATE_KEY_AUTHENTICATION = 'Service Account Private Key Authentication'
|
||||
SERVICE_ACCOUNT_CHECK_PRIVATE_KEY_AGE = 'Service Account Private Key age; Google recommends rotating keys on a routine basis'
|
||||
SERVICE_ACCOUNT_PRIVATE_KEY_AGE = 'Service Account Private Key age: {0} days'
|
||||
SERVICE_ACCOUNT_SKIPPING_KEY_AGE_CHECK = 'Skipping Private Key age check: {0} rotation not necessary'
|
||||
UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS = 'Please run "gam update project" to view/manage service account keys'
|
||||
DOMAIN_WIDE_DELEGATION_AUTHENTICATION = 'Domain-wide Delegation authentication'
|
||||
SCOPE_AUTHORIZATION_PASSED = '''All scopes PASSED!
|
||||
|
||||
Service Account Client name: {0} is fully authorized.
|
||||
'''
|
||||
SCOPE_AUTHORIZATION_UPDATE_PASSED = '''All scopes PASSED!
|
||||
To authorize them (in case some scopes were unselected), please go to the following link in your browser:
|
||||
{0}
|
||||
{1}
|
||||
|
||||
You will be directed to the Google Workspace admin console Security > API Controls > Domain-wide Delegation page
|
||||
The "Add a new Client ID" box will open
|
||||
Make sure that "Overwrite existing client ID" is checked
|
||||
Click AUTHORIZE
|
||||
When the box closes you're done
|
||||
After authorizing it may take some time for this test to pass so wait a few moments and then try this command again.
|
||||
'''
|
||||
SCOPE_AUTHORIZATION_FAILED = '''Some scopes FAILED!
|
||||
To authorize them, please go to the following link in your browser:
|
||||
{0}
|
||||
{1}
|
||||
|
||||
You will be directed to the Google Workspace admin console Security > API Controls > Domain-wide Delegation page
|
||||
The "Add a new Client ID" box will open
|
||||
Make sure that "Overwrite existing client ID" is checked
|
||||
Click AUTHORIZE
|
||||
When the box closes you're done
|
||||
After authorizing it may take some time for this test to pass so wait a few moments and then try this command again.
|
||||
'''
|
||||
# General messages
|
||||
ACCESS_FORBIDDEN = 'Access Forbidden'
|
||||
ACTION_APPLIED = 'Action Applied'
|
||||
ACTION_IN_PROGRESS = 'Action {0} in progress'
|
||||
ACTION_MAY_BE_DELAYED = 'Action may be delayed'
|
||||
ADMIN_STATUS_CHANGED_TO = 'Admin Status Changed to'
|
||||
ALL = 'All'
|
||||
ALL_FOLDER_NAMES_MUST_BE_NON_BLANK = 'All folder names must be non-blank: {0}'
|
||||
ALL_SKU_PRODUCTIDS_MUST_MATCH = 'All SKU productIds must match, {0} != {1}'
|
||||
ALREADY_WAS_OWNER = 'Already was owner'
|
||||
ALREADY_EXISTS = 'Already exists'
|
||||
ALREADY_EXISTS_IN_TARGET_FOLDER = 'Already exists in {0}: {1}'
|
||||
ALREADY_EXISTS_USE_MERGE_ARGUMENT = 'Already exists; use the "merge" argument to merge the labels'
|
||||
API_ACCESS_DENIED = 'API access Denied'
|
||||
API_CALLS_RETRY_DATA = 'API calls retry data\n'
|
||||
API_CHECK_CLIENT_AUTHORIZATION = 'Please make sure the Client ID: {0} is authorized for the appropriate API or scopes:\n{1}\n\nRun: gam oauth create\n'
|
||||
API_CHECK_SVCACCT_AUTHORIZATION = 'Please make sure the Service Account Client name: {0} is authorized for the appropriate API or scopes:\n{1}\n\nRun: gam user {2} update serviceaccount\n'
|
||||
API_ERROR_SETTINGS = 'API error, some settings not set'
|
||||
ARE_BOTH_REQUIRED = 'Arguments {0} and {1} are both required'
|
||||
ARE_MUTUALLY_EXCLUSIVE = 'Arguments {0} and {1} are mutually exclusive'
|
||||
AS = 'as'
|
||||
ATTENDEES_ADD = 'Add Attendees'
|
||||
ATTENDEES_ADD_REMOVE = 'Add/Remove Attendees'
|
||||
ATTENDEES_REMOVE = 'Remove Attendees'
|
||||
AUTHORIZATION_FILE_ALREADY_EXISTS = '{0} already exists. Please delete or rename it before attempting to {1} project.'
|
||||
AUTHENTICATION_FLOW_COMPLETE = '\nThe authentication flow has completed.'
|
||||
AUTHENTICATION_FLOW_COMPLETE_CLOSE_BROWSER = 'The authentication flow has completed. You may close this browser window and return to {0}.'
|
||||
AUTHENTICATION_FLOW_FAILED = 'The authentication flow failed, reissue command'
|
||||
BAD_ENTITIES_IN_SOURCE = '{0} {1} {2} in source marked >>> <<< above'
|
||||
BAD_REQUEST = 'Bad Request'
|
||||
BATCH = 'Batch'
|
||||
BATCH_CSV_LOOP_DASH_DEBUG_INCOMPATIBLE = '"gam {0} - ..." is not compatible with debugging. Disable debugging by setting debug_level = 0 in gam.cfg'
|
||||
BATCH_CSV_PROCESSING_COMPLETE = '{0},0/{1},Processing complete\n'
|
||||
BATCH_CSV_TERMINATE_N_PROCESSES = '{0},0/{1},Terminating {2} running {3}\n'
|
||||
BATCH_CSV_WAIT_LIMIT = ', wait limit {0} seconds'
|
||||
BATCH_CSV_WAIT_N_PROCESSES = '{0},0/{1},Waiting for {2} running {3} to finish before terminating{4}\n'
|
||||
BATCH_NOT_PROCESSED_ERRORS = '{0}batch file: {1}, not processed, {2} {3}\n'
|
||||
CALLING_GCLOUD_FOR_REAUTH = 'Calling gcloud for reauth credentials..."\n'
|
||||
CAN_NOT_DELETE_USER_WITH_VAULT_HOLD = '{0}: The user may be (or have recently been) on Google Vault Hold and thus not eligible for deletion. You can check holds with "gam user {1} show vaultholds".'
|
||||
CAN_NOT_BE_SPECIFIED_MORE_THAN_ONCE = 'Argument {0} can not be specified more than once'
|
||||
CHAT_ADMIN_ACCESS_LIMITED_TO_ONE_USER = 'Chat adminaccess|asadmin limited to one user, {0} specified'
|
||||
CHROME_TARGET_VERSION_FORMAT = r'^([a-z]+)-(\d+)$ or ^(\d{1,4}\.){1,4}$'
|
||||
COLUMN_DOES_NOT_MATCH_ANY_INPUT_COLUMNS = '{0} column "{1}" does not match any input columns'
|
||||
COLUMN_DOES_NOT_MATCH_ANY_OUTPUT_COLUMNS = '{0} column "{1}" does not match any output columns'
|
||||
COMMAND_NOT_COMPATIBLE_WITH_ENABLE_DASA = 'gam {0} {1} is not compatible with enable_dasa = true in gam.cfg'
|
||||
COMMIT_BATCH_COMPLETE = '{0},0/{1},commit-batch - running {2} finished, proceeding\n'
|
||||
COMMIT_BATCH_WAIT_N_PROCESSES = '{0},0/{1},commit-batch - waiting for {2} running {3} to finish before proceeding\n'
|
||||
CONFIRM_WIPE_YUBIKEY_PIV = 'This will wipe all YubiKey PIV keys and configuration from your YubiKey. Are you sure? (y/N) '
|
||||
CONTACT_ADMINISTRATOR_FOR_PASSWORD = 'Contact administrator for password'
|
||||
CONTACT_PHOTO_NOT_FOUND = 'Contact photo not found'
|
||||
CONTAINS_AT_LEAST_1_ITEM = 'Contains at least 1 item'
|
||||
COUNT_N_EXCEEDS_MAX_TO_PROCESS_M = 'Count {0} exceeds maximum to {1} {2}'
|
||||
CORRUPT_FILE = 'Corrupt file'
|
||||
COULD_NOT_FIND_ANY_YUBIKEY = 'Could not find any YubiKey\n'
|
||||
COULD_NOT_FIND_YUBIKEY_WITH_SERIAL = 'Could not find YubiKey with serial number {0}\n'
|
||||
CREATE_USER_NOTIFY_MESSAGE = 'Hello #givenname# #familyname#,\n\nYou have a new account at #domain#\nAccount details:\nUsername: #user#\nPassword: #password#\nStart using your new account by signing in at\nhttps://www.google.com/accounts/AccountChooser?Email=#user#&continue=https://workspace.google.com/dashboard\n'
|
||||
CREATE_USER_NOTIFY_SUBJECT = 'Welcome to #domain#'
|
||||
CSV_DATA_ALREADY_SAVED = 'CSV data already saved'
|
||||
CSV_FILE_HEADERS = 'The CSV file ({0}) has the following headers:\n'
|
||||
CSV_SAMPLE_COMMANDS = 'Here are the first {0} commands {1} will run\n'
|
||||
DATA_FIELD_MISMATCH = 'datafield {0} does not match saved datafield {1}'
|
||||
DATA_TRANSFER_COMPLETED = 'Data Transfer completed: {0}\n'
|
||||
DATA_UPLOADED_TO_DRIVE_FILE = 'Data uploaded to Drive File'
|
||||
DEFAULT_SMIME = 'Default S/MIME'
|
||||
DELETED = 'Deleted'
|
||||
DEVICE_LIST_BUG = 'GAM hit Google internal bug 237397223. Please file a Google Support ticket stating that you are encountering this bug.'
|
||||
DEVICE_LIST_BUG_WORKAROUND_NOT_POSSIBLE = 'GAM workaround for this issue only works if orderby argument is not used and query does not contain \'register\'.'
|
||||
DEVICE_LIST_BUG_ATTEMPTING_WORKAROUND = 'GAM is attempting to work around the bug by filtering for devices created on or after the newest we\'ve seen ({0})...\n'
|
||||
DIRECTLY_IN_THE = ' directly in the {0}'
|
||||
DISABLE_TLS_MIN_MAX = 'Execute: gam select default config tls_max_version "" tls_min_version "" save\n'
|
||||
DISPLAYNAME_NOT_ALLOWED_WHEN_UPDATING_MULTIPLE_SCHEMAS = 'displayname not allowed when updating multiple schemas'
|
||||
DOES_NOT_EXIST = 'Does not exist'
|
||||
DOES_NOT_EXIST_OR_HAS_INVALID_FORMAT = '{0}: {1}, Does not exist or has invalid format, {2}'
|
||||
DOES_NOT_MATCH = 'Does not match {0}'
|
||||
DOMAIN_NOT_FOUND_IN_DNS = 'Domain not found in DNS!'
|
||||
DOMAIN_NOT_VERIFIED_SECONDARY = 'Domain is not a verified secondary domain'
|
||||
DONE_GENERATING_PRIVATE_KEY_AND_PUBLIC_CERTIFICATE = 'Done generating private key and public certificate'
|
||||
DO_NOT_EXIST = 'Do not exist'
|
||||
DOWNLOADING_AGAIN_AND_OVER_WRITING = 'Downloading again and over-writing...'
|
||||
DUPLICATE = 'Duplicate'
|
||||
DUPLICATE_ALREADY_A_ROLE = 'Duplicate, already a {0}'
|
||||
DYNAMIC_GROUP_MEMBERSHIP_CANNOT_BE_MODIFIED = 'Dynamic group membership cannot be modified'
|
||||
EITHER = 'Either'
|
||||
EMAIL_ADDRESS_IS_UNMANAGED_ACCOUNT = 'The email address is an unmanaged account'
|
||||
ENABLE_PROJECT_APIS_AUTOMATICALLY_OR_MANUALLY = 'Do you want to enable project APIs [a]utomatically or [m]anually? (a/m): '
|
||||
ENTER_GSUITE_ADMIN_EMAIL_ADDRESS = '\nEnter your Google Workspace admin email address? '
|
||||
ENTER_MANAGE_GCP_PROJECT_EMAIL_ADDRESS = '\nEnter your Google Workspace admin or GCP project manager email address authorized to manage project(s): {0}? '
|
||||
ENTER_VERIFICATION_CODE_OR_URL = 'Enter verification code or paste "Unable to connect" URL from other computer (only URL data up to &scope required): '
|
||||
ENTITY_DOES_NOT_EXIST = '{0} does not exist'
|
||||
ENTITY_NAME_NOT_VALID = 'Entity Name Not Valid'
|
||||
ERROR = 'error'
|
||||
ERRORS = 'errors'
|
||||
EVENT_IS_CANCELED = 'Event is canceled'
|
||||
EXECUTE_GAM_OAUTH_CREATE = '\nPlease run\n\ngam oauth delete\ngam oauth create\n\n'
|
||||
EXISTS = 'Exists'
|
||||
EXPECTED = 'Expected'
|
||||
EXPORT_NOT_COMPLETE = 'Export needs to be complete before downloading, current status is: {0}'
|
||||
EXTRACTING_PUBLIC_CERTIFICATE = 'Extracting public certificate'
|
||||
FAILED_PRECONDITION = 'Failed precondition'
|
||||
FAILED_TO_PARSE_AS_JSON = 'Failed to parse as JSON'
|
||||
FAILED_TO_PARSE_AS_LIST = 'Failed to parse as list'
|
||||
FIELD_NOT_FOUND_IN_SCHEMA = 'Field {0} not found in schema {1}'
|
||||
FILE_NOT_FOUND = 'File {0} not found'
|
||||
FINISHED = 'Finished'
|
||||
FILTER_CAN_ONLY_CONTAIN_ONE_CATEGORY_LABEL = 'Filter can only contain one CATEGORY label'
|
||||
FILTER_CAN_ONLY_CONTAIN_ONE_USER_LABEL = 'Filter can only contain one USER label'
|
||||
FOR = 'for'
|
||||
FORBIDDEN = 'Forbidden'
|
||||
FORMAT_NOT_AVAILABLE = 'Format ({0}) not available'
|
||||
FORMAT_NOT_DOWNLOADABLE = 'Format not downloadable'
|
||||
FROM = 'From'
|
||||
FROM_LC = 'from'
|
||||
FULL_PATH_MUST_START_WITH_DRIVE = 'fullpath must start with {0} or {1}'
|
||||
GAM_BATCH_FILE_WRITTEN = 'GAM batch file {0} written\n'
|
||||
GAM_LATEST_VERSION_NOT_AVAILABLE = 'GAM Latest Version information not available'
|
||||
GAM_OUT_OF_MEMORY = 'GAM has run out of memory. If this is a large Google Workspace instance, you should use a 64-bit version of GAM on Windows or a 64-bit version of Python on other systems.'
|
||||
GENERATING_NEW_PRIVATE_KEY = 'Generating new private key'
|
||||
GETTING = 'Getting'
|
||||
GETTING_ALL = 'Getting all'
|
||||
GOOGLE_DELEGATION_ERROR = 'Google delegation error, delegator and delegate both exist and are valid for delegation'
|
||||
GOT = 'Got'
|
||||
GROUP_MAPS_TO_MULTIPLE_OUS = 'File: {0}, Group: {1} references multiple OUs: {2}'
|
||||
GROUP_MAPS_TO_OU_INVALID_ROW = 'File: {0}, Invalid row, must contain non-blank <EmailAddress> and <OrgUnitPath>: <{1}> <{2}>'
|
||||
GUARDIAN_INVITATION_STATUS_NOT_PENDING = 'Guardian invitation status is not PENDING'
|
||||
HAS_CHILD_ORGS = 'Has child {0}'
|
||||
HAS_INVALID_FORMAT = '{0}: {1}, Has invalid format'
|
||||
HAS_RIGHTS_TO_ROTATE_OWN_PRIVATE_KEY = 'Giving account {0} rights to rotate {1} private key'
|
||||
HEADER_NOT_FOUND_IN_CSV_HEADERS = 'Header "{0}" not found in CSV headers of "{1}".'
|
||||
HELP_SYNTAX = 'Help: Syntax in file {0}\n'
|
||||
HELP_WIKI = 'Help: Documentation is at {0}\n'
|
||||
IGNORED = 'Ignored'
|
||||
INSTRUCTIONS_CLIENT_SECRETS_JSON = 'Please run\n\ngam create|use project\ngam oauth create\n\nto create and authorize a Client account.\n'
|
||||
INSTRUCTIONS_OAUTH2SERVICE_JSON = 'Please run\n\ngam create|use project\ngam user <user> check serviceaccount\n\nto create and authorize a Service account.\n'
|
||||
INSUFFICIENT_PERMISSIONS_TO_PERFORM_TASK = 'Insufficient permissions to perform this task'
|
||||
INTER_BATCH_WAIT_INCREASED = 'inter_batch_wait increased to {0:.2f}'
|
||||
INVALID = 'Invalid'
|
||||
INVALID_ALIAS = 'Invalid Alias'
|
||||
INVALID_ATTENDEE_CHANGE = 'Invalid attendee change "{0}"'
|
||||
INVALID_CHARSET = 'Invalid charset "{0}"'
|
||||
INVALID_DATE_TIME_RANGE = '{0} {1} must be greater than/equal to {2} {3}'
|
||||
INVALID_ENTITY = 'Invalid {0}, {1}'
|
||||
INVALID_FILE_SELECTION_WITH_ADMIN_ACCESS = 'Invalid file selection with adminaccess|asadmin'
|
||||
INVALID_GROUP = 'Invalid Group'
|
||||
INVALID_HTTP_HEADER = 'Invalid http header data: {0}'
|
||||
INVALID_JSON_INFORMATION = 'Google API reported Invalid JSON Information'
|
||||
INVALID_JSON_SETTING = 'Invalid JSON setting'
|
||||
INVALID_LIST = 'Invalid list'
|
||||
INVALID_MEMBER = 'Invalid Member address'
|
||||
INVALID_MESSAGE_ID = 'Invalid message id(s)'
|
||||
INVALID_MIMETYPE = 'Invalid mimeType {0}, must be {1}'
|
||||
INVALID_NUMBER_OF_CHAT_SPACE_MEMBERS = '{0} type {1} number of members, {2}, must be in range {3} to {4}'
|
||||
INVALID_ORGUNIT = 'Invalid Organizational Unit'
|
||||
INVALID_PATH = 'Invalid Path'
|
||||
INVALID_PERMISSION_ATTRIBUTE_TYPE = 'permission attribute {0} not allowed with type {1}'
|
||||
INVALID_REGION = 'See: https://github.com/taers232c/GAMADV-XTD3/wiki/Context-Aware-Access-Levels#caa-region-codes'
|
||||
INVALID_QUERY = 'Invalid Query'
|
||||
INVALID_RE = 'Invalid RE'
|
||||
INVALID_REQUEST = 'Invalid Request'
|
||||
INVALID_RESELLER_CUSTOMER_NAME = 'name must be: accounts/<ResellerID>/customers/<ChannelCustomerID>'
|
||||
INVALID_ROLE = 'Invalid subkeyfield Role, must be one of: {0}'
|
||||
INVALID_SCHEMA_VALUE = 'Invalid Schema Value'
|
||||
INVALID_SCOPE = 'Invalid Scope'
|
||||
INVALID_SITE = 'Invalid Site ({0}), must match pattern ({1})'
|
||||
INVALID_TAG_SPECIFICATION = 'Invalid tag, expected field.subfield or field.subfield.subfield.string'
|
||||
INVALID_TIMEOFDAY_RANGE = '{0} must be less than/equal to {1}'
|
||||
IN_SKIPIDS = 'In skipids'
|
||||
IN_THE = ' in the {0}'
|
||||
IN_TRASH_AND_EXCLUDE_TRASHED = 'In Trash and excludeTrashed'
|
||||
IS_EXPIRED_OR_REVOKED = '{0}: {1}, Is expired or has been revoked'
|
||||
IS_NOT_DONE_CHECKING_IN_SECONDS = 'Is not done, checking again in {0} seconds'
|
||||
IS_NOT_UNIQUE = 'Is not unique, {0}: {1}'
|
||||
IS_REQD_TO_CHG_PWD_NO_DELEGATION = 'Is required to change password at next login. You must change password or clear changepassword flag for delegation.'
|
||||
IS_SUSPENDED_NO_BACKUPCODES = 'User is suspended. You must unsuspend to process backupcodes'
|
||||
IS_SUSPENDED_NO_DELEGATION = 'Is suspended. You must unsuspend for delegation.'
|
||||
IS_YUBIKEY_INSERTED = 'Is YubiKey inserted?'
|
||||
JSON_ERROR = 'JSON error "{0}" in file {1}'
|
||||
JSON_KEY_NOT_FOUND = 'JSON key "{0}" not found in file {1}'
|
||||
KIOSK_MODE_REQUIRED = ' This command ({0}) requires that the ChromeOS device be in Kiosk mode.'
|
||||
LESS_THAN_1_SECOND = 'less than 1 second'
|
||||
LIST_CHROMEOS_INVALID_INPUT_PAGE_TOKEN_RETRY = 'List ChromeOSdevices Invalid Input: pageToken retry'
|
||||
LOGGING_INITIALIZATION_ERROR = 'Logging initialization error: {0}'
|
||||
LOOKING_UP_GOOGLE_UNIQUE_ID = 'Looking up Google Unique ID'
|
||||
MARKED_AS = 'Marked as'
|
||||
MATCHED_THE_FOLLOWING = 'Matched the following'
|
||||
MATTER_NOT_OPEN = 'Matter needs to be open, current state is: {0}'
|
||||
MAXIMUM_OF = 'maximum of'
|
||||
MEMBERSHIP_IS_PENDING_WILL_DELETE_ADD_TO_ACCEPT = 'Membership is pending, will delete and add to accept'
|
||||
MIMETYPE_MISMATCH = 'Shortcut target mimeType {0} does not match actual target mimeType {1}'
|
||||
MIMETYPE_NOT_PRESENT_IN_ATTACHMENT = 'MIME type not present in attachment'
|
||||
MISMATCH_RE_SEARCH_REPLACE_SUBFIELDS = 'The subfield ({2}) in replace "{3}" exceeds the number of subfields ({0}) in search "{1}"'
|
||||
MISMATCH_SEARCH_REPLACE_SUBFIELDS = 'The number of subfields ({0}) in search "{1}" does not match the number of subfields ({2}) in replace "{3}"'
|
||||
MISSING_FIELDS = 'Missing fields: {0}\n'
|
||||
MULTIPLE_BUILDINGS_SAME_NAME = '{0} {1} with the same (case-insensitive) name exist'
|
||||
MULTIPLE_ENTITIES_FOUND = 'Multiple {0} ({1}) found, {2}'
|
||||
MULTIPLE_ITEMS_SPECIFIED = 'Multiple {0} are specfied, only one is allowed'
|
||||
MULTIPLE_ITEMS_MARKED_PRIMARY = 'Multiple {0} are marked primary, only one can be primary'
|
||||
MULTIPLE_PARENTS_SPECIFIED = 'Multiple parents ({0}) specified, only one is allowed'
|
||||
MULTIPLE_SEARCH_METHODS_SPECIFIED = 'Multiple search methods ({0}) specified, only one is allowed'
|
||||
MULTIPLE_SSO_PROFILES_MATCH = 'Multiple SSO profiles match display name {0}:\n'
|
||||
MULTIPLE_YUBIKEYS_CONNECTED = 'Multiple YubiKeys connected. Specify yubikey_serial_number and one of {0}\n'
|
||||
MUST_BE_NUMERIC = 'Must be numeric'
|
||||
NEED_READ_ACCESS = 'Need Read access'
|
||||
NEED_READ_WRITE_ACCESS = 'Need Read/Write access'
|
||||
NEED_WRITE_ACCESS = 'Need Write access'
|
||||
NESTED_LOOP_CMD_NOT_ALLOWED = 'Command can not be nested.'
|
||||
NEWUSER_REQUIREMENTS = 'newuser option requires: at least 1 recipient and givenname, familyname and password options'
|
||||
NEW_OWNER_MUST_DIFFER_FROM_OLD_OWNER = 'New owner must differ from old owner'
|
||||
NO_DATA = 'No data'
|
||||
NON_BLANK = 'Non-blank'
|
||||
NON_EMPTY = 'Non-empty'
|
||||
NOT_A = 'Not a'
|
||||
NOT_A_PRIMARY_EMAIL_ADDRESS = 'Not a primary email address'
|
||||
NOT_A_MEMBER = 'Not a member'
|
||||
NOT_ACTIVE = 'Not Active'
|
||||
NOT_ALLOWED = 'Not Allowed'
|
||||
NOT_AN_ENTITY = 'Not a {0}'
|
||||
NOT_APPROPRIATE = 'Not Appropriate'
|
||||
NOT_COMPATIBLE = 'Not Compatible'
|
||||
NOT_COPYABLE = 'Not Copyable'
|
||||
NOT_COPYABLE_INTO_ITSELF = 'Not copyable into itself'
|
||||
NOT_COPYABLE_SAME_NAME_CURRENT_FOLDER_MERGE = 'Not copyable with same name into current folder with duplicatefolders merge'
|
||||
NOT_COPYABLE_SAME_NAME_CURRENT_FOLDER_OVERWRITE = 'Not copyable with same name into current folder with duplicatefiles overwriteall|overwriteolder'
|
||||
NOT_DELETABLE = 'Not Deletable'
|
||||
NOT_FOUND = 'Not Found'
|
||||
NOT_MOVABLE = 'Not Movable'
|
||||
NOT_MOVABLE_IN_TRASH = 'Not Movable, in Trash'
|
||||
NOT_MOVABLE_INTO_ITSELF = 'Not movable into itself'
|
||||
NOT_MOVABLE_SAME_NAME_CURRENT_FOLDER_MERGE = 'Not movable with same name into current folder with duplicatefolders merge'
|
||||
NOT_MOVABLE_SAME_NAME_CURRENT_FOLDER_OVERWRITE = 'Not movable with same name into current folder with duplicatefiles overwriteall|overwriteolder'
|
||||
NOT_OWNED_BY = 'Not owned by {0}'
|
||||
NOT_SELECTED = 'Not Selected'
|
||||
NOT_WRITABLE = 'Not Writable'
|
||||
NOW_THE_PRIMARY_DOMAIN = 'Now the primary domain'
|
||||
NO_ACTION_SPECIFIED = 'No action specified'
|
||||
NO_AVAILABLE_LICENSES = "There aren't enough available licenses for the specified product-SKU pair(s)"
|
||||
NO_CHANGES = 'No changes'
|
||||
NO_CLIENT_ACCESS_ALLOWED = 'No Client Access allowed'
|
||||
NO_CLIENT_ACCESS_CREATE_UPDATE_ALLOWED = 'No Client Access create/update allowed'
|
||||
NO_COLUMNS_SELECTED_WITH_CSV_OUTPUT_HEADER_FILTER = 'No columns selected with {0} and {1}'
|
||||
NO_CREDENTIALS_REPLACEMENT = '{0}: {1} has {2} {3}. We only replace if there are 2.\n'
|
||||
NO_CSV_DATA_TO_UPLOAD = 'No CSV data to upload'
|
||||
NO_CSV_FILE_DATA_FOUND = 'No CSV file data found'
|
||||
NO_CSV_FILE_DATA_SAVED = 'No CSV file data saved'
|
||||
NO_CSV_FILE_SUBKEYS_SAVED = 'No CSV file subkeys saved'
|
||||
NO_DATA_TRANSFER_APP_FOR_PARAMETER = 'No data transfer application for key {0}'
|
||||
NO_ENTITIES_FOUND = 'No {0} found'
|
||||
NO_ENTITIES_MATCHED = 'No {0} matched'
|
||||
NO_FILTER_ACTIONS = 'No {0} actions specified'
|
||||
NO_FILTER_CRITERIA = 'No {0} criteria specified'
|
||||
NO_LABELS_MATCH = 'No Labels match'
|
||||
NO_LABELS_TO_PROCESS = 'No Labels to process'
|
||||
NO_MESSAGES_WITH_LABEL = 'No Messages with Label'
|
||||
NO_PARENTS_TO_CONVERT_TO_SHORTCUTS = 'No parents to convert to shortcuts'
|
||||
NO_REPORT_AVAILABLE = 'No {0} report available.'
|
||||
NO_SCOPES_FOR_API = 'There are no scopes authorized for the {0}'
|
||||
NO_SERIAL_NUMBERS_SPECIFIED = 'No serial numbers specified'
|
||||
NO_SSO_PROFILE_MATCHES = 'No SSO profile matches display name {0}'
|
||||
NO_SSO_PROFILE_ASSIGNED = 'No SSO profile assigned to {0} {1}'
|
||||
NO_SVCACCT_ACCESS_ALLOWED = 'No Service Account Access allowed'
|
||||
NO_TRANSFER_LACK_OF_DISK_SPACE = 'Transfer not performed due to lack of target drive space.'
|
||||
NO_USAGE_PARAMETERS_DATA_AVAILABLE = 'No usage parameters data available.'
|
||||
NO_USER_COUNTS_DATA_AVAILABLE = 'No User counts data available.'
|
||||
OAUTH2_GO_TO_LINK_MESSAGE = """
|
||||
Go to the following link in a browser on this computer or on another computer:
|
||||
|
||||
{url}
|
||||
|
||||
If you use a browser on another computer, you will get a browser error that the site can't be reached AFTER you
|
||||
click the Allow button, paste "Unable to connect" URL from other computer (only URL data up to &scope required):
|
||||
"""
|
||||
ON_CURRENT_PRIVATE_KEY = ' on current key'
|
||||
ON_VAULT_HOLD = 'On Google Vault Hold'
|
||||
ONLY_ADMINISTRATORS_CAN_PERFORM_SHARED_DRIVE_QUERIES = 'Only administrators can perform Shared Drive queries'
|
||||
ONLY_ADMINISTRATORS_CAN_SPECIFY_SHARED_DRIVE_ORGUNIT = 'Only administrators can specify Shared Drive Org Unit'
|
||||
ONLY_ONE_DEVICE_SELECTION_ALLOWED = 'Only one device selection allowed, filter = "{0}"'
|
||||
ONLY_ONE_JSON_RANGE_ALLOWED = 'Only one range/json allowed'
|
||||
ONLY_ONE_OWNER_ALLOWED = 'Only one owner allowed'
|
||||
OR = 'or'
|
||||
OU_AND_MOVETOOU_CANNOT_BE_IDENTICAL = 'ou {0} can not be be identical to movetoou {1}'
|
||||
OU_SUBOUS_CANNOT_BE_MOVED_TO_MOVETOOU = 'ou {0} sub OUs can not be be moved to movetoou {1}'
|
||||
PERMISSION_DENIED = 'The caller does not have permission'
|
||||
PLEASE_CORRECT_YOUR_SYSTEM_TIME = 'Please correct your system time.'
|
||||
PLEASE_ENTER_A_OR_M = 'Please enter a or m ...\n'
|
||||
PLEASE_SELECT_ENTITY_TO_PROCESS = '{0} {1} found, please select the correct one to {2} and specify with {3}'
|
||||
PLEASE_SPECIFY_BUILDING_EXACT_CASE_NAME_OR_ID = 'Please specify building by exact case name or ID.'
|
||||
PREVIEW_ONLY = 'Preview Only'
|
||||
PRIMARY_EMAIL_DID_NOT_MATCH_PATTERN = 'primaryEmail address did not match pattern: {0}'
|
||||
PROCESS = 'process'
|
||||
PROCESSES = 'processes'
|
||||
PROCESSING_ITEM_N = '{0},0,Processing item {1}\n'
|
||||
PROCESSING_ITEM_N_OF_M = '{0},0,Processing item {1}/{2}\n'
|
||||
PROFILE_PHOTO_NOT_FOUND = 'Profile photo not found'
|
||||
PROFILE_PHOTO_IS_DEFAULT = 'Profile photo is default'
|
||||
REASON_ONLY_VALID_WITH_CONTENTRESTRICTIONS_READONLY_TRUE = 'reason only valid with contentrestrictions readonly true'
|
||||
REAUTHENTICATION_IS_NEEDED = 'Reauthentication is needed, please run\n\ngam oauth create'
|
||||
RECOMMEND_RUNNING_GAM_ROTATE_SAKEY = 'Recommend running "gam rotate sakey" to get a new key\n'
|
||||
REFUSING_TO_DEPROVISION_DEVICES = 'Refusing to deprovision {0} devices because acknowledge_device_touch_requirement not specified.\nDeprovisioning a device means the device will have to be physically wiped and re-enrolled to be managed by your domain again.\nThis requires physical access to the device and is very time consuming to perform for each device.\nPlease add "acknowledge_device_touch_requirement" to the GAM command if you understand this and wish to proceed with the deprovision.\nPlease also be aware that deprovisioning can have an effect on your device license count.\nSee https://support.google.com/chrome/a/answer/3523633 for full details.'
|
||||
REPLY_TO_CUSTOM_REQUIRES_EMAIL_ADDRESS = 'replyto REPLY_TO_CUSTOM requires customReplyTo <EmailAddress>'
|
||||
REQUEST_COMPLETED_NO_FILES = 'Request completed but no results/files were returned, try requesting again'
|
||||
REQUEST_NOT_COMPLETE = 'Request needs to be completed before downloading, current status is: {0}'
|
||||
RESOURCE_CAPACITY_FLOOR_REQUIRED = 'Options "capacity <Number>" (<Number> > 0) and "floor <String>" required'
|
||||
RESOURCE_FLOOR_REQUIRED = 'Option "floor <String>" required'
|
||||
RESULTS_TOO_LARGE_FOR_GOOGLE_SPREADSHEET = 'Results are too large for Google Spreadsheets. Uploading as a regular CSV file.'
|
||||
RETRIES_EXHAUSTED = 'Retries {0} exhausted'
|
||||
RETRYING_GOOGLE_SHEET_EXPORT_SLEEPING = 'Retrying Google Sheet export {0}/{1}. Sleeping {2} seconds\n'
|
||||
ROLE_MUST_BE_ORGANIZER = 'Role must be organizer'
|
||||
ROLE_NOT_IN_SET = 'Role not in set: {0})'
|
||||
SCHEMA_WOULD_HAVE_NO_FIELDS = '{0} would have no {1}'
|
||||
SELECTED = 'Selected'
|
||||
SERVICE_NOT_APPLICABLE = 'Service not applicable/Does not exist'
|
||||
SERVICE_NOT_APPLICABLE_THIS_ADDRESS = 'Service not applicable for this address: {0}'
|
||||
SERVICE_NOT_ENABLED = '{0} Service/App not enabled'
|
||||
SHORTCUT_TARGET_CAPABILITY_IS_FALSE = '{0} capability {1} is False'
|
||||
STARTING_THREAD = 'Starting thread'
|
||||
STATISTICS_COPY_FILE = 'Total: {0}, Copied: {1}, Shortcut created {2}, Shortcut exists {3}, Duplicate: {4}, Copy Failed: {5}, Not copyable: {6}, In skipids: {7}, Permissions Failed: {8}, Protected Ranges Failed: {9}'
|
||||
STATISTICS_COPY_FOLDER = 'Total: {0}, Copied: {1}, Shortcut created {2}, Shortcut exists {3}, Duplicate: {4}, Merged: {5}, Copy Failed: {6}, Not writable: {7}, Permissions Failed: {8}'
|
||||
STATISTICS_MOVE_FILE = 'Total: {0}, Moved: {1}, Shortcut created {2}, Shortcut exists {3}, Duplicate: {4}, Move Failed: {5}, Not movable: {6}'
|
||||
STATISTICS_MOVE_FOLDER = 'Total: {0}, Moved: {1}, Shortcut created {2}, Shortcut exists {3}, Duplicate: {4}, Merged: {5}, Move Failed: {6}, Not writable: {7}'
|
||||
STATISTICS_USER_NOT_ORGANIZER = 'User not organizer: {0}'
|
||||
STRING_LENGTH = 'string length'
|
||||
SUBKEY_FIELD_MISMATCH = 'subkeyfield {0} does not match saved subkeyfield {1}'
|
||||
SUBSCRIPTION_NOT_FOUND = 'Could not find subscription'
|
||||
SUFFIX_NOT_ALLOWED_WITH_CUSTOMLANGUAGE = 'Suffix {0} not allowed with customLanguage {1}'
|
||||
TASKLIST_TITLE_NOT_FOUND = 'Task list title not found'
|
||||
THREAD = 'thread'
|
||||
THREADS = 'threads'
|
||||
TO = 'To'
|
||||
TO_LC = 'to'
|
||||
TO_MAXIMUM_OF = 'to maximum of'
|
||||
TO_SET_UP_GOOGLE_CHAT = """
|
||||
To set up Google Chat for your API project, please go to:
|
||||
|
||||
{0}
|
||||
|
||||
and complete all fields.
|
||||
"""
|
||||
TOTAL_ITEMS_IN_ENTITY = 'Total {0} in {1}'
|
||||
TRIMMED_MESSAGE_FROM_LENGTH_TO_MAXIMUM = 'Trimmed message of length {0} to maximum length {1}'
|
||||
UNABLE_TO_GET_PERMISSION_ID = 'Unable to get Permission ID for <{0}>'
|
||||
UNABLE_TO_CREATE_NOT_FOUND_USER = 'Unable to create not found user, some required field (givenName, familyName, password/notfoundpassword) not present'
|
||||
UNAVAILABLE = 'Unavailable'
|
||||
UNKNOWN = 'Unknown'
|
||||
UNKNOWN_API_OR_VERSION = 'Unknown Google API or version: ({0}), contact {1}'
|
||||
UNRECOVERABLE_ERROR = 'Unrecoverable error'
|
||||
UPDATE_ATTENDEE_CHANGES = 'Update attendee changes'
|
||||
UPDATE_GAM_TO_64BIT = "You're running a 32-bit version of GAM on a 64-bit version of Windows, upgrade to a windows-x86_64 version of GAM"
|
||||
UPDATE_USER_PASSWORD_CHANGE_NOTIFY_MESSAGE = 'The account password for #givenname# #familyname#, #user# has been changed to: #password#\n'
|
||||
UPDATE_USER_PASSWORD_CHANGE_NOTIFY_SUBJECT = 'Account #user# password has been changed'
|
||||
UPLOAD_CSV_FILE_INTERNAL_ERROR = 'Google reported "{0}" but the file was probably uploaded, check that it has {1} rows'
|
||||
UPLOADING_NEW_PUBLIC_CERTIFICATE_TO_GOOGLE = 'Uploading new public certificate to Google...\n'
|
||||
URL_ERROR = 'URL error: {0}'
|
||||
USE_MIMETYPE_TO_SPECIFY_GOOGLE_FORMAT = 'Use "mimetype <MimeType>" to specify Google file format\n'
|
||||
USED = 'Used'
|
||||
USER_BELONGS_TO_N_GROUPS_THAT_MAP_TO_ORGUNITS = 'User belongs to {0} groups ({1}) that map to OUs'
|
||||
USER_CANCELLED = 'User cancelled'
|
||||
USER_HAS_MULTIPLE_DIRECT_OR_INHERITED_MEMBERSHIPS_IN_GROUP = 'User has multiple direct or inherited memberships in group'
|
||||
USER_IN_OTHER_DOMAIN = '{0}: {1} in other domain.'
|
||||
USER_IS_NOT_ORGANIZER = 'User is not organizer, use anyorganizer option to override'
|
||||
USER_NOT_IN_MATCHUSERS = 'User not in matchusers'
|
||||
USER_SUBS_NOT_ALLOWED_TAG_REPLACEMENT = 'user substitutions not allowed in replace <Tag> <String>'
|
||||
USE_DOIT_ARGUMENT_TO_PERFORM_ACTION = 'Use the "doit" argument to perform action'
|
||||
USING_N_PROCESSES = '{0},0/{1},Using {2} {3}...\n'
|
||||
VALUES_ARE_NOT_CONSISTENT = 'Values are not consistent'
|
||||
VERSION_UPDATE_AVAILABLE = 'Version update available'
|
||||
WAITING_FOR_DATA_TRANSFER_TO_COMPLETE_SLEEPING = 'Waiting for Data Transfer to complete. Sleeping {0} seconds\n'
|
||||
WAITING_FOR_ITEM_CREATION_TO_COMPLETE_SLEEPING = 'Waiting for {0} creation to complete. Sleeping {1} seconds\n'
|
||||
WHAT_IS_YOUR_PROJECT_ID = '\nWhat is your project ID? '
|
||||
WILL_RERUN_WITH_NO_BROWSER_TRUE = 'Will re-run command with no_browser true\n'
|
||||
WITH = 'with'
|
||||
WOULD_MAKE_MEMBERSHIP_CYCLE = 'Would make membership cycle'
|
||||
WRITER_ACCESS_REQUIRED_TO_BOTH_CALENDARS = 'Writer access required to both calendars'
|
||||
WROTE_PRIVATE_KEY_DATA = 'Wrote private key data to {0}\n'
|
||||
WROTE_PUBLIC_CERTIFICATE = 'Wrote public certificate to {0}\n'
|
||||
YOU_CAN_ADD_DOMAIN_TO_ACCOUNT = 'You can now add: {0} or its subdomains as secondary or domain aliases of the Google Workspace Account: {1}'
|
||||
YUBIKEY_GENERATING_NONEXPORTABLE_PRIVATE_KEY = 'YubiKey is generating a non-exportable private key...\n'
|
||||
YUBIKEY_PIN_SET_TO = 'YubiKey PIN set to: {0}\n'
|
||||
244
src/gam/gamlib/glskus.py
Normal file
244
src/gam/gamlib/glskus.py
Normal file
@@ -0,0 +1,244 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2023 Ross Scroggs All Rights Reserved.
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Google SKUs
|
||||
|
||||
"""
|
||||
|
||||
# Products/SKUs
|
||||
_PRODUCTS = {
|
||||
'101001': 'Cloud Identity Free',
|
||||
'101005': 'Cloud Identity Premium',
|
||||
'101031': 'Google Workspace for Education',
|
||||
'101033': 'Google Voice',
|
||||
'101034': 'Google Workspace Archived User',
|
||||
'101035': 'Cloud Search',
|
||||
'101036': 'Google Meet Global Dialing',
|
||||
'101037': 'Google Workspace for Education',
|
||||
'101038': 'AppSheet',
|
||||
'101039': 'Assured Controls',
|
||||
'101040': 'Chrome Enterprise',
|
||||
'101043': 'Google Workspace Additional Storage',
|
||||
'101047': 'Gemini',
|
||||
'101049': 'Education Endpoint Management',
|
||||
'101050': 'Colab',
|
||||
'Google-Apps': 'Google Workspace',
|
||||
'Google-Chrome-Device-Management': 'Google Chrome Device Management',
|
||||
'Google-Drive-storage': 'Google Drive Storage',
|
||||
'Google-Vault': 'Google Vault',
|
||||
}
|
||||
_SKUS = {
|
||||
'1010010001': {
|
||||
'product': '101001', 'aliases': ['identity', 'cloudidentity'], 'displayName': 'Cloud Identity'},
|
||||
'1010050001': {
|
||||
'product': '101005', 'aliases': ['identitypremium', 'cloudidentitypremium'], 'displayName': 'Cloud Identity Premium'},
|
||||
'1010310002': {
|
||||
'product': '101031', 'aliases': ['gsefe', 'e4e', 'gsuiteenterpriseeducation'], 'displayName': 'Google Workspace for Education Plus - Legacy'},
|
||||
'1010310003': {
|
||||
'product': '101031', 'aliases': ['gsefes', 'e4es', 'gsuiteenterpriseeducationstudent'], 'displayName': 'Google Workspace for Education Plus - Legacy (Student)'},
|
||||
'1010310005': {
|
||||
'product': '101031', 'aliases': ['gwes', 'workspaceeducationstandard'], 'displayName': 'Google Workspace for Education Standard'},
|
||||
'1010310006': {
|
||||
'product': '101031', 'aliases': ['gwesstaff', 'workspaceeducationstandardstaff'], 'displayName': 'Google Workspace for Education Standard (Staff)'},
|
||||
'1010310007': {
|
||||
'product': '101031', 'aliases': ['gwesstudent', 'workspaceeducationstandardstudent'], 'displayName': 'Google Workspace for Education Standard (Extra Student)'},
|
||||
'1010310008': {
|
||||
'product': '101031', 'aliases': ['gwep', 'workspaceeducationplus'], 'displayName': 'Google Workspace for Education Plus'},
|
||||
'1010310009': {
|
||||
'product': '101031', 'aliases': ['gwepstaff', 'workspaceeducationplusstaff'], 'displayName': 'Google Workspace for Education Plus (Staff)'},
|
||||
'1010310010': {
|
||||
'product': '101031', 'aliases': ['gwepstudent', 'workspaceeducationplusstudent'], 'displayName': 'Google Workspace for Education Plus (Extra Student)'},
|
||||
'1010330002': {
|
||||
'product': '101033', 'aliases': ['gvpremier', 'voicepremier', 'googlevoicepremier'], 'displayName': 'Google Voice Premier'},
|
||||
'1010330003': {
|
||||
'product': '101033', 'aliases': ['gvstarter', 'voicestarter', 'googlevoicestarter'], 'displayName': 'Google Voice Starter'},
|
||||
'1010330004': {
|
||||
'product': '101033', 'aliases': ['gvstandard', 'voicestandard', 'googlevoicestandard'], 'displayName': 'Google Voice Standard'},
|
||||
'1010350001': {
|
||||
'product': '101035', 'aliases': ['cloudsearch'], 'displayName': 'Cloud Search'},
|
||||
'1010360001': {
|
||||
'product': '101036', 'aliases': ['meetdialing','googlemeetglobaldialing'], 'displayName': 'Google Meet Global Dialing'},
|
||||
'1010370001': {
|
||||
'product': '101037', 'aliases': ['gwetlu', 'workspaceeducationupgrade'], 'displayName': 'Google Workspace for Education: Teaching and Learning Upgrade'},
|
||||
'1010380001': {
|
||||
'product': '101038', 'aliases': ['appsheetcore'], 'displayName': 'AppSheet Core'},
|
||||
'1010380002': {
|
||||
'product': '101038', 'aliases': ['appsheetstandard', 'appsheetenterprisestandard'], 'displayName': 'AppSheet Enterprise Standard'},
|
||||
'1010380003': {
|
||||
'product': '101038', 'aliases': ['appsheetplus', 'appsheetenterpriseplus'], 'displayName': 'AppSheet Enterprise Plus'},
|
||||
'1010390001': {
|
||||
'product': '101039', 'aliases': ['assuredcontrols'], 'displayName': 'Assured Controls'},
|
||||
'1010400001': {
|
||||
'product': '101040', 'aliases': ['beyondcorp', 'beyondcorpenterprise', 'bce', 'cep', 'chromeenterprisepremium'], 'displayName': 'Chrome Enterprise Premium'},
|
||||
'1010430001': {
|
||||
'product': '101043', 'aliases': ['gwas', 'plusstorage'], 'displayName': 'Google Workspace Additional Storage'},
|
||||
'1010470001': {
|
||||
'product': '101047', 'aliases': ['geminient', 'duetai'], 'displayName': 'Gemini Enterprise'},
|
||||
'1010470002': {
|
||||
'product': '101047', 'aliases': ['gwlabs', 'workspacelabs'], 'displayName': 'Google Workspace Labs'},
|
||||
'1010470003': {
|
||||
'product': '101047', 'aliases': ['geminibiz'], 'displayName': 'Gemini Business'},
|
||||
'1010470004': {
|
||||
'product': '101047', 'aliases': ['geminiedu'], 'displayName': 'Gemini Education'},
|
||||
'1010470005': {
|
||||
'product': '101047', 'aliases': ['geminiedupremium'], 'displayName': 'Gemini Education Premium'},
|
||||
'1010470006': {
|
||||
'product': '101047', 'aliases': ['aisecurity'], 'displayName': 'AI Security'},
|
||||
'1010470007': {
|
||||
'product': '101047', 'aliases': ['aimeetingsandmessaging'], 'displayName': 'AI Meetings and Messaging'},
|
||||
'1010490001': {
|
||||
'product': '101049', 'aliases': ['eeu'], 'displayName': 'Endpoint Education Upgrade'},
|
||||
'1010500001': {
|
||||
'product': '101050', 'aliases': ['colabpro'], 'displayName': 'Colab Pro'},
|
||||
'1010500002': {
|
||||
'product': '101050', 'aliases': ['colabpro+', 'colabproplus'], 'displayName': 'Colab Pro+'},
|
||||
'Google-Apps': {
|
||||
'product': 'Google-Apps', 'aliases': ['standard', 'free'], 'displayName': 'G Suite Legacy'},
|
||||
'Google-Apps-For-Business': {
|
||||
'product': 'Google-Apps', 'aliases': ['gafb', 'gafw', 'basic', 'gsuitebasic'], 'displayName': 'G Suite Basic'},
|
||||
'Google-Apps-For-Government': {
|
||||
'product': 'Google-Apps', 'aliases': ['gafg', 'gsuitegovernment', 'gsuitegov'], 'displayName': 'Google Workspace Government'},
|
||||
'Google-Apps-For-Postini': {
|
||||
'product': 'Google-Apps', 'aliases': ['gams', 'postini', 'gsuitegams', 'gsuitepostini', 'gsuitemessagesecurity'], 'displayName': 'Google Apps Message Security'},
|
||||
'Google-Apps-Lite': {
|
||||
'product': 'Google-Apps', 'aliases': ['gal', 'gsl', 'lite', 'gsuitelite'], 'displayName': 'G Suite Lite'},
|
||||
'Google-Apps-Unlimited': {
|
||||
'product': 'Google-Apps', 'aliases': ['gau', 'gsb', 'unlimited', 'gsuitebusiness'], 'displayName': 'G Suite Business'},
|
||||
'1010020020': {
|
||||
'product': 'Google-Apps', 'aliases': ['gae', 'gse', 'enterprise', 'gsuiteenterprise',
|
||||
'wsentplus', 'workspaceenterpriseplus'], 'displayName': 'Google Workspace Enterprise Plus'},
|
||||
'1010020025': {
|
||||
'product': 'Google-Apps', 'aliases': ['wsbizplus', 'workspacebusinessplus'], 'displayName': 'Google Workspace Business Plus'},
|
||||
'1010020026': {
|
||||
'product': 'Google-Apps', 'aliases': ['wsentstan', 'workspaceenterprisestandard'], 'displayName': 'Google Workspace Enterprise Standard'},
|
||||
'1010020027': {
|
||||
'product': 'Google-Apps', 'aliases': ['wsbizstart', 'wsbizstarter', 'workspacebusinessstarter'], 'displayName': 'Google Workspace Business Starter'},
|
||||
'1010020028': {
|
||||
'product': 'Google-Apps', 'aliases': ['wsbizstan', 'workspacebusinessstandard'], 'displayName': 'Google Workspace Business Standard'},
|
||||
'1010020029': {
|
||||
'product': 'Google-Apps', 'aliases': ['wes', 'wsentstarter', 'workspaceenterprisestarter'], 'displayName': 'Workspace Enterprise Starter'},
|
||||
'1010020030': {
|
||||
'product': 'Google-Apps', 'aliases': ['wsflw', 'workspacefrontline', 'workspacefrontlineworker'], 'displayName': 'Google Workspace Frontline Starter'},
|
||||
'1010020031': {
|
||||
'product': 'Google-Apps', 'aliases': ['wsflwstan', 'workspacefrontlinestan', 'workspacefrontlineworkerstan'], 'displayName': 'Google Workspace Frontline Standard'},
|
||||
'1010340001': {
|
||||
'product': '101034', 'aliases': ['gseau', 'enterprisearchived', 'gsuiteenterprisearchived'], 'displayName': 'Google Workspace Enterprise Plus - Archived User'},
|
||||
'1010340002': {
|
||||
'product': '101034', 'aliases': ['gsbau', 'businessarchived', 'gsuitebusinessarchived'], 'displayName': 'Google Workspace Business - Archived User'},
|
||||
'1010340003': {
|
||||
'product': '101034', 'aliases': ['wsbizplusarchived', 'workspacebusinessplusarchived'], 'displayName': 'Google Workspace Business Plus - Archived User'},
|
||||
'1010340004': {
|
||||
'product': '101034', 'aliases': ['wsentstanarchived', 'workspaceenterprisestandardarchived'], 'displayName': 'Google Workspace Enterprise Standard - Archived User'},
|
||||
'1010340005': {
|
||||
'product': '101034', 'aliases': ['wsbizstarterarchived', 'workspacebusinessstarterarchived'], 'displayName': 'Google Workspace Business Starter - Archived User'},
|
||||
'1010340006': {
|
||||
'product': '101034', 'aliases': ['wsbizstanarchived', 'workspacebusinessstanarchived'], 'displayName': 'Google Workspace Business Standard - Archived User'},
|
||||
'1010060001': {
|
||||
'product': '101006', 'aliases': ['gsuiteessentials', 'essentials',
|
||||
'd4e', 'driveenterprise', 'drive4enterprise',
|
||||
'wsess', 'workspaceesentials'], 'displayName': 'Google Workspace Essentials'},
|
||||
'1010060003': {
|
||||
'product': 'Google-Apps', 'aliases': ['wsentess', 'workspaceenterpriseessentials'], 'displayName': 'Google Workspace Enterprise Essentials'},
|
||||
'1010060005': {
|
||||
'product': 'Google-Apps', 'aliases': ['wsessplus', 'workspaceessentialsplus'], 'displayName': 'Google Workspace Essentials Plus'},
|
||||
'Google-Drive-storage-20GB': {
|
||||
'product': 'Google-Drive-storage', 'aliases': ['drive20gb', '20gb', 'googledrivestorage20gb'], 'displayName': 'Google Drive Storage 20GB'},
|
||||
'Google-Drive-storage-50GB': {
|
||||
'product': 'Google-Drive-storage', 'aliases': ['drive50gb', '50gb', 'googledrivestorage50gb'], 'displayName': 'Google Drive Storage 50GB'},
|
||||
'Google-Drive-storage-200GB': {
|
||||
'product': 'Google-Drive-storage', 'aliases': ['drive200gb', '200gb', 'googledrivestorage200gb'], 'displayName': 'Google Drive Storage 200GB'},
|
||||
'Google-Drive-storage-400GB': {
|
||||
'product': 'Google-Drive-storage', 'aliases': ['drive400gb', '400gb', 'googledrivestorage400gb'], 'displayName': 'Google Drive Storage 400GB'},
|
||||
'Google-Drive-storage-1TB': {
|
||||
'product': 'Google-Drive-storage', 'aliases': ['drive1tb', '1tb', 'googledrivestorage1tb'], 'displayName': 'Google Drive Storage 1TB'},
|
||||
'Google-Drive-storage-2TB': {
|
||||
'product': 'Google-Drive-storage', 'aliases': ['drive2tb', '2tb', 'googledrivestorage2tb'], 'displayName': 'Google Drive Storage 2TB'},
|
||||
'Google-Drive-storage-4TB': {
|
||||
'product': 'Google-Drive-storage', 'aliases': ['drive4tb', '4tb', 'googledrivestorage4tb'], 'displayName': 'Google Drive Storage 4TB'},
|
||||
'Google-Drive-storage-8TB': {
|
||||
'product': 'Google-Drive-storage', 'aliases': ['drive8tb', '8tb', 'googledrivestorage8tb'], 'displayName': 'Google Drive Storage 8TB'},
|
||||
'Google-Drive-storage-16TB': {
|
||||
'product': 'Google-Drive-storage', 'aliases': ['drive16tb', '16tb', 'googledrivestorage16tb'], 'displayName': 'Google Drive Storage 16TB'},
|
||||
'Google-Vault': {
|
||||
'product': 'Google-Vault', 'aliases': ['vault', 'googlevault'], 'displayName': 'Google Vault'},
|
||||
'Google-Vault-Former-Employee': {
|
||||
'product': 'Google-Vault', 'aliases': ['vfe', 'googlevaultformeremployee'], 'displayName': 'Google Vault - Former Employee'},
|
||||
'Google-Chrome-Device-Management': {
|
||||
'product': 'Google-Chrome-Device-Management', 'aliases': ['chrome', 'cdm', 'googlechromedevicemanagement'], 'displayName': 'Google Chrome Device Management'}
|
||||
}
|
||||
|
||||
def getProductAndSKU(sku):
|
||||
l_sku = sku.lower().replace('-', '').replace(' ', '').replace('"', '').replace("'", '').strip()
|
||||
if l_sku.startswith('nv:'):
|
||||
if ':' in sku[3:]:
|
||||
return sku[3:].split(':', 1)
|
||||
return (None, sku)
|
||||
for a_sku, sku_values in list(_SKUS.items()):
|
||||
if ((l_sku == a_sku.lower().replace('-', '')) or
|
||||
(l_sku in sku_values['aliases']) or
|
||||
(l_sku == sku_values['displayName'].lower().replace(' ', ''))):
|
||||
return (sku_values['product'], a_sku)
|
||||
return (None, sku)
|
||||
|
||||
def productIdToDisplayName(productId):
|
||||
return _PRODUCTS.get(productId, productId)
|
||||
|
||||
def formatProductIdDisplayName(productId):
|
||||
productIdDisplay = productIdToDisplayName(productId)
|
||||
if productId == productIdDisplay:
|
||||
return productId
|
||||
return f'{productId} ({productIdDisplay})'
|
||||
|
||||
def normalizeProductId(product):
|
||||
l_product = product.lower().replace('-', '').replace(' ', '').strip()
|
||||
if l_product.startswith('nv:'):
|
||||
return (True, product[3:])
|
||||
for a_sku, sku_values in list(_SKUS.items()):
|
||||
if ((l_product == sku_values['product'].lower().replace('-', '')) or
|
||||
(l_product == a_sku.lower().replace('-', '')) or
|
||||
(l_product in sku_values['aliases']) or
|
||||
(l_product == sku_values['displayName'].lower().replace(' ', ''))):
|
||||
return (True, sku_values['product'])
|
||||
return (False, product)
|
||||
|
||||
def getSortedProductList():
|
||||
return sorted(_PRODUCTS)
|
||||
|
||||
def skuIdToDisplayName(skuId):
|
||||
return _SKUS[skuId]['displayName'] if skuId in _SKUS else skuId
|
||||
|
||||
def formatSKUIdDisplayName(skuId):
|
||||
skuIdDisplay = skuIdToDisplayName(skuId)
|
||||
if skuId == skuIdDisplay:
|
||||
return skuId
|
||||
return f'{skuId} ({skuIdDisplay})'
|
||||
|
||||
def getSortedSKUList():
|
||||
return sorted(_SKUS)
|
||||
|
||||
def convertProductListToSKUList(productList):
|
||||
skuList = []
|
||||
for productId in productList:
|
||||
skuList += [(productId, skuId) for skuId in _SKUS if _SKUS[skuId]['product'] == productId]
|
||||
return skuList
|
||||
|
||||
def getAllSKUs():
|
||||
return convertProductListToSKUList(sorted(_PRODUCTS))
|
||||
|
||||
def getGSuiteSKUs():
|
||||
return convertProductListToSKUList(['Google-Apps', '101031'])
|
||||
279
src/gam/gamlib/gluprop.py
Normal file
279
src/gam/gamlib/gluprop.py
Normal file
@@ -0,0 +1,279 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2023 Ross Scroggs All Rights Reserved.
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""GAM user properties
|
||||
|
||||
"""
|
||||
|
||||
# notes
|
||||
# a|b|c
|
||||
# getKeywordAttribute(CUSTOM_TYPE_NOCUSTOM, attrdict)
|
||||
|
||||
#CUSTOM_TYPE_NOCUSTOM = {
|
||||
# PTKW_CL_TYPE_KEYWORD: 'type',
|
||||
# PTKW_CL_CUSTOM_KEYWORD: None,
|
||||
# PTKW_ATTR_TYPE_KEYWORD: 'type',
|
||||
# PTKW_ATTR_TYPE_CUSTOM_VALUE: None,
|
||||
# PTKW_ATTR_CUSTOMTYPE_KEYWORD: None,
|
||||
# PTKW_KEYWORD_LIST: ['a', 'b', 'c']
|
||||
# }
|
||||
|
||||
# addresses, ims
|
||||
# type a|b|c|([custom] <String>)
|
||||
# getChoice([CUSTOM_TYPE_CUSTOM[PTKW_CL_TYPE_KEYWORD]])
|
||||
# getKeywordAttribute(CUSTOM_TYPE_CUSTOM, attrdict)
|
||||
|
||||
# emails, externalids, relations, websites
|
||||
# [type] a|b|c|([custom] <String>)
|
||||
# getChoice([CUSTOM_TYPE_CUSTOM[PTKW_CL_TYPE_KEYWORD]], defaultChoice=None)
|
||||
# getKeywordAttribute(CUSTOM_TYPE_IMPLICIT, attrdict)
|
||||
|
||||
# locations, phones
|
||||
# type a|b|c|([custom] <String>)
|
||||
# if argument == CUSTOM_TYPE_CUSTOM[PTKW_CL_TYPE_KEYWORD]:
|
||||
# getKeywordAttribute(CUSTOM_TYPE_CUSTOM, attrdict)
|
||||
|
||||
#CUSTOM_TYPE_CUSTOM = {
|
||||
# PTKW_CL_TYPE_KEYWORD: 'type',
|
||||
# PTKW_CL_CUSTOM_KEYWORD: 'custom',
|
||||
# PTKW_ATTR_TYPE_KEYWORD: 'type',
|
||||
# PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom',
|
||||
# PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
|
||||
# PTKW_KEYWORD_LIST: ['custom', 'a', 'b', 'c']
|
||||
# }
|
||||
|
||||
# organizations
|
||||
# (type a|b|c|([custom] <String>)) | (custom_type <String>)
|
||||
# if argument == CUSTOM_TYPE_DIFFERENT_KEYWORD[PTKW_CL_TYPE_KEYWORD]:
|
||||
# getKeywordAttribute(CUSTOM_TYPE_DIFFERENT_KEYWORD, attrdict)
|
||||
# elif argument == CUSTOM_TYPE_DIFFERENT_KEYWORD[PTKW_CL_CUSTOM_KEYWORD]:
|
||||
# attrdict[CUSTOM_TYPE_DIFFERENT_KEYWORD[PTKW_ATTR_CUSTOMTYPE_KEYWORD]] = getValue()
|
||||
|
||||
#CUSTOM_TYPE_DIFFERENT_KEYWORD = {
|
||||
# PTKW_CL_TYPE_KEYWORD: 'type',
|
||||
# PTKW_CL_CUSTOM_KEYWORD: 'custom',
|
||||
# PTKW_CL_CUSTOMTYPE_KEYWORD: 'custom_type',
|
||||
# PTKW_ATTR_TYPE_KEYWORD: 'type',
|
||||
# PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom',
|
||||
# PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
|
||||
# PTKW_KEYWORD_LIST: ['custom', 'a', 'b', 'c']
|
||||
# }
|
||||
|
||||
# Keys into USER_PROPERTIES
|
||||
CLASS = 'clas'
|
||||
TITLE = 'titl'
|
||||
TYPE_KEYWORDS = 'tykw'
|
||||
PTKW_CL_TYPE_KEYWORD = 'ctkw'
|
||||
PTKW_CL_CUSTOM_KEYWORD = 'ccuk'
|
||||
PTKW_CL_CUSTOMTYPE_KEYWORD = 'cctk'
|
||||
PTKW_ATTR_TYPE_KEYWORD = 'atkw'
|
||||
PTKW_ATTR_TYPE_CUSTOM_VALUE = 'atcv'
|
||||
PTKW_ATTR_CUSTOMTYPE_KEYWORD = 'actk'
|
||||
PTKW_KEYWORD_LIST = 'kwli'
|
||||
#
|
||||
PC_ADDRESSES = 'addr'
|
||||
PC_ALIASES = 'alia'
|
||||
PC_ARRAY = 'arry'
|
||||
PC_BOOLEAN = 'bool'
|
||||
PC_EMAILS = 'emai'
|
||||
PC_GENDER = 'gndr'
|
||||
PC_IMS = 'ims '
|
||||
PC_LANGUAGES = 'lang'
|
||||
PC_LOCATIONS = 'loca'
|
||||
PC_NAME = 'name'
|
||||
PC_NOTES = 'note'
|
||||
PC_ORGANIZATIONS = 'orga'
|
||||
PC_POSIX = 'posi'
|
||||
PC_SCHEMAS = 'schm'
|
||||
PC_SSH = 'ssh '
|
||||
PC_STRING = 'stri'
|
||||
PC_TIME = 'time'
|
||||
|
||||
PROPERTIES = {
|
||||
'primaryEmail':
|
||||
{CLASS: PC_STRING, TITLE: 'User',},
|
||||
'name':
|
||||
{CLASS: PC_NAME, TITLE: 'Name',},
|
||||
'givenName':
|
||||
{CLASS: PC_STRING, TITLE: 'First Name',},
|
||||
'familyName':
|
||||
{CLASS: PC_STRING, TITLE: 'Last Name',},
|
||||
'fullName':
|
||||
{CLASS: PC_STRING, TITLE: 'Full Name',},
|
||||
'displayName':
|
||||
{CLASS: PC_STRING, TITLE: 'Display Name',},
|
||||
'languages':
|
||||
{CLASS: PC_LANGUAGES, TITLE: 'Languages',},
|
||||
'languageCode':
|
||||
{CLASS: PC_LANGUAGES, TITLE: 'Languages',},
|
||||
'customLanguage':
|
||||
{CLASS: PC_LANGUAGES, TITLE: 'Custom Languages',},
|
||||
'password':
|
||||
{CLASS: PC_STRING, TITLE: 'Password',},
|
||||
'hashFunction':
|
||||
{CLASS: PC_STRING, TITLE: 'Hash Function',},
|
||||
'isAdmin':
|
||||
{CLASS: PC_BOOLEAN, TITLE: 'Is a Super Admin',},
|
||||
'isDelegatedAdmin':
|
||||
{CLASS: PC_BOOLEAN, TITLE: 'Is Delegated Admin',},
|
||||
'isEnrolledIn2Sv':
|
||||
{CLASS: PC_BOOLEAN, TITLE: '2-step enrolled',},
|
||||
'isEnforcedIn2Sv':
|
||||
{CLASS: PC_BOOLEAN, TITLE: '2-step enforced',},
|
||||
'agreedToTerms':
|
||||
{CLASS: PC_BOOLEAN, TITLE: 'Has Agreed to Terms',},
|
||||
'ipWhitelisted':
|
||||
{CLASS: PC_BOOLEAN, TITLE: 'IP Whitelisted',},
|
||||
'archived':
|
||||
{CLASS: PC_BOOLEAN, TITLE: 'Is Archived',},
|
||||
'suspended':
|
||||
{CLASS: PC_BOOLEAN, TITLE: 'Account Suspended',},
|
||||
'suspensionReason':
|
||||
{CLASS: PC_STRING, TITLE: 'Suspension Reason',},
|
||||
'changePasswordAtNextLogin':
|
||||
{CLASS: PC_BOOLEAN, TITLE: 'Must Change Password',},
|
||||
'recoveryEmail':
|
||||
{CLASS: PC_STRING, TITLE: 'Recovery Email',},
|
||||
'recoveryPhone':
|
||||
{CLASS: PC_STRING, TITLE: 'Recovery Phone',},
|
||||
'id':
|
||||
{CLASS: PC_STRING, TITLE: 'Google Unique ID',},
|
||||
'customerId':
|
||||
{CLASS: PC_STRING, TITLE: 'Customer ID',},
|
||||
'isMailboxSetup':
|
||||
{CLASS: PC_BOOLEAN, TITLE: 'Mailbox is setup',},
|
||||
'includeInGlobalAddressList':
|
||||
{CLASS: PC_BOOLEAN, TITLE: 'Included in GAL',},
|
||||
'creationTime':
|
||||
{CLASS: PC_TIME, TITLE: 'Creation Time',},
|
||||
'lastLoginTime':
|
||||
{CLASS: PC_TIME, TITLE: 'Last login time',},
|
||||
'deletionTime':
|
||||
{CLASS: PC_TIME, TITLE: 'Deletion Time',},
|
||||
'orgUnitPath':
|
||||
{CLASS: PC_STRING, TITLE: 'Google Org Unit Path',},
|
||||
'thumbnailPhotoUrl':
|
||||
{CLASS: PC_STRING, TITLE: 'Photo URL',},
|
||||
'addresses':
|
||||
{CLASS: PC_ADDRESSES, TITLE: 'Addresses',
|
||||
TYPE_KEYWORDS:
|
||||
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
|
||||
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
|
||||
PTKW_KEYWORD_LIST: ['custom', 'home', 'other', 'work'],},},
|
||||
'emails':
|
||||
{CLASS: PC_EMAILS, TITLE: 'Other Emails',
|
||||
TYPE_KEYWORDS:
|
||||
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
|
||||
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
|
||||
PTKW_KEYWORD_LIST: ['custom', 'home', 'other', 'work'],},},
|
||||
'externalIds':
|
||||
{CLASS: PC_ARRAY, TITLE: 'External IDs',
|
||||
TYPE_KEYWORDS:
|
||||
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
|
||||
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
|
||||
PTKW_KEYWORD_LIST: ['custom', 'account', 'customer', 'login_id', 'network', 'organization'],},},
|
||||
'gender':
|
||||
{CLASS: PC_GENDER, TITLE: 'Gender',
|
||||
TYPE_KEYWORDS:
|
||||
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'other',
|
||||
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'other', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customGender',
|
||||
PTKW_KEYWORD_LIST: ['male', 'female', 'other', 'unknown'],},},
|
||||
'ims':
|
||||
{CLASS: PC_IMS, TITLE: 'IMs',
|
||||
TYPE_KEYWORDS:
|
||||
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
|
||||
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
|
||||
PTKW_KEYWORD_LIST: ['custom', 'home', 'other', 'work'],},},
|
||||
'keywords':
|
||||
{CLASS: PC_ARRAY, TITLE: 'Keywords',
|
||||
TYPE_KEYWORDS:
|
||||
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
|
||||
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
|
||||
PTKW_KEYWORD_LIST: ['custom', 'mission', 'occupation', 'outlook'],},},
|
||||
'locations':
|
||||
{CLASS: PC_LOCATIONS, TITLE: 'Locations',
|
||||
TYPE_KEYWORDS:
|
||||
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
|
||||
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
|
||||
PTKW_KEYWORD_LIST: ['custom', 'default', 'desk'],},},
|
||||
'notes':
|
||||
{CLASS: PC_NOTES, TITLE: 'Notes',
|
||||
TYPE_KEYWORDS:
|
||||
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: None,
|
||||
PTKW_ATTR_TYPE_KEYWORD: 'contentType', PTKW_ATTR_TYPE_CUSTOM_VALUE: None, PTKW_ATTR_CUSTOMTYPE_KEYWORD: None,
|
||||
PTKW_KEYWORD_LIST: ['text_plain', 'text_html'],},},
|
||||
'organizations':
|
||||
{CLASS: PC_ORGANIZATIONS, TITLE: 'Organizations',
|
||||
TYPE_KEYWORDS:
|
||||
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom', PTKW_CL_CUSTOMTYPE_KEYWORD: 'customtype',
|
||||
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: None, PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
|
||||
PTKW_KEYWORD_LIST: ['custom', 'domain_only', 'school', 'unknown', 'work'],},},
|
||||
'phones':
|
||||
{CLASS: PC_ARRAY, TITLE: 'Phones',
|
||||
TYPE_KEYWORDS:
|
||||
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
|
||||
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
|
||||
PTKW_KEYWORD_LIST: ['custom', 'home', 'work', 'other',
|
||||
'home_fax', 'work_fax', 'other_fax',
|
||||
'mobile', 'pager',
|
||||
'company_main', 'assistant',
|
||||
'car', 'radio', 'isdn', 'callback',
|
||||
'telex', 'tty_tdd', 'work_mobile',
|
||||
'work_pager', 'main', 'grand_central'],},},
|
||||
'posixAccounts':
|
||||
{CLASS: PC_POSIX, TITLE: 'Posix Accounts',},
|
||||
'relations':
|
||||
{CLASS: PC_ARRAY, TITLE: 'Relations',
|
||||
TYPE_KEYWORDS:
|
||||
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
|
||||
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
|
||||
PTKW_KEYWORD_LIST: ['custom', 'spouse', 'child', 'mother',
|
||||
'father', 'parent', 'brother',
|
||||
'sister', 'friend', 'relative',
|
||||
'domestic_partner', 'partner',
|
||||
'manager', 'dotted_line_manager',
|
||||
'assistant', 'admin_assistant', 'exec_assistant',
|
||||
'referred_by'],},},
|
||||
'sshPublicKeys':
|
||||
{CLASS: PC_SSH, TITLE: 'SSH Public Keys',},
|
||||
'websites':
|
||||
{CLASS: PC_ARRAY, TITLE: 'Websites',
|
||||
TYPE_KEYWORDS:
|
||||
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
|
||||
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
|
||||
PTKW_KEYWORD_LIST: ['custom', 'home', 'work',
|
||||
'home_page', 'ftp', 'blog',
|
||||
'profile', 'other', 'reservations',
|
||||
'app_install_page', 'resume'],},},
|
||||
'customSchemas':
|
||||
{CLASS: PC_SCHEMAS, TITLE: 'Custom Schemas',
|
||||
TYPE_KEYWORDS:
|
||||
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
|
||||
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
|
||||
PTKW_KEYWORD_LIST: ['custom', 'home', 'other', 'work'],},},
|
||||
'aliases': {
|
||||
CLASS: PC_ALIASES, TITLE: 'Email Aliases',},
|
||||
'nonEditableAliases': {
|
||||
CLASS: PC_ALIASES, TITLE: 'NonEditable Aliases',},
|
||||
}
|
||||
#
|
||||
IM_PROTOCOLS = {
|
||||
PTKW_CL_TYPE_KEYWORD: 'protocol', PTKW_CL_CUSTOM_KEYWORD: 'custom_protocol',
|
||||
PTKW_ATTR_TYPE_KEYWORD: 'protocol', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom_protocol', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customProtocol',
|
||||
PTKW_KEYWORD_LIST: ['custom_protocol', 'aim', 'gtalk', 'icq', 'jabber', 'msn', 'net_meeting', 'qq', 'skype', 'xmpp', 'yahoo']
|
||||
}
|
||||
33
src/gam/gamlib/glverlibs.py
Normal file
33
src/gam/gamlib/glverlibs.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2023 Ross Scroggs All Rights Reserved.
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""GAM extended library versions
|
||||
|
||||
"""
|
||||
|
||||
GAM_VER_LIBS = ['cryptography',
|
||||
'filelock',
|
||||
'google-api-python-client',
|
||||
'google-auth-httplib2',
|
||||
'google-auth-oauthlib',
|
||||
'google-auth',
|
||||
'httplib2',
|
||||
'passlib',
|
||||
'python-dateutil',
|
||||
'yubikey-manager',
|
||||
]
|
||||
202
src/gam/gamlib/yubikey.py
Normal file
202
src/gam/gamlib/yubikey.py
Normal file
@@ -0,0 +1,202 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2023 Ross Scroggs All Rights Reserved.
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""YubiKey"""
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
from secrets import SystemRandom
|
||||
import string
|
||||
import sys
|
||||
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from smartcard.Exceptions import CardConnectionException
|
||||
from ykman.device import list_all_devices
|
||||
from ykman.piv import generate_self_signed_certificate, generate_chuid
|
||||
from yubikit.piv import DEFAULT_MANAGEMENT_KEY, \
|
||||
InvalidPinError, \
|
||||
KEY_TYPE, \
|
||||
MANAGEMENT_KEY_TYPE, \
|
||||
PIN_POLICY, \
|
||||
PivSession, \
|
||||
OBJECT_ID, \
|
||||
SLOT, \
|
||||
TOUCH_POLICY
|
||||
from yubikit.core.smartcard import ApduError, SmartCardConnection
|
||||
|
||||
YUBIKEY_CONNECTION_ERROR_RC = 80
|
||||
YUBIKEY_INVALID_KEY_TYPE_RC = 81
|
||||
YUBIKEY_INVALID_SLOT_RC = 82
|
||||
YUBIKEY_INVALID_PIN_RC = 83
|
||||
YUBIKEY_APDU_ERROR_RC = 84
|
||||
YUBIKEY_VALUE_ERROR_RC = 85
|
||||
YUBIKEY_MULTIPLE_CONNECTED_RC = 86
|
||||
YUBIKEY_NOT_FOUND_RC = 87
|
||||
|
||||
from gam import mplock
|
||||
|
||||
from gam import systemErrorExit
|
||||
from gam import readStdin
|
||||
from gam import writeStdout
|
||||
|
||||
from gam.gamlib import glmsgs as Msg
|
||||
|
||||
PIN_PUK_CHARS = string.ascii_letters+string.digits+string.punctuation
|
||||
|
||||
class YubiKey():
|
||||
|
||||
def __init__(self, service_account_info=None):
|
||||
self.key_type = None
|
||||
self.slot = None
|
||||
self.serial_number = None
|
||||
self.pin = None
|
||||
self.key_id = None
|
||||
if service_account_info:
|
||||
key_type = service_account_info.get('yubikey_key_type', 'RSA2048')
|
||||
try:
|
||||
self.key_type = getattr(KEY_TYPE, key_type.upper())
|
||||
except AttributeError:
|
||||
systemErrorExit(YUBIKEY_INVALID_KEY_TYPE_RC, f'{key_type} is not a valid value for yubikey_key_type')
|
||||
slot = service_account_info.get('yubikey_slot', 'AUTHENTICATION')
|
||||
try:
|
||||
self.slot = getattr(SLOT, slot.upper())
|
||||
except AttributeError:
|
||||
systemErrorExit(YUBIKEY_INVALID_SLOT_RC, f'{slot} is not a valid value for yubikey_slot')
|
||||
self.serial_number = service_account_info.get('yubikey_serial_number')
|
||||
self.pin = service_account_info.get('yubikey_pin')
|
||||
self.key_id = service_account_info.get('private_key_id')
|
||||
|
||||
def _connect(self):
|
||||
try:
|
||||
devices = list_all_devices()
|
||||
for (device, info) in devices:
|
||||
if info.serial == self.serial_number:
|
||||
return device.open_connection(SmartCardConnection)
|
||||
except CardConnectionException as err:
|
||||
systemErrorExit(YUBIKEY_CONNECTION_ERROR_RC, f'YubiKey - {err}')
|
||||
|
||||
def get_certificate(self):
|
||||
try:
|
||||
conn = self._connect()
|
||||
with conn:
|
||||
session = PivSession(conn)
|
||||
if self.pin:
|
||||
try:
|
||||
session.verify_pin(self.pin)
|
||||
except InvalidPinError as err:
|
||||
systemErrorExit(YUBIKEY_INVALID_PIN_RC, f'YubiKey - {err}')
|
||||
try:
|
||||
cert = session.get_certificate(self.slot)
|
||||
except ApduError as err:
|
||||
systemErrorExit(YUBIKEY_APDU_ERROR_RC, f'YubiKey - {err}')
|
||||
cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode()
|
||||
publicKeyData = base64.b64encode(cert_pem.encode())
|
||||
if isinstance(publicKeyData, bytes):
|
||||
publicKeyData = publicKeyData.decode()
|
||||
return publicKeyData
|
||||
except ValueError as err:
|
||||
systemErrorExit(YUBIKEY_VALUE_ERROR_RC, f'YubiKey - {err}')
|
||||
except TypeError as err:
|
||||
systemErrorExit(YUBIKEY_NOT_FOUND_RC, f'YubiKey - {err} - {Msg.IS_YUBIKEY_INSERTED}')
|
||||
|
||||
def get_serial_number(self):
|
||||
try:
|
||||
devices = list_all_devices()
|
||||
if not devices:
|
||||
systemErrorExit(YUBIKEY_NOT_FOUND_RC, Msg.COULD_NOT_FIND_ANY_YUBIKEY)
|
||||
if self.serial_number:
|
||||
for (_, info) in devices:
|
||||
if info.serial == self.serial_number:
|
||||
return info.serial
|
||||
systemErrorExit(YUBIKEY_NOT_FOUND_RC, Msg.COULD_NOT_FIND_YUBIKEY_WITH_SERIAL.format(self.serial_number))
|
||||
if len(devices) > 1:
|
||||
serials = ', '.join([str(info.serial) for (_, info) in devices])
|
||||
systemErrorExit(YUBIKEY_MULTIPLE_CONNECTED_RC, Msg.MULTIPLE_YUBIKEYS_CONNECTED.format(serials))
|
||||
return devices[0][1].serial
|
||||
except ValueError as err:
|
||||
systemErrorExit(YUBIKEY_VALUE_ERROR_RC, f'YubiKey - {err}')
|
||||
|
||||
def reset_piv(self):
|
||||
'''Resets YubiKey PIV app and generates new key for GAM to use.'''
|
||||
reply = str(readStdin(Msg.CONFIRM_WIPE_YUBIKEY_PIV).lower().strip())
|
||||
if reply != 'y':
|
||||
sys.exit(1)
|
||||
try:
|
||||
conn = self._connect()
|
||||
with conn:
|
||||
piv = PivSession(conn)
|
||||
piv.reset()
|
||||
rnd = SystemRandom()
|
||||
new_puk = ''.join(rnd.choice(PIN_PUK_CHARS) for _ in range(8))
|
||||
new_pin = ''.join(rnd.choice(PIN_PUK_CHARS) for _ in range(8))
|
||||
piv.change_puk('12345678', new_puk)
|
||||
piv.change_pin('123456', new_pin)
|
||||
writeStdout(Msg.YUBIKEY_PIN_SET_TO.format(new_pin))
|
||||
piv.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY)
|
||||
piv.verify_pin(new_pin)
|
||||
writeStdout(Msg.YUBIKEY_GENERATING_NONEXPORTABLE_PRIVATE_KEY)
|
||||
pubkey = piv.generate_key(SLOT.AUTHENTICATION,
|
||||
KEY_TYPE.RSA2048,
|
||||
PIN_POLICY.ALWAYS,
|
||||
TOUCH_POLICY.NEVER)
|
||||
now = datetime.datetime.utcnow()
|
||||
valid_to = now + datetime.timedelta(days=36500)
|
||||
subject = 'CN=GAM Created Key'
|
||||
piv.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY)
|
||||
piv.verify_pin(new_pin)
|
||||
cert = generate_self_signed_certificate(piv,
|
||||
SLOT.AUTHENTICATION,
|
||||
pubkey,
|
||||
subject,
|
||||
now,
|
||||
valid_to)
|
||||
piv.put_certificate(SLOT.AUTHENTICATION, cert)
|
||||
piv.put_object(OBJECT_ID.CHUID, generate_chuid())
|
||||
except ValueError as err:
|
||||
systemErrorExit(YUBIKEY_VALUE_ERROR_RC, f'YubiKey - {err}')
|
||||
except TypeError as err:
|
||||
systemErrorExit(YUBIKEY_NOT_FOUND_RC, f'YubiKey - {err} - {Msg.IS_YUBIKEY_INSERTED}')
|
||||
|
||||
def sign(self, message):
|
||||
if mplock is not None:
|
||||
mplock.acquire()
|
||||
try:
|
||||
conn = self._connect()
|
||||
with conn:
|
||||
session = PivSession(conn)
|
||||
if self.pin:
|
||||
try:
|
||||
session.verify_pin(self.pin)
|
||||
except InvalidPinError as err:
|
||||
systemErrorExit(YUBIKEY_INVALID_PIN_RC, f'YubiKey - {err}')
|
||||
try:
|
||||
signed = session.sign(slot=self.slot,
|
||||
key_type=self.key_type,
|
||||
message=message,
|
||||
hash_algorithm=hashes.SHA256(),
|
||||
padding=padding.PKCS1v15())
|
||||
except ApduError as err:
|
||||
systemErrorExit(YUBIKEY_APDU_ERROR_RC, f'YubiKey - {err}')
|
||||
except ValueError as err:
|
||||
systemErrorExit(YUBIKEY_VALUE_ERROR_RC, f'YubiKey - {err}')
|
||||
except TypeError as err:
|
||||
systemErrorExit(YUBIKEY_NOT_FOUND_RC, f'YubiKey - {err} - {Msg.IS_YUBIKEY_INSERTED}')
|
||||
if mplock is not None:
|
||||
mplock.release()
|
||||
return signed
|
||||
825
src/gam/gdata/__init__.py
Normal file
825
src/gam/gdata/__init__.py
Normal file
@@ -0,0 +1,825 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2006 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
"""Contains classes representing Google Data elements.
|
||||
|
||||
Extends Atom classes to add Google Data specific elements.
|
||||
"""
|
||||
|
||||
|
||||
__author__ = 'j.s@google.com (Jeffrey Scudder)'
|
||||
|
||||
import os
|
||||
import atom
|
||||
import lxml.etree as ElementTree
|
||||
|
||||
# XML namespaces which are often used in GData entities.
|
||||
GDATA_NAMESPACE = 'http://schemas.google.com/g/2005'
|
||||
GDATA_TEMPLATE = '{http://schemas.google.com/g/2005}%s'
|
||||
OPENSEARCH_NAMESPACE = 'http://a9.com/-/spec/opensearchrss/1.0/'
|
||||
OPENSEARCH_TEMPLATE = '{http://a9.com/-/spec/opensearchrss/1.0/}%s'
|
||||
BATCH_NAMESPACE = 'http://schemas.google.com/gdata/batch'
|
||||
GACL_NAMESPACE = 'http://schemas.google.com/acl/2007'
|
||||
GACL_TEMPLATE = '{http://schemas.google.com/acl/2007}%s'
|
||||
|
||||
|
||||
# Labels used in batch request entries to specify the desired CRUD operation.
|
||||
BATCH_INSERT = 'insert'
|
||||
BATCH_UPDATE = 'update'
|
||||
BATCH_DELETE = 'delete'
|
||||
BATCH_QUERY = 'query'
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MissingRequiredParameters(Error):
|
||||
pass
|
||||
|
||||
|
||||
class MediaSource(object):
|
||||
"""GData Entries can refer to media sources, so this class provides a
|
||||
place to store references to these objects along with some metadata.
|
||||
"""
|
||||
|
||||
def __init__(self, file_handle=None, content_type=None, content_length=None,
|
||||
file_path=None, file_name=None):
|
||||
"""Creates an object of type MediaSource.
|
||||
|
||||
Args:
|
||||
file_handle: A file handle pointing to the file to be encapsulated in the
|
||||
MediaSource
|
||||
content_type: string The MIME type of the file. Required if a file_handle
|
||||
is given.
|
||||
content_length: int The size of the file. Required if a file_handle is
|
||||
given.
|
||||
file_path: string (optional) A full path name to the file. Used in
|
||||
place of a file_handle.
|
||||
file_name: string The name of the file without any path information.
|
||||
Required if a file_handle is given.
|
||||
"""
|
||||
self.file_handle = file_handle
|
||||
self.content_type = content_type
|
||||
self.content_length = content_length
|
||||
self.file_name = file_name
|
||||
|
||||
if (file_handle is None and content_type is not None and
|
||||
file_path is not None):
|
||||
self.setFile(file_path, content_type)
|
||||
|
||||
def setFile(self, file_name, content_type):
|
||||
"""A helper function which can create a file handle from a given filename
|
||||
and set the content type and length all at once.
|
||||
|
||||
Args:
|
||||
file_name: string The path and file name to the file containing the media
|
||||
content_type: string A MIME type representing the type of the media
|
||||
"""
|
||||
|
||||
self.file_handle = open(file_name, 'rb')
|
||||
self.content_type = content_type
|
||||
self.content_length = os.path.getsize(file_name)
|
||||
self.file_name = os.path.basename(file_name)
|
||||
|
||||
|
||||
class LinkFinder(atom.LinkFinder):
|
||||
"""An "interface" providing methods to find link elements
|
||||
|
||||
GData Entry elements often contain multiple links which differ in the rel
|
||||
attribute or content type. Often, developers are interested in a specific
|
||||
type of link so this class provides methods to find specific classes of
|
||||
links.
|
||||
|
||||
This class is used as a mixin in GData entries.
|
||||
"""
|
||||
|
||||
def GetSelfLink(self):
|
||||
"""Find the first link with rel set to 'self'
|
||||
|
||||
Returns:
|
||||
An atom.Link or none if none of the links had rel equal to 'self'
|
||||
"""
|
||||
|
||||
for a_link in self.link:
|
||||
if a_link.rel == 'self':
|
||||
return a_link
|
||||
return None
|
||||
|
||||
def GetEditLink(self):
|
||||
for a_link in self.link:
|
||||
if a_link.rel == 'edit':
|
||||
return a_link
|
||||
return None
|
||||
|
||||
def GetEditMediaLink(self):
|
||||
"""The Picasa API mistakenly returns media-edit rather than edit-media, but
|
||||
this may change soon.
|
||||
"""
|
||||
for a_link in self.link:
|
||||
if a_link.rel == 'edit-media':
|
||||
return a_link
|
||||
if a_link.rel == 'media-edit':
|
||||
return a_link
|
||||
return None
|
||||
|
||||
def GetHtmlLink(self):
|
||||
"""Find the first link with rel of alternate and type of text/html
|
||||
|
||||
Returns:
|
||||
An atom.Link or None if no links matched
|
||||
"""
|
||||
for a_link in self.link:
|
||||
if a_link.rel == 'alternate' and a_link.type == 'text/html':
|
||||
return a_link
|
||||
return None
|
||||
|
||||
def GetPostLink(self):
|
||||
"""Get a link containing the POST target URL.
|
||||
|
||||
The POST target URL is used to insert new entries.
|
||||
|
||||
Returns:
|
||||
A link object with a rel matching the POST type.
|
||||
"""
|
||||
for a_link in self.link:
|
||||
if a_link.rel == 'http://schemas.google.com/g/2005#post':
|
||||
return a_link
|
||||
return None
|
||||
|
||||
def GetAclLink(self):
|
||||
for a_link in self.link:
|
||||
if a_link.rel == 'http://schemas.google.com/acl/2007#accessControlList':
|
||||
return a_link
|
||||
return None
|
||||
|
||||
def GetFeedLink(self):
|
||||
for a_link in self.link:
|
||||
if a_link.rel == 'http://schemas.google.com/g/2005#feed':
|
||||
return a_link
|
||||
return None
|
||||
|
||||
def GetNextLink(self):
|
||||
for a_link in self.link:
|
||||
if a_link.rel == 'next':
|
||||
return a_link
|
||||
return None
|
||||
|
||||
def GetPrevLink(self):
|
||||
for a_link in self.link:
|
||||
if a_link.rel == 'previous':
|
||||
return a_link
|
||||
return None
|
||||
|
||||
|
||||
class TotalResults(atom.AtomBase):
|
||||
"""opensearch:TotalResults for a GData feed"""
|
||||
|
||||
_tag = 'totalResults'
|
||||
_namespace = OPENSEARCH_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
|
||||
def __init__(self, extension_elements=None,
|
||||
extension_attributes=None, text=None):
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
|
||||
def TotalResultsFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(TotalResults, xml_string)
|
||||
|
||||
|
||||
class StartIndex(atom.AtomBase):
|
||||
"""The opensearch:startIndex element in GData feed"""
|
||||
|
||||
_tag = 'startIndex'
|
||||
_namespace = OPENSEARCH_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
|
||||
def __init__(self, extension_elements=None,
|
||||
extension_attributes=None, text=None):
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
|
||||
def StartIndexFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(StartIndex, xml_string)
|
||||
|
||||
|
||||
class ItemsPerPage(atom.AtomBase):
|
||||
"""The opensearch:itemsPerPage element in GData feed"""
|
||||
|
||||
_tag = 'itemsPerPage'
|
||||
_namespace = OPENSEARCH_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
|
||||
def __init__(self, extension_elements=None,
|
||||
extension_attributes=None, text=None):
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
|
||||
def ItemsPerPageFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(ItemsPerPage, xml_string)
|
||||
|
||||
|
||||
class ExtendedProperty(atom.AtomBase):
|
||||
"""The Google Data extendedProperty element.
|
||||
|
||||
Used to store arbitrary key-value information specific to your
|
||||
application. The value can either be a text string stored as an XML
|
||||
attribute (.value), or an XML node (XmlBlob) as a child element.
|
||||
|
||||
This element is used in the Google Calendar data API and the Google
|
||||
Contacts data API.
|
||||
"""
|
||||
|
||||
_tag = 'extendedProperty'
|
||||
_namespace = GDATA_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
_attributes['name'] = 'name'
|
||||
_attributes['value'] = 'value'
|
||||
|
||||
def __init__(self, name=None, value=None, extension_elements=None,
|
||||
extension_attributes=None, text=None):
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
def GetXmlBlobExtensionElement(self):
|
||||
"""Returns the XML blob as an atom.ExtensionElement.
|
||||
|
||||
Returns:
|
||||
An atom.ExtensionElement representing the blob's XML, or None if no
|
||||
blob was set.
|
||||
"""
|
||||
if len(self.extension_elements) < 1:
|
||||
return None
|
||||
else:
|
||||
return self.extension_elements[0]
|
||||
|
||||
def GetXmlBlobString(self):
|
||||
"""Returns the XML blob as a string.
|
||||
|
||||
Returns:
|
||||
A string containing the blob's XML, or None if no blob was set.
|
||||
"""
|
||||
blob = self.GetXmlBlobExtensionElement()
|
||||
if blob:
|
||||
return blob.ToString()
|
||||
return None
|
||||
|
||||
def SetXmlBlob(self, blob):
|
||||
"""Sets the contents of the extendedProperty to XML as a child node.
|
||||
|
||||
Since the extendedProperty is only allowed one child element as an XML
|
||||
blob, setting the XML blob will erase any preexisting extension elements
|
||||
in this object.
|
||||
|
||||
Args:
|
||||
blob: str, ElementTree Element or atom.ExtensionElement representing
|
||||
the XML blob stored in the extendedProperty.
|
||||
"""
|
||||
# Erase any existing extension_elements, clears the child nodes from the
|
||||
# extendedProperty.
|
||||
self.extension_elements = []
|
||||
if isinstance(blob, atom.ExtensionElement):
|
||||
self.extension_elements.append(blob)
|
||||
elif ElementTree.iselement(blob):
|
||||
self.extension_elements.append(atom._ExtensionElementFromElementTree(
|
||||
blob))
|
||||
else:
|
||||
self.extension_elements.append(atom.ExtensionElementFromString(blob))
|
||||
|
||||
|
||||
def ExtendedPropertyFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(ExtendedProperty, xml_string)
|
||||
|
||||
|
||||
class GDataEntry(atom.Entry, LinkFinder):
|
||||
"""Extends Atom Entry to provide data processing"""
|
||||
|
||||
_tag = atom.Entry._tag
|
||||
_namespace = atom.Entry._namespace
|
||||
_children = atom.Entry._children.copy()
|
||||
_attributes = atom.Entry._attributes.copy()
|
||||
|
||||
def __GetId(self):
|
||||
return self.__id
|
||||
|
||||
# This method was created to strip the unwanted whitespace from the id's
|
||||
# text node.
|
||||
def __SetId(self, id):
|
||||
self.__id = id
|
||||
if id is not None and id.text is not None:
|
||||
self.__id.text = id.text.strip()
|
||||
|
||||
id = property(__GetId, __SetId)
|
||||
|
||||
def IsMedia(self):
|
||||
"""Determines whether or not an entry is a GData Media entry.
|
||||
"""
|
||||
if (self.GetEditMediaLink()):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def GetMediaURL(self):
|
||||
"""Returns the URL to the media content, if the entry is a media entry.
|
||||
Otherwise returns None.
|
||||
"""
|
||||
if not self.IsMedia():
|
||||
return None
|
||||
else:
|
||||
return self.content.src
|
||||
|
||||
|
||||
def GDataEntryFromString(xml_string):
|
||||
"""Creates a new GDataEntry instance given a string of XML."""
|
||||
return atom.CreateClassFromXMLString(GDataEntry, xml_string)
|
||||
|
||||
|
||||
class GDataFeed(atom.Feed, LinkFinder):
|
||||
"""A Feed from a GData service"""
|
||||
|
||||
_tag = 'feed'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = atom.Feed._children.copy()
|
||||
_attributes = atom.Feed._attributes.copy()
|
||||
_children['{%s}totalResults' % OPENSEARCH_NAMESPACE] = ('total_results',
|
||||
TotalResults)
|
||||
_children['{%s}startIndex' % OPENSEARCH_NAMESPACE] = ('start_index',
|
||||
StartIndex)
|
||||
_children['{%s}itemsPerPage' % OPENSEARCH_NAMESPACE] = ('items_per_page',
|
||||
ItemsPerPage)
|
||||
# Add a conversion rule for atom:entry to make it into a GData
|
||||
# Entry.
|
||||
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [GDataEntry])
|
||||
|
||||
def __GetId(self):
|
||||
return self.__id
|
||||
|
||||
def __SetId(self, id):
|
||||
self.__id = id
|
||||
if id is not None and id.text is not None:
|
||||
self.__id.text = id.text.strip()
|
||||
|
||||
id = property(__GetId, __SetId)
|
||||
|
||||
def __GetGenerator(self):
|
||||
return self.__generator
|
||||
|
||||
def __SetGenerator(self, generator):
|
||||
self.__generator = generator
|
||||
if generator is not None:
|
||||
self.__generator.text = generator.text.strip()
|
||||
|
||||
generator = property(__GetGenerator, __SetGenerator)
|
||||
|
||||
def __init__(self, author=None, category=None, contributor=None,
|
||||
generator=None, icon=None, atom_id=None, link=None, logo=None,
|
||||
rights=None, subtitle=None, title=None, updated=None, entry=None,
|
||||
total_results=None, start_index=None, items_per_page=None,
|
||||
extension_elements=None, extension_attributes=None, text=None):
|
||||
"""Constructor for Source
|
||||
|
||||
Args:
|
||||
author: list (optional) A list of Author instances which belong to this
|
||||
class.
|
||||
category: list (optional) A list of Category instances
|
||||
contributor: list (optional) A list on Contributor instances
|
||||
generator: Generator (optional)
|
||||
icon: Icon (optional)
|
||||
id: Id (optional) The entry's Id element
|
||||
link: list (optional) A list of Link instances
|
||||
logo: Logo (optional)
|
||||
rights: Rights (optional) The entry's Rights element
|
||||
subtitle: Subtitle (optional) The entry's subtitle element
|
||||
title: Title (optional) the entry's title element
|
||||
updated: Updated (optional) the entry's updated element
|
||||
entry: list (optional) A list of the Entry instances contained in the
|
||||
feed.
|
||||
text: String (optional) The text contents of the element. This is the
|
||||
contents of the Entry's XML text node.
|
||||
(Example: <foo>This is the text</foo>)
|
||||
extension_elements: list (optional) A list of ExtensionElement instances
|
||||
which are children of this element.
|
||||
extension_attributes: dict (optional) A dictionary of strings which are
|
||||
the values for additional XML attributes of this element.
|
||||
"""
|
||||
|
||||
self.author = author or []
|
||||
self.category = category or []
|
||||
self.contributor = contributor or []
|
||||
self.generator = generator
|
||||
self.icon = icon
|
||||
self.id = atom_id
|
||||
self.link = link or []
|
||||
self.logo = logo
|
||||
self.rights = rights
|
||||
self.subtitle = subtitle
|
||||
self.title = title
|
||||
self.updated = updated
|
||||
self.entry = entry or []
|
||||
self.total_results = total_results
|
||||
self.start_index = start_index
|
||||
self.items_per_page = items_per_page
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
|
||||
def GDataFeedFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(GDataFeed, xml_string)
|
||||
|
||||
|
||||
class BatchId(atom.AtomBase):
|
||||
_tag = 'id'
|
||||
_namespace = BATCH_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
|
||||
|
||||
def BatchIdFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(BatchId, xml_string)
|
||||
|
||||
|
||||
class BatchOperation(atom.AtomBase):
|
||||
_tag = 'operation'
|
||||
_namespace = BATCH_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
_attributes['type'] = 'type'
|
||||
|
||||
def __init__(self, op_type=None, extension_elements=None,
|
||||
extension_attributes=None,
|
||||
text=None):
|
||||
self.type = op_type
|
||||
atom.AtomBase.__init__(self,
|
||||
extension_elements=extension_elements,
|
||||
extension_attributes=extension_attributes,
|
||||
text=text)
|
||||
|
||||
|
||||
def BatchOperationFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(BatchOperation, xml_string)
|
||||
|
||||
|
||||
class BatchStatus(atom.AtomBase):
|
||||
"""The batch:status element present in a batch response entry.
|
||||
|
||||
A status element contains the code (HTTP response code) and
|
||||
reason as elements. In a single request these fields would
|
||||
be part of the HTTP response, but in a batch request each
|
||||
Entry operation has a corresponding Entry in the response
|
||||
feed which includes status information.
|
||||
|
||||
See http://code.google.com/apis/gdata/batch.html#Handling_Errors
|
||||
"""
|
||||
|
||||
_tag = 'status'
|
||||
_namespace = BATCH_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
_attributes['code'] = 'code'
|
||||
_attributes['reason'] = 'reason'
|
||||
_attributes['content-type'] = 'content_type'
|
||||
|
||||
def __init__(self, code=None, reason=None, content_type=None,
|
||||
extension_elements=None, extension_attributes=None, text=None):
|
||||
self.code = code
|
||||
self.reason = reason
|
||||
self.content_type = content_type
|
||||
atom.AtomBase.__init__(self, extension_elements=extension_elements,
|
||||
extension_attributes=extension_attributes,
|
||||
text=text)
|
||||
|
||||
|
||||
def BatchStatusFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(BatchStatus, xml_string)
|
||||
|
||||
|
||||
class BatchEntry(GDataEntry):
|
||||
"""An atom:entry for use in batch requests.
|
||||
|
||||
The BatchEntry contains additional members to specify the operation to be
|
||||
performed on this entry and a batch ID so that the server can reference
|
||||
individual operations in the response feed. For more information, see:
|
||||
http://code.google.com/apis/gdata/batch.html
|
||||
"""
|
||||
|
||||
_tag = GDataEntry._tag
|
||||
_namespace = GDataEntry._namespace
|
||||
_children = GDataEntry._children.copy()
|
||||
_children['{%s}operation' % BATCH_NAMESPACE] = ('batch_operation', BatchOperation)
|
||||
_children['{%s}id' % BATCH_NAMESPACE] = ('batch_id', BatchId)
|
||||
_children['{%s}status' % BATCH_NAMESPACE] = ('batch_status', BatchStatus)
|
||||
_attributes = GDataEntry._attributes.copy()
|
||||
|
||||
def __init__(self, author=None, category=None, content=None,
|
||||
contributor=None, atom_id=None, link=None, published=None, rights=None,
|
||||
source=None, summary=None, control=None, title=None, updated=None,
|
||||
batch_operation=None, batch_id=None, batch_status=None,
|
||||
extension_elements=None, extension_attributes=None, text=None):
|
||||
self.batch_operation = batch_operation
|
||||
self.batch_id = batch_id
|
||||
self.batch_status = batch_status
|
||||
GDataEntry.__init__(self, author=author, category=category,
|
||||
content=content, contributor=contributor, atom_id=atom_id, link=link,
|
||||
published=published, rights=rights, source=source, summary=summary,
|
||||
control=control, title=title, updated=updated,
|
||||
extension_elements=extension_elements,
|
||||
extension_attributes=extension_attributes, text=text)
|
||||
|
||||
|
||||
def BatchEntryFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(BatchEntry, xml_string)
|
||||
|
||||
|
||||
class BatchInterrupted(atom.AtomBase):
|
||||
"""The batch:interrupted element sent if batch request was interrupted.
|
||||
|
||||
Only appears in a feed if some of the batch entries could not be processed.
|
||||
See: http://code.google.com/apis/gdata/batch.html#Handling_Errors
|
||||
"""
|
||||
|
||||
_tag = 'interrupted'
|
||||
_namespace = BATCH_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
_attributes['reason'] = 'reason'
|
||||
_attributes['success'] = 'success'
|
||||
_attributes['failures'] = 'failures'
|
||||
_attributes['parsed'] = 'parsed'
|
||||
|
||||
def __init__(self, reason=None, success=None, failures=None, parsed=None,
|
||||
extension_elements=None, extension_attributes=None, text=None):
|
||||
self.reason = reason
|
||||
self.success = success
|
||||
self.failures = failures
|
||||
self.parsed = parsed
|
||||
atom.AtomBase.__init__(self, extension_elements=extension_elements,
|
||||
extension_attributes=extension_attributes,
|
||||
text=text)
|
||||
|
||||
|
||||
def BatchInterruptedFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(BatchInterrupted, xml_string)
|
||||
|
||||
|
||||
class BatchFeed(GDataFeed):
|
||||
"""A feed containing a list of batch request entries."""
|
||||
|
||||
_tag = GDataFeed._tag
|
||||
_namespace = GDataFeed._namespace
|
||||
_children = GDataFeed._children.copy()
|
||||
_attributes = GDataFeed._attributes.copy()
|
||||
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [BatchEntry])
|
||||
_children['{%s}interrupted' % BATCH_NAMESPACE] = ('interrupted', BatchInterrupted)
|
||||
|
||||
def __init__(self, author=None, category=None, contributor=None,
|
||||
generator=None, icon=None, atom_id=None, link=None, logo=None,
|
||||
rights=None, subtitle=None, title=None, updated=None, entry=None,
|
||||
total_results=None, start_index=None, items_per_page=None,
|
||||
interrupted=None,
|
||||
extension_elements=None, extension_attributes=None, text=None):
|
||||
self.interrupted = interrupted
|
||||
GDataFeed.__init__(self, author=author, category=category,
|
||||
contributor=contributor, generator=generator,
|
||||
icon=icon, atom_id=atom_id, link=link,
|
||||
logo=logo, rights=rights, subtitle=subtitle,
|
||||
title=title, updated=updated, entry=entry,
|
||||
total_results=total_results, start_index=start_index,
|
||||
items_per_page=items_per_page,
|
||||
extension_elements=extension_elements,
|
||||
extension_attributes=extension_attributes,
|
||||
text=text)
|
||||
|
||||
def AddBatchEntry(self, entry=None, id_url_string=None,
|
||||
batch_id_string=None, operation_string=None):
|
||||
"""Logic for populating members of a BatchEntry and adding to the feed.
|
||||
|
||||
|
||||
If the entry is not a BatchEntry, it is converted to a BatchEntry so
|
||||
that the batch specific members will be present.
|
||||
|
||||
The id_url_string can be used in place of an entry if the batch operation
|
||||
applies to a URL. For example query and delete operations require just
|
||||
the URL of an entry, no body is sent in the HTTP request. If an
|
||||
id_url_string is sent instead of an entry, a BatchEntry is created and
|
||||
added to the feed.
|
||||
|
||||
This method also assigns the desired batch id to the entry so that it
|
||||
can be referenced in the server's response. If the batch_id_string is
|
||||
None, this method will assign a batch_id to be the index at which this
|
||||
entry will be in the feed's entry list.
|
||||
|
||||
Args:
|
||||
entry: BatchEntry, atom.Entry, or another Entry flavor (optional) The
|
||||
entry which will be sent to the server as part of the batch request.
|
||||
The item must have a valid atom id so that the server knows which
|
||||
entry this request references.
|
||||
id_url_string: str (optional) The URL of the entry to be acted on. You
|
||||
can find this URL in the text member of the atom id for an entry.
|
||||
If an entry is not sent, this id will be used to construct a new
|
||||
BatchEntry which will be added to the request feed.
|
||||
batch_id_string: str (optional) The batch ID to be used to reference
|
||||
this batch operation in the results feed. If this parameter is None,
|
||||
the current length of the feed's entry array will be used as a
|
||||
count. Note that batch_ids should either always be specified or
|
||||
never, mixing could potentially result in duplicate batch ids.
|
||||
operation_string: str (optional) The desired batch operation which will
|
||||
set the batch_operation.type member of the entry. Options are
|
||||
'insert', 'update', 'delete', and 'query'
|
||||
|
||||
Raises:
|
||||
MissingRequiredParameters: Raised if neither an id_ url_string nor an
|
||||
entry are provided in the request.
|
||||
|
||||
Returns:
|
||||
The added entry.
|
||||
"""
|
||||
if entry is None and id_url_string is None:
|
||||
raise MissingRequiredParameters('supply either an entry or URL string')
|
||||
if entry is None and id_url_string is not None:
|
||||
entry = BatchEntry(atom_id=atom.Id(text=id_url_string))
|
||||
# TODO: handle cases in which the entry lacks batch_... members.
|
||||
#if not isinstance(entry, BatchEntry):
|
||||
# Convert the entry to a batch entry.
|
||||
if batch_id_string is not None:
|
||||
entry.batch_id = BatchId(text=batch_id_string)
|
||||
elif entry.batch_id is None or entry.batch_id.text is None:
|
||||
entry.batch_id = BatchId(text=str(len(self.entry)))
|
||||
if operation_string is not None:
|
||||
entry.batch_operation = BatchOperation(op_type=operation_string)
|
||||
self.entry.append(entry)
|
||||
return entry
|
||||
|
||||
def AddInsert(self, entry, batch_id_string=None):
|
||||
"""Add an insert request to the operations in this batch request feed.
|
||||
|
||||
If the entry doesn't yet have an operation or a batch id, these will
|
||||
be set to the insert operation and a batch_id specified as a parameter.
|
||||
|
||||
Args:
|
||||
entry: BatchEntry The entry which will be sent in the batch feed as an
|
||||
insert request.
|
||||
batch_id_string: str (optional) The batch ID to be used to reference
|
||||
this batch operation in the results feed. If this parameter is None,
|
||||
the current length of the feed's entry array will be used as a
|
||||
count. Note that batch_ids should either always be specified or
|
||||
never, mixing could potentially result in duplicate batch ids.
|
||||
"""
|
||||
entry = self.AddBatchEntry(entry=entry, batch_id_string=batch_id_string,
|
||||
operation_string=BATCH_INSERT)
|
||||
|
||||
def AddUpdate(self, entry, batch_id_string=None):
|
||||
"""Add an update request to the list of batch operations in this feed.
|
||||
|
||||
Sets the operation type of the entry to insert if it is not already set
|
||||
and assigns the desired batch id to the entry so that it can be
|
||||
referenced in the server's response.
|
||||
|
||||
Args:
|
||||
entry: BatchEntry The entry which will be sent to the server as an
|
||||
update (HTTP PUT) request. The item must have a valid atom id
|
||||
so that the server knows which entry to replace.
|
||||
batch_id_string: str (optional) The batch ID to be used to reference
|
||||
this batch operation in the results feed. If this parameter is None,
|
||||
the current length of the feed's entry array will be used as a
|
||||
count. See also comments for AddInsert.
|
||||
"""
|
||||
entry = self.AddBatchEntry(entry=entry, batch_id_string=batch_id_string,
|
||||
operation_string=BATCH_UPDATE)
|
||||
|
||||
def AddDelete(self, url_string=None, entry=None, batch_id_string=None):
|
||||
"""Adds a delete request to the batch request feed.
|
||||
|
||||
This method takes either the url_string which is the atom id of the item
|
||||
to be deleted, or the entry itself. The atom id of the entry must be
|
||||
present so that the server knows which entry should be deleted.
|
||||
|
||||
Args:
|
||||
url_string: str (optional) The URL of the entry to be deleted. You can
|
||||
find this URL in the text member of the atom id for an entry.
|
||||
entry: BatchEntry (optional) The entry to be deleted.
|
||||
batch_id_string: str (optional)
|
||||
|
||||
Raises:
|
||||
MissingRequiredParameters: Raised if neither a url_string nor an entry
|
||||
are provided in the request.
|
||||
"""
|
||||
entry = self.AddBatchEntry(entry=entry, id_url_string=url_string,
|
||||
batch_id_string=batch_id_string,
|
||||
operation_string=BATCH_DELETE)
|
||||
|
||||
def AddQuery(self, url_string=None, entry=None, batch_id_string=None):
|
||||
"""Adds a query request to the batch request feed.
|
||||
|
||||
This method takes either the url_string which is the query URL
|
||||
whose results will be added to the result feed. The query URL will
|
||||
be encapsulated in a BatchEntry, and you may pass in the BatchEntry
|
||||
with a query URL instead of sending a url_string.
|
||||
|
||||
Args:
|
||||
url_string: str (optional)
|
||||
entry: BatchEntry (optional)
|
||||
batch_id_string: str (optional)
|
||||
|
||||
Raises:
|
||||
MissingRequiredParameters
|
||||
"""
|
||||
entry = self.AddBatchEntry(entry=entry, id_url_string=url_string,
|
||||
batch_id_string=batch_id_string,
|
||||
operation_string=BATCH_QUERY)
|
||||
|
||||
def GetBatchLink(self):
|
||||
for link in self.link:
|
||||
if link.rel == 'http://schemas.google.com/g/2005#batch':
|
||||
return link
|
||||
return None
|
||||
|
||||
|
||||
def BatchFeedFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(BatchFeed, xml_string)
|
||||
|
||||
|
||||
class EntryLink(atom.AtomBase):
|
||||
"""The gd:entryLink element"""
|
||||
|
||||
_tag = 'entryLink'
|
||||
_namespace = GDATA_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
# The entry used to be an atom.Entry, now it is a GDataEntry.
|
||||
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', GDataEntry)
|
||||
_attributes['rel'] = 'rel'
|
||||
_attributes['readOnly'] = 'read_only'
|
||||
_attributes['href'] = 'href'
|
||||
|
||||
def __init__(self, href=None, read_only=None, rel=None,
|
||||
entry=None, extension_elements=None,
|
||||
extension_attributes=None, text=None):
|
||||
self.href = href
|
||||
self.read_only = read_only
|
||||
self.rel = rel
|
||||
self.entry = entry
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
|
||||
def EntryLinkFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(EntryLink, xml_string)
|
||||
|
||||
|
||||
class FeedLink(atom.AtomBase):
|
||||
"""The gd:feedLink element"""
|
||||
|
||||
_tag = 'feedLink'
|
||||
_namespace = GDATA_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
_children['{%s}feed' % atom.ATOM_NAMESPACE] = ('feed', GDataFeed)
|
||||
_attributes['rel'] = 'rel'
|
||||
_attributes['readOnly'] = 'read_only'
|
||||
_attributes['countHint'] = 'count_hint'
|
||||
_attributes['href'] = 'href'
|
||||
|
||||
def __init__(self, count_hint=None, href=None, read_only=None, rel=None,
|
||||
feed=None, extension_elements=None, extension_attributes=None,
|
||||
text=None):
|
||||
self.count_hint = count_hint
|
||||
self.href = href
|
||||
self.read_only = read_only
|
||||
self.rel = rel
|
||||
self.feed = feed
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
|
||||
def FeedLinkFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(FeedLink, xml_string)
|
||||
@@ -1,4 +1,6 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -11,13 +13,8 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""This package's modules adapt the gdata library to run in other environments
|
||||
|
||||
"""Client library for using OAuth2, especially with Google APIs."""
|
||||
|
||||
__version__ = '3.0.0'
|
||||
|
||||
GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth'
|
||||
GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code'
|
||||
GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke'
|
||||
GOOGLE_TOKEN_URI = 'https://www.googleapis.com/oauth2/v4/token'
|
||||
GOOGLE_TOKEN_INFO_URI = 'https://www.googleapis.com/oauth2/v3/tokeninfo'
|
||||
The first example is the appengine module which contains functions and
|
||||
classes which modify a GDataService object to run on Google App Engine.
|
||||
"""
|
||||
101
src/gam/gdata/alt/app_engine.py
Normal file
101
src/gam/gdata/alt/app_engine.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2009 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
"""Provides functions to persist serialized auth tokens in the datastore.
|
||||
|
||||
The get_token and set_token functions should be used in conjunction with
|
||||
gdata.gauth's token_from_blob and token_to_blob to allow auth token objects
|
||||
to be reused across requests. It is up to your own code to ensure that the
|
||||
token key's are unique.
|
||||
"""
|
||||
|
||||
__author__ = 'j.s@google.com (Jeff Scudder)'
|
||||
|
||||
|
||||
from google.appengine.ext import db
|
||||
from google.appengine.api import memcache
|
||||
|
||||
|
||||
class Token(db.Model):
|
||||
"""Datastore Model which stores a serialized auth token."""
|
||||
t = db.BlobProperty()
|
||||
|
||||
|
||||
def get_token(unique_key):
|
||||
"""Searches for a stored token with the desired key.
|
||||
|
||||
Checks memcache and then the datastore if required.
|
||||
|
||||
Args:
|
||||
unique_key: str which uniquely identifies the desired auth token.
|
||||
|
||||
Returns:
|
||||
A string encoding the auth token data. Use gdata.gauth.token_from_blob to
|
||||
convert back into a usable token object. None if the token was not found
|
||||
in memcache or the datastore.
|
||||
"""
|
||||
token_string = memcache.get(unique_key)
|
||||
if token_string is None:
|
||||
# The token wasn't in memcache, so look in the datastore.
|
||||
token = Token.get_by_key_name(unique_key)
|
||||
if token is None:
|
||||
return None
|
||||
return token.t
|
||||
return token_string
|
||||
|
||||
|
||||
def set_token(unique_key, token_str):
|
||||
"""Saves the serialized auth token in the datastore.
|
||||
|
||||
The token is also stored in memcache to speed up retrieval on a cache hit.
|
||||
|
||||
Args:
|
||||
unique_key: The unique name for this token as a string. It is up to your
|
||||
code to ensure that this token value is unique in your application.
|
||||
Previous values will be silently overwitten.
|
||||
token_str: A serialized auth token as a string. I expect that this string
|
||||
will be generated by gdata.gauth.token_to_blob.
|
||||
|
||||
Returns:
|
||||
True if the token was stored sucessfully, False if the token could not be
|
||||
safely cached (if an old value could not be cleared). If the token was
|
||||
set in memcache, but not in the datastore, this function will return None.
|
||||
However, in that situation an exception will likely be raised.
|
||||
|
||||
Raises:
|
||||
Datastore exceptions may be raised from the App Engine SDK in the event of
|
||||
failure.
|
||||
"""
|
||||
# First try to save in memcache.
|
||||
result = memcache.set(unique_key, token_str)
|
||||
# If memcache fails to save the value, clear the cached value.
|
||||
if not result:
|
||||
result = memcache.delete(unique_key)
|
||||
# If we could not clear the cached value for this token, refuse to save.
|
||||
if result == 0:
|
||||
return False
|
||||
# Save to the datastore.
|
||||
if Token(key_name=unique_key, t=token_str).put():
|
||||
return True
|
||||
return None
|
||||
|
||||
|
||||
def delete_token(unique_key):
|
||||
# Clear from memcache.
|
||||
memcache.delete(unique_key)
|
||||
# Clear from the datastore.
|
||||
Token(key_name=unique_key).delete()
|
||||
321
src/gam/gdata/alt/appengine.py
Normal file
321
src/gam/gdata/alt/appengine.py
Normal file
@@ -0,0 +1,321 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
"""Provides HTTP functions for gdata.service to use on Google App Engine
|
||||
|
||||
AppEngineHttpClient: Provides an HTTP request method which uses App Engine's
|
||||
urlfetch API. Set the http_client member of a GDataService object to an
|
||||
instance of an AppEngineHttpClient to allow the gdata library to run on
|
||||
Google App Engine.
|
||||
|
||||
run_on_appengine: Function which will modify an existing GDataService object
|
||||
to allow it to run on App Engine. It works by creating a new instance of
|
||||
the AppEngineHttpClient and replacing the GDataService object's
|
||||
http_client.
|
||||
"""
|
||||
|
||||
|
||||
__author__ = 'api.jscudder (Jeff Scudder)'
|
||||
|
||||
|
||||
import StringIO
|
||||
import pickle
|
||||
import atom.http_interface
|
||||
import atom.token_store
|
||||
from google.appengine.api import urlfetch
|
||||
from google.appengine.ext import db
|
||||
from google.appengine.api import users
|
||||
from google.appengine.api import memcache
|
||||
|
||||
|
||||
def run_on_appengine(gdata_service, store_tokens=True,
|
||||
single_user_mode=False, deadline=None):
|
||||
"""Modifies a GDataService object to allow it to run on App Engine.
|
||||
|
||||
Args:
|
||||
gdata_service: An instance of AtomService, GDataService, or any
|
||||
of their subclasses which has an http_client member and a
|
||||
token_store member.
|
||||
store_tokens: Boolean, defaults to True. If True, the gdata_service
|
||||
will attempt to add each token to it's token_store when
|
||||
SetClientLoginToken or SetAuthSubToken is called. If False
|
||||
the tokens will not automatically be added to the
|
||||
token_store.
|
||||
single_user_mode: Boolean, defaults to False. If True, the current_token
|
||||
member of gdata_service will be set when
|
||||
SetClientLoginToken or SetAuthTubToken is called. If set
|
||||
to True, the current_token is set in the gdata_service
|
||||
and anyone who accesses the object will use the same
|
||||
token.
|
||||
|
||||
Note: If store_tokens is set to False and
|
||||
single_user_mode is set to False, all tokens will be
|
||||
ignored, since the library assumes: the tokens should not
|
||||
be stored in the datastore and they should not be stored
|
||||
in the gdata_service object. This will make it
|
||||
impossible to make requests which require authorization.
|
||||
deadline: int (optional) The number of seconds to wait for a response
|
||||
before timing out on the HTTP request. If no deadline is
|
||||
specified, the deafault deadline for HTTP requests from App
|
||||
Engine is used. The maximum is currently 10 (for 10 seconds).
|
||||
The default deadline for App Engine is 5 seconds.
|
||||
"""
|
||||
gdata_service.http_client = AppEngineHttpClient(deadline=deadline)
|
||||
gdata_service.token_store = AppEngineTokenStore()
|
||||
gdata_service.auto_store_tokens = store_tokens
|
||||
gdata_service.auto_set_current_token = single_user_mode
|
||||
return gdata_service
|
||||
|
||||
|
||||
class AppEngineHttpClient(atom.http_interface.GenericHttpClient):
|
||||
def __init__(self, headers=None, deadline=None):
|
||||
self.debug = False
|
||||
self.headers = headers or {}
|
||||
self.deadline = deadline
|
||||
|
||||
def request(self, operation, url, data=None, headers=None):
|
||||
"""Performs an HTTP call to the server, supports GET, POST, PUT, and
|
||||
DELETE.
|
||||
|
||||
Usage example, perform and HTTP GET on http://www.google.com/:
|
||||
import atom.http
|
||||
client = atom.http.HttpClient()
|
||||
http_response = client.request('GET', 'http://www.google.com/')
|
||||
|
||||
Args:
|
||||
operation: str The HTTP operation to be performed. This is usually one
|
||||
of 'GET', 'POST', 'PUT', or 'DELETE'
|
||||
data: filestream, list of parts, or other object which can be converted
|
||||
to a string. Should be set to None when performing a GET or DELETE.
|
||||
If data is a file-like object which can be read, this method will
|
||||
read a chunk of 100K bytes at a time and send them.
|
||||
If the data is a list of parts to be sent, each part will be
|
||||
evaluated and sent.
|
||||
url: The full URL to which the request should be sent. Can be a string
|
||||
or atom.url.Url.
|
||||
headers: dict of strings. HTTP headers which should be sent
|
||||
in the request.
|
||||
"""
|
||||
all_headers = self.headers.copy()
|
||||
if headers:
|
||||
all_headers.update(headers)
|
||||
|
||||
# Construct the full payload.
|
||||
# Assume that data is None or a string.
|
||||
data_str = data
|
||||
if data:
|
||||
if isinstance(data, list):
|
||||
# If data is a list of different objects, convert them all to strings
|
||||
# and join them together.
|
||||
converted_parts = [_convert_data_part(x) for x in data]
|
||||
data_str = ''.join(converted_parts)
|
||||
else:
|
||||
data_str = _convert_data_part(data)
|
||||
|
||||
# If the list of headers does not include a Content-Length, attempt to
|
||||
# calculate it based on the data object.
|
||||
if data and 'Content-Length' not in all_headers:
|
||||
all_headers['Content-Length'] = str(len(data_str))
|
||||
|
||||
# Set the content type to the default value if none was set.
|
||||
if 'Content-Type' not in all_headers:
|
||||
all_headers['Content-Type'] = 'application/atom+xml'
|
||||
|
||||
# Lookup the urlfetch operation which corresponds to the desired HTTP verb.
|
||||
if operation == 'GET':
|
||||
method = urlfetch.GET
|
||||
elif operation == 'POST':
|
||||
method = urlfetch.POST
|
||||
elif operation == 'PUT':
|
||||
method = urlfetch.PUT
|
||||
elif operation == 'DELETE':
|
||||
method = urlfetch.DELETE
|
||||
else:
|
||||
method = None
|
||||
if self.deadline is None:
|
||||
return HttpResponse(urlfetch.Fetch(url=str(url), payload=data_str,
|
||||
method=method, headers=all_headers, follow_redirects=False))
|
||||
return HttpResponse(urlfetch.Fetch(url=str(url), payload=data_str,
|
||||
method=method, headers=all_headers, follow_redirects=False,
|
||||
deadline=self.deadline))
|
||||
|
||||
|
||||
def _convert_data_part(data):
|
||||
if not data or isinstance(data, str):
|
||||
return data
|
||||
elif hasattr(data, 'read'):
|
||||
# data is a file like object, so read it completely.
|
||||
return data.read()
|
||||
# The data object was not a file.
|
||||
# Try to convert to a string and send the data.
|
||||
return str(data)
|
||||
|
||||
|
||||
class HttpResponse(object):
|
||||
"""Translates a urlfetch resoinse to look like an hhtplib resoinse.
|
||||
|
||||
Used to allow the resoinse from HttpRequest to be usable by gdata.service
|
||||
methods.
|
||||
"""
|
||||
|
||||
def __init__(self, urlfetch_response):
|
||||
self.body = StringIO.StringIO(urlfetch_response.content)
|
||||
self.headers = urlfetch_response.headers
|
||||
self.status = urlfetch_response.status_code
|
||||
self.reason = ''
|
||||
|
||||
def read(self, length=None):
|
||||
if not length:
|
||||
return self.body.read()
|
||||
else:
|
||||
return self.body.read(length)
|
||||
|
||||
def getheader(self, name):
|
||||
if not self.headers.has_key(name):
|
||||
return self.headers[name.lower()]
|
||||
return self.headers[name]
|
||||
|
||||
|
||||
class TokenCollection(db.Model):
|
||||
"""Datastore Model which associates auth tokens with the current user."""
|
||||
user = db.UserProperty()
|
||||
pickled_tokens = db.BlobProperty()
|
||||
|
||||
|
||||
class AppEngineTokenStore(atom.token_store.TokenStore):
|
||||
"""Stores the user's auth tokens in the App Engine datastore.
|
||||
|
||||
Tokens are only written to the datastore if a user is signed in (if
|
||||
users.get_current_user() returns a user object).
|
||||
"""
|
||||
def __init__(self):
|
||||
self.user = None
|
||||
|
||||
def add_token(self, token):
|
||||
"""Associates the token with the current user and stores it.
|
||||
|
||||
If there is no current user, the token will not be stored.
|
||||
|
||||
Returns:
|
||||
False if the token was not stored.
|
||||
"""
|
||||
tokens = load_auth_tokens(self.user)
|
||||
if not hasattr(token, 'scopes') or not token.scopes:
|
||||
return False
|
||||
for scope in token.scopes:
|
||||
tokens[str(scope)] = token
|
||||
key = save_auth_tokens(tokens, self.user)
|
||||
if key:
|
||||
return True
|
||||
return False
|
||||
|
||||
def find_token(self, url):
|
||||
"""Searches the current user's collection of token for a token which can
|
||||
be used for a request to the url.
|
||||
|
||||
Returns:
|
||||
The stored token which belongs to the current user and is valid for the
|
||||
desired URL. If there is no current user, or there is no valid user
|
||||
token in the datastore, a atom.http_interface.GenericToken is returned.
|
||||
"""
|
||||
if url is None:
|
||||
return None
|
||||
if isinstance(url, (str, unicode)):
|
||||
url = atom.url.parse_url(url)
|
||||
tokens = load_auth_tokens(self.user)
|
||||
if url in tokens:
|
||||
token = tokens[url]
|
||||
if token.valid_for_scope(url):
|
||||
return token
|
||||
else:
|
||||
del tokens[url]
|
||||
save_auth_tokens(tokens, self.user)
|
||||
for scope, token in tokens.iteritems():
|
||||
if token.valid_for_scope(url):
|
||||
return token
|
||||
return atom.http_interface.GenericToken()
|
||||
|
||||
def remove_token(self, token):
|
||||
"""Removes the token from the current user's collection in the datastore.
|
||||
|
||||
Returns:
|
||||
False if the token was not removed, this could be because the token was
|
||||
not in the datastore, or because there is no current user.
|
||||
"""
|
||||
token_found = False
|
||||
scopes_to_delete = []
|
||||
tokens = load_auth_tokens(self.user)
|
||||
for scope, stored_token in tokens.iteritems():
|
||||
if stored_token == token:
|
||||
scopes_to_delete.append(scope)
|
||||
token_found = True
|
||||
for scope in scopes_to_delete:
|
||||
del tokens[scope]
|
||||
if token_found:
|
||||
save_auth_tokens(tokens, self.user)
|
||||
return token_found
|
||||
|
||||
def remove_all_tokens(self):
|
||||
"""Removes all of the current user's tokens from the datastore."""
|
||||
save_auth_tokens({}, self.user)
|
||||
|
||||
|
||||
def save_auth_tokens(token_dict, user=None):
|
||||
"""Associates the tokens with the current user and writes to the datastore.
|
||||
|
||||
If there us no current user, the tokens are not written and this function
|
||||
returns None.
|
||||
|
||||
Returns:
|
||||
The key of the datastore entity containing the user's tokens, or None if
|
||||
there was no current user.
|
||||
"""
|
||||
if user is None:
|
||||
user = users.get_current_user()
|
||||
if user is None:
|
||||
return None
|
||||
memcache.set('gdata_pickled_tokens:%s' % user, pickle.dumps(token_dict))
|
||||
user_tokens = TokenCollection.all().filter('user =', user).get()
|
||||
if user_tokens:
|
||||
user_tokens.pickled_tokens = pickle.dumps(token_dict)
|
||||
return user_tokens.put()
|
||||
else:
|
||||
user_tokens = TokenCollection(
|
||||
user=user,
|
||||
pickled_tokens=pickle.dumps(token_dict))
|
||||
return user_tokens.put()
|
||||
|
||||
|
||||
def load_auth_tokens(user=None):
|
||||
"""Reads a dictionary of the current user's tokens from the datastore.
|
||||
|
||||
If there is no current user (a user is not signed in to the app) or the user
|
||||
does not have any tokens, an empty dictionary is returned.
|
||||
"""
|
||||
if user is None:
|
||||
user = users.get_current_user()
|
||||
if user is None:
|
||||
return {}
|
||||
pickled_tokens = memcache.get('gdata_pickled_tokens:%s' % user)
|
||||
if pickled_tokens:
|
||||
return pickle.loads(pickled_tokens)
|
||||
user_tokens = TokenCollection.all().filter('user =', user).get()
|
||||
if user_tokens:
|
||||
memcache.set('gdata_pickled_tokens:%s' % user, user_tokens.pickled_tokens)
|
||||
return pickle.loads(user_tokens.pickled_tokens)
|
||||
return {}
|
||||
|
||||
526
src/gam/gdata/apps/__init__.py
Normal file
526
src/gam/gdata/apps/__init__.py
Normal file
@@ -0,0 +1,526 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2007 SIOS Technology, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Contains objects used with Google Apps."""
|
||||
|
||||
__author__ = 'tmatsuo@sios.com (Takashi MATSUO)'
|
||||
|
||||
|
||||
import atom
|
||||
import gdata
|
||||
|
||||
|
||||
# XML namespaces which are often used in Google Apps entity.
|
||||
APPS_NAMESPACE = 'http://schemas.google.com/apps/2006'
|
||||
APPS_TEMPLATE = '{http://schemas.google.com/apps/2006}%s'
|
||||
|
||||
|
||||
class EmailList(atom.AtomBase):
|
||||
"""The Google Apps EmailList element"""
|
||||
|
||||
_tag = 'emailList'
|
||||
_namespace = APPS_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
_attributes['name'] = 'name'
|
||||
|
||||
def __init__(self, name=None, extension_elements=None,
|
||||
extension_attributes=None, text=None):
|
||||
self.name = name
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
def EmailListFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(EmailList, xml_string)
|
||||
|
||||
|
||||
class Who(atom.AtomBase):
|
||||
"""The Google Apps Who element"""
|
||||
|
||||
_tag = 'who'
|
||||
_namespace = gdata.GDATA_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
_attributes['rel'] = 'rel'
|
||||
_attributes['email'] = 'email'
|
||||
|
||||
def __init__(self, rel=None, email=None, extension_elements=None,
|
||||
extension_attributes=None, text=None):
|
||||
self.rel = rel
|
||||
self.email = email
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
def WhoFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(Who, xml_string)
|
||||
|
||||
|
||||
class Login(atom.AtomBase):
|
||||
"""The Google Apps Login element"""
|
||||
|
||||
_tag = 'login'
|
||||
_namespace = APPS_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
_attributes['userName'] = 'user_name'
|
||||
_attributes['password'] = 'password'
|
||||
_attributes['suspended'] = 'suspended'
|
||||
_attributes['admin'] = 'admin'
|
||||
_attributes['changePasswordAtNextLogin'] = 'change_password'
|
||||
_attributes['agreedToTerms'] = 'agreed_to_terms'
|
||||
_attributes['ipWhitelisted'] = 'ip_whitelisted'
|
||||
_attributes['hashFunctionName'] = 'hash_function_name'
|
||||
|
||||
def __init__(self, user_name=None, password=None, suspended=None,
|
||||
ip_whitelisted=None, hash_function_name=None,
|
||||
admin=None, change_password=None, agreed_to_terms=None,
|
||||
extension_elements=None, extension_attributes=None,
|
||||
text=None):
|
||||
self.user_name = user_name
|
||||
self.password = password
|
||||
self.suspended = suspended
|
||||
self.admin = admin
|
||||
self.change_password = change_password
|
||||
self.agreed_to_terms = agreed_to_terms
|
||||
self.ip_whitelisted = ip_whitelisted
|
||||
self.hash_function_name = hash_function_name
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
|
||||
def LoginFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(Login, xml_string)
|
||||
|
||||
|
||||
class Quota(atom.AtomBase):
|
||||
"""The Google Apps Quota element"""
|
||||
|
||||
_tag = 'quota'
|
||||
_namespace = APPS_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
_attributes['limit'] = 'limit'
|
||||
|
||||
def __init__(self, limit=None, extension_elements=None,
|
||||
extension_attributes=None, text=None):
|
||||
self.limit = limit
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
|
||||
def QuotaFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(Quota, xml_string)
|
||||
|
||||
|
||||
class Name(atom.AtomBase):
|
||||
"""The Google Apps Name element"""
|
||||
|
||||
_tag = 'name'
|
||||
_namespace = APPS_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
_attributes['familyName'] = 'family_name'
|
||||
_attributes['givenName'] = 'given_name'
|
||||
|
||||
def __init__(self, family_name=None, given_name=None,
|
||||
extension_elements=None, extension_attributes=None, text=None):
|
||||
self.family_name = family_name
|
||||
self.given_name = given_name
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
|
||||
def NameFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(Name, xml_string)
|
||||
|
||||
|
||||
class Nickname(atom.AtomBase):
|
||||
"""The Google Apps Nickname element"""
|
||||
|
||||
_tag = 'nickname'
|
||||
_namespace = APPS_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
_attributes['name'] = 'name'
|
||||
|
||||
def __init__(self, name=None,
|
||||
extension_elements=None, extension_attributes=None, text=None):
|
||||
self.name = name
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
|
||||
def NicknameFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(Nickname, xml_string)
|
||||
|
||||
|
||||
class NicknameEntry(gdata.GDataEntry):
|
||||
"""A Google Apps flavor of an Atom Entry for Nickname"""
|
||||
|
||||
_tag = 'entry'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.GDataEntry._children.copy()
|
||||
_attributes = gdata.GDataEntry._attributes.copy()
|
||||
_children['{%s}login' % APPS_NAMESPACE] = ('login', Login)
|
||||
_children['{%s}nickname' % APPS_NAMESPACE] = ('nickname', Nickname)
|
||||
|
||||
def __init__(self, author=None, category=None, content=None,
|
||||
atom_id=None, link=None, published=None,
|
||||
title=None, updated=None,
|
||||
login=None, nickname=None,
|
||||
extended_property=None,
|
||||
extension_elements=None, extension_attributes=None, text=None):
|
||||
|
||||
gdata.GDataEntry.__init__(self, author=author, category=category,
|
||||
content=content,
|
||||
atom_id=atom_id, link=link, published=published,
|
||||
title=title, updated=updated)
|
||||
self.login = login
|
||||
self.nickname = nickname
|
||||
self.extended_property = extended_property or []
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
|
||||
def NicknameEntryFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(NicknameEntry, xml_string)
|
||||
|
||||
|
||||
class NicknameFeed(gdata.GDataFeed, gdata.LinkFinder):
|
||||
"""A Google Apps Nickname feed flavor of an Atom Feed"""
|
||||
|
||||
_tag = 'feed'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.GDataFeed._children.copy()
|
||||
_attributes = gdata.GDataFeed._attributes.copy()
|
||||
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [NicknameEntry])
|
||||
|
||||
def __init__(self, author=None, category=None, contributor=None,
|
||||
generator=None, icon=None, atom_id=None, link=None, logo=None,
|
||||
rights=None, subtitle=None, title=None, updated=None,
|
||||
entry=None, total_results=None, start_index=None,
|
||||
items_per_page=None, extension_elements=None,
|
||||
extension_attributes=None, text=None):
|
||||
gdata.GDataFeed.__init__(self, author=author, category=category,
|
||||
contributor=contributor, generator=generator,
|
||||
icon=icon, atom_id=atom_id, link=link,
|
||||
logo=logo, rights=rights, subtitle=subtitle,
|
||||
title=title, updated=updated, entry=entry,
|
||||
total_results=total_results,
|
||||
start_index=start_index,
|
||||
items_per_page=items_per_page,
|
||||
extension_elements=extension_elements,
|
||||
extension_attributes=extension_attributes,
|
||||
text=text)
|
||||
|
||||
|
||||
def NicknameFeedFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(NicknameFeed, xml_string)
|
||||
|
||||
|
||||
class UserEntry(gdata.GDataEntry):
|
||||
"""A Google Apps flavor of an Atom Entry"""
|
||||
|
||||
_tag = 'entry'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.GDataEntry._children.copy()
|
||||
_attributes = gdata.GDataEntry._attributes.copy()
|
||||
_children['{%s}login' % APPS_NAMESPACE] = ('login', Login)
|
||||
_children['{%s}name' % APPS_NAMESPACE] = ('name', Name)
|
||||
_children['{%s}quota' % APPS_NAMESPACE] = ('quota', Quota)
|
||||
# This child may already be defined in GDataEntry, confirm before removing.
|
||||
_children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link',
|
||||
[gdata.FeedLink])
|
||||
_children['{%s}who' % gdata.GDATA_NAMESPACE] = ('who', Who)
|
||||
|
||||
def __init__(self, author=None, category=None, content=None,
|
||||
atom_id=None, link=None, published=None,
|
||||
title=None, updated=None,
|
||||
login=None, name=None, quota=None, who=None, feed_link=None,
|
||||
extended_property=None,
|
||||
extension_elements=None, extension_attributes=None, text=None):
|
||||
|
||||
gdata.GDataEntry.__init__(self, author=author, category=category,
|
||||
content=content,
|
||||
atom_id=atom_id, link=link, published=published,
|
||||
title=title, updated=updated)
|
||||
self.login = login
|
||||
self.name = name
|
||||
self.quota = quota
|
||||
self.who = who
|
||||
self.feed_link = feed_link or []
|
||||
self.extended_property = extended_property or []
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
|
||||
def UserEntryFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(UserEntry, xml_string)
|
||||
|
||||
|
||||
class UserFeed(gdata.GDataFeed, gdata.LinkFinder):
|
||||
"""A Google Apps User feed flavor of an Atom Feed"""
|
||||
|
||||
_tag = 'feed'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.GDataFeed._children.copy()
|
||||
_attributes = gdata.GDataFeed._attributes.copy()
|
||||
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [UserEntry])
|
||||
|
||||
def __init__(self, author=None, category=None, contributor=None,
|
||||
generator=None, icon=None, atom_id=None, link=None, logo=None,
|
||||
rights=None, subtitle=None, title=None, updated=None,
|
||||
entry=None, total_results=None, start_index=None,
|
||||
items_per_page=None, extension_elements=None,
|
||||
extension_attributes=None, text=None):
|
||||
gdata.GDataFeed.__init__(self, author=author, category=category,
|
||||
contributor=contributor, generator=generator,
|
||||
icon=icon, atom_id=atom_id, link=link,
|
||||
logo=logo, rights=rights, subtitle=subtitle,
|
||||
title=title, updated=updated, entry=entry,
|
||||
total_results=total_results,
|
||||
start_index=start_index,
|
||||
items_per_page=items_per_page,
|
||||
extension_elements=extension_elements,
|
||||
extension_attributes=extension_attributes,
|
||||
text=text)
|
||||
|
||||
|
||||
def UserFeedFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(UserFeed, xml_string)
|
||||
|
||||
|
||||
class EmailListEntry(gdata.GDataEntry):
|
||||
"""A Google Apps EmailList flavor of an Atom Entry"""
|
||||
|
||||
_tag = 'entry'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.GDataEntry._children.copy()
|
||||
_attributes = gdata.GDataEntry._attributes.copy()
|
||||
_children['{%s}emailList' % APPS_NAMESPACE] = ('email_list', EmailList)
|
||||
# Might be able to remove this _children entry.
|
||||
_children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link',
|
||||
[gdata.FeedLink])
|
||||
|
||||
def __init__(self, author=None, category=None, content=None,
|
||||
atom_id=None, link=None, published=None,
|
||||
title=None, updated=None,
|
||||
email_list=None, feed_link=None,
|
||||
extended_property=None,
|
||||
extension_elements=None, extension_attributes=None, text=None):
|
||||
|
||||
gdata.GDataEntry.__init__(self, author=author, category=category,
|
||||
content=content,
|
||||
atom_id=atom_id, link=link, published=published,
|
||||
title=title, updated=updated)
|
||||
self.email_list = email_list
|
||||
self.feed_link = feed_link or []
|
||||
self.extended_property = extended_property or []
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
|
||||
def EmailListEntryFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(EmailListEntry, xml_string)
|
||||
|
||||
|
||||
class EmailListFeed(gdata.GDataFeed, gdata.LinkFinder):
|
||||
"""A Google Apps EmailList feed flavor of an Atom Feed"""
|
||||
|
||||
_tag = 'feed'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.GDataFeed._children.copy()
|
||||
_attributes = gdata.GDataFeed._attributes.copy()
|
||||
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [EmailListEntry])
|
||||
|
||||
def __init__(self, author=None, category=None, contributor=None,
|
||||
generator=None, icon=None, atom_id=None, link=None, logo=None,
|
||||
rights=None, subtitle=None, title=None, updated=None,
|
||||
entry=None, total_results=None, start_index=None,
|
||||
items_per_page=None, extension_elements=None,
|
||||
extension_attributes=None, text=None):
|
||||
gdata.GDataFeed.__init__(self, author=author, category=category,
|
||||
contributor=contributor, generator=generator,
|
||||
icon=icon, atom_id=atom_id, link=link,
|
||||
logo=logo, rights=rights, subtitle=subtitle,
|
||||
title=title, updated=updated, entry=entry,
|
||||
total_results=total_results,
|
||||
start_index=start_index,
|
||||
items_per_page=items_per_page,
|
||||
extension_elements=extension_elements,
|
||||
extension_attributes=extension_attributes,
|
||||
text=text)
|
||||
|
||||
|
||||
def EmailListFeedFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(EmailListFeed, xml_string)
|
||||
|
||||
|
||||
class EmailListRecipientEntry(gdata.GDataEntry):
|
||||
"""A Google Apps EmailListRecipient flavor of an Atom Entry"""
|
||||
|
||||
_tag = 'entry'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.GDataEntry._children.copy()
|
||||
_attributes = gdata.GDataEntry._attributes.copy()
|
||||
_children['{%s}who' % gdata.GDATA_NAMESPACE] = ('who', Who)
|
||||
|
||||
def __init__(self, author=None, category=None, content=None,
|
||||
atom_id=None, link=None, published=None,
|
||||
title=None, updated=None,
|
||||
who=None,
|
||||
extended_property=None,
|
||||
extension_elements=None, extension_attributes=None, text=None):
|
||||
|
||||
gdata.GDataEntry.__init__(self, author=author, category=category,
|
||||
content=content,
|
||||
atom_id=atom_id, link=link, published=published,
|
||||
title=title, updated=updated)
|
||||
self.who = who
|
||||
self.extended_property = extended_property or []
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
|
||||
def EmailListRecipientEntryFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(EmailListRecipientEntry, xml_string)
|
||||
|
||||
|
||||
class EmailListRecipientFeed(gdata.GDataFeed, gdata.LinkFinder):
|
||||
"""A Google Apps EmailListRecipient feed flavor of an Atom Feed"""
|
||||
|
||||
_tag = 'feed'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.GDataFeed._children.copy()
|
||||
_attributes = gdata.GDataFeed._attributes.copy()
|
||||
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry',
|
||||
[EmailListRecipientEntry])
|
||||
|
||||
def __init__(self, author=None, category=None, contributor=None,
|
||||
generator=None, icon=None, atom_id=None, link=None, logo=None,
|
||||
rights=None, subtitle=None, title=None, updated=None,
|
||||
entry=None, total_results=None, start_index=None,
|
||||
items_per_page=None, extension_elements=None,
|
||||
extension_attributes=None, text=None):
|
||||
gdata.GDataFeed.__init__(self, author=author, category=category,
|
||||
contributor=contributor, generator=generator,
|
||||
icon=icon, atom_id=atom_id, link=link,
|
||||
logo=logo, rights=rights, subtitle=subtitle,
|
||||
title=title, updated=updated, entry=entry,
|
||||
total_results=total_results,
|
||||
start_index=start_index,
|
||||
items_per_page=items_per_page,
|
||||
extension_elements=extension_elements,
|
||||
extension_attributes=extension_attributes,
|
||||
text=text)
|
||||
|
||||
|
||||
def EmailListRecipientFeedFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(EmailListRecipientFeed, xml_string)
|
||||
|
||||
|
||||
class Property(atom.AtomBase):
|
||||
"""The Google Apps Property element"""
|
||||
|
||||
_tag = 'property'
|
||||
_namespace = APPS_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
_attributes['name'] = 'name'
|
||||
_attributes['value'] = 'value'
|
||||
|
||||
def __init__(self, name=None, value=None, extension_elements=None,
|
||||
extension_attributes=None, text=None):
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
|
||||
def PropertyFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(Property, xml_string)
|
||||
|
||||
|
||||
class PropertyEntry(gdata.GDataEntry):
|
||||
"""A Google Apps Property flavor of an Atom Entry"""
|
||||
|
||||
_tag = 'entry'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.GDataEntry._children.copy()
|
||||
_attributes = gdata.GDataEntry._attributes.copy()
|
||||
_children['{%s}property' % APPS_NAMESPACE] = ('property', [Property])
|
||||
|
||||
def __init__(self, author=None, category=None, content=None,
|
||||
atom_id=None, link=None, published=None,
|
||||
title=None, updated=None,
|
||||
property=None,
|
||||
extended_property=None,
|
||||
extension_elements=None, extension_attributes=None, text=None):
|
||||
|
||||
gdata.GDataEntry.__init__(self, author=author, category=category,
|
||||
content=content,
|
||||
atom_id=atom_id, link=link, published=published,
|
||||
title=title, updated=updated)
|
||||
self.property = property
|
||||
self.extended_property = extended_property or []
|
||||
self.text = text
|
||||
self.extension_elements = extension_elements or []
|
||||
self.extension_attributes = extension_attributes or {}
|
||||
|
||||
|
||||
def PropertyEntryFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(PropertyEntry, xml_string)
|
||||
|
||||
class PropertyFeed(gdata.GDataFeed, gdata.LinkFinder):
|
||||
"""A Google Apps Property feed flavor of an Atom Feed"""
|
||||
|
||||
_tag = 'feed'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.GDataFeed._children.copy()
|
||||
_attributes = gdata.GDataFeed._attributes.copy()
|
||||
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [PropertyEntry])
|
||||
|
||||
def __init__(self, author=None, category=None, contributor=None,
|
||||
generator=None, icon=None, atom_id=None, link=None, logo=None,
|
||||
rights=None, subtitle=None, title=None, updated=None,
|
||||
entry=None, total_results=None, start_index=None,
|
||||
items_per_page=None, extension_elements=None,
|
||||
extension_attributes=None, text=None):
|
||||
gdata.GDataFeed.__init__(self, author=author, category=category,
|
||||
contributor=contributor, generator=generator,
|
||||
icon=icon, atom_id=atom_id, link=link,
|
||||
logo=logo, rights=rights, subtitle=subtitle,
|
||||
title=title, updated=updated, entry=entry,
|
||||
total_results=total_results,
|
||||
start_index=start_index,
|
||||
items_per_page=items_per_page,
|
||||
extension_elements=extension_elements,
|
||||
extension_attributes=extension_attributes,
|
||||
text=text)
|
||||
|
||||
def PropertyFeedFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(PropertyFeed, xml_string)
|
||||
278
src/gam/gdata/apps/audit/service.py
Normal file
278
src/gam/gdata/apps/audit/service.py
Normal file
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright (C) 2008 Google, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Allow Google Apps domain administrators to audit user data.
|
||||
|
||||
AuditService: Set auditing."""
|
||||
|
||||
__author__ = 'jlee@pbu.edu'
|
||||
|
||||
from base64 import b64encode
|
||||
|
||||
import gdata.apps
|
||||
import gdata.apps.service
|
||||
import gdata.service
|
||||
|
||||
class AuditService(gdata.apps.service.PropertyService):
|
||||
"""Client for the Google Apps Audit service."""
|
||||
|
||||
def _serviceUrl(self, setting_id, domain=None, user=None):
|
||||
if domain is None:
|
||||
domain = self.domain
|
||||
if user is None:
|
||||
return '/a/feeds/compliance/audit/%s/%s' % (setting_id, domain)
|
||||
else:
|
||||
return '/a/feeds/compliance/audit/%s/%s/%s' % (setting_id, domain, user)
|
||||
|
||||
def updatePGPKey(self, pgpkey):
|
||||
"""Updates Public PGP Key Google uses to encrypt audit data
|
||||
|
||||
Args:
|
||||
pgpkey: string, ASCII text of PGP Public Key to be used
|
||||
|
||||
Returns:
|
||||
A dict containing the result of the POST operation."""
|
||||
|
||||
uri = self._serviceUrl('publickey')
|
||||
b64pgpkey = b64encode(pgpkey)
|
||||
properties = {}
|
||||
properties['publicKey'] = b64pgpkey
|
||||
return self._PostProperties(uri, properties)
|
||||
|
||||
def createEmailMonitor(self, source_user, destination_user, end_date,
|
||||
begin_date=None, incoming_headers_only=False,
|
||||
outgoing_headers_only=False, drafts=False,
|
||||
drafts_headers_only=False, chats=False,
|
||||
chats_headers_only=False):
|
||||
"""Creates a email monitor, forwarding the source_users emails/chats
|
||||
|
||||
Args:
|
||||
source_user: string, the user whose email will be audited
|
||||
destination_user: string, the user to receive the audited email
|
||||
end_date: string, the date the audit will end in
|
||||
"yyyy-MM-dd HH:mm" format, required
|
||||
begin_date: string, the date the audit will start in
|
||||
"yyyy-MM-dd HH:mm" format, leave blank to use current time
|
||||
incoming_headers_only: boolean, whether to audit only the headers of
|
||||
mail delivered to source user
|
||||
outgoing_headers_only: boolean, whether to audit only the headers of
|
||||
mail sent from the source user
|
||||
drafts: boolean, whether to audit draft messages of the source user
|
||||
drafts_headers_only: boolean, whether to audit only the headers of
|
||||
mail drafts saved by the user
|
||||
chats: boolean, whether to audit archived chats of the source user
|
||||
chats_headers_only: boolean, whether to audit only the headers of
|
||||
archived chats of the source user
|
||||
|
||||
Returns:
|
||||
A dict containing the result of the POST operation."""
|
||||
|
||||
uri = self._serviceUrl('mail/monitor', user=source_user)
|
||||
properties = {}
|
||||
properties['destUserName'] = destination_user
|
||||
if begin_date is not None:
|
||||
properties['beginDate'] = begin_date
|
||||
properties['endDate'] = end_date
|
||||
if incoming_headers_only:
|
||||
properties['incomingEmailMonitorLevel'] = 'HEADER_ONLY'
|
||||
else:
|
||||
properties['incomingEmailMonitorLevel'] = 'FULL_MESSAGE'
|
||||
if outgoing_headers_only:
|
||||
properties['outgoingEmailMonitorLevel'] = 'HEADER_ONLY'
|
||||
else:
|
||||
properties['outgoingEmailMonitorLevel'] = 'FULL_MESSAGE'
|
||||
if drafts:
|
||||
if drafts_headers_only:
|
||||
properties['draftMonitorLevel'] = 'HEADER_ONLY'
|
||||
else:
|
||||
properties['draftMonitorLevel'] = 'FULL_MESSAGE'
|
||||
if chats:
|
||||
if chats_headers_only:
|
||||
properties['chatMonitorLevel'] = 'HEADER_ONLY'
|
||||
else:
|
||||
properties['chatMonitorLevel'] = 'FULL_MESSAGE'
|
||||
return self._PostProperties(uri, properties)
|
||||
|
||||
def getEmailMonitors(self, user):
|
||||
""""Gets the email monitors for the given user
|
||||
|
||||
Args:
|
||||
user: string, the user to retrieve email monitors for
|
||||
|
||||
Returns:
|
||||
list results of the POST operation
|
||||
|
||||
"""
|
||||
uri = self._serviceUrl('mail/monitor', user=user)
|
||||
return self._GetPropertiesList(uri)
|
||||
|
||||
def deleteEmailMonitor(self, source_user, destination_user):
|
||||
"""Deletes the email monitor for the given user
|
||||
|
||||
Args:
|
||||
source_user: string, the user who is being monitored
|
||||
destination_user: string, theuser who recieves the monitored emails
|
||||
|
||||
Returns:
|
||||
Nothing
|
||||
"""
|
||||
|
||||
uri = self._serviceUrl('mail/monitor', user=source_user+'/'+destination_user)
|
||||
try:
|
||||
return self._DeleteProperties(uri)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def createAccountInformationRequest(self, user):
|
||||
"""Creates a request for account auditing details
|
||||
|
||||
Args:
|
||||
user: string, the user to request account information for
|
||||
|
||||
Returns:
|
||||
A dict containing the result of the post operation."""
|
||||
|
||||
uri = self._serviceUrl('account', user=user)
|
||||
properties = {}
|
||||
#XML Body is left empty
|
||||
try:
|
||||
return self._PostProperties(uri, properties)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def getAccountInformationRequestStatus(self, user, request_id):
|
||||
"""Gets the status of an account auditing request
|
||||
|
||||
Args:
|
||||
user: string, the user whose account auditing details were requested
|
||||
request_id: string, the request_id
|
||||
|
||||
Returns:
|
||||
A dict containing the result of the get operation."""
|
||||
|
||||
uri = self._serviceUrl('account', user=user+'/'+request_id)
|
||||
try:
|
||||
return self._GetProperties(uri)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def getAllAccountInformationRequestsStatus(self):
|
||||
"""Gets the status of all account auditing requests for the domain
|
||||
|
||||
Args:
|
||||
None
|
||||
|
||||
Returns:
|
||||
list results of the POST operation
|
||||
"""
|
||||
|
||||
uri = self._serviceUrl('account')
|
||||
return self._GetPropertiesList(uri)
|
||||
|
||||
|
||||
def deleteAccountInformationRequest(self, user, request_id):
|
||||
"""Deletes the request for account auditing information
|
||||
|
||||
Args:
|
||||
user: string, the user whose account auditing details were requested
|
||||
request_id: string, the request_id
|
||||
|
||||
Returns:
|
||||
Nothing
|
||||
"""
|
||||
|
||||
uri = self._serviceUrl('account', user=user+'/'+request_id)
|
||||
try:
|
||||
return self._DeleteProperties(uri)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def createMailboxExportRequest(self, user, begin_date=None, end_date=None, include_deleted=False, search_query=None, headers_only=False):
|
||||
"""Creates a mailbox export request
|
||||
|
||||
Args:
|
||||
user: string, the user whose mailbox export is being requested
|
||||
begin_date: string, date of earliest emails to export, optional, defaults to date of account creation
|
||||
format is 'yyyy-MM-dd HH:mm'
|
||||
end_date: string, date of latest emails to export, optional, defaults to current date
|
||||
format is 'yyyy-MM-dd HH:mm'
|
||||
include_deleted: boolean, whether to include deleted emails in export, mutually exclusive with search_query
|
||||
search_query: string, gmail style search query, matched emails will be exported, mutually exclusive with include_deleted
|
||||
|
||||
Returns:
|
||||
A dict containing the result of the post operation."""
|
||||
|
||||
uri = self._serviceUrl('mail/export', user=user)
|
||||
properties = {}
|
||||
if begin_date is not None:
|
||||
properties['beginDate'] = begin_date
|
||||
if end_date is not None:
|
||||
properties['endDate'] = end_date
|
||||
if include_deleted is not None:
|
||||
properties['includeDeleted'] = gdata.apps.service._bool2str(include_deleted)
|
||||
if search_query is not None:
|
||||
properties['searchQuery'] = search_query
|
||||
if headers_only is True:
|
||||
properties['packageContent'] = 'HEADER_ONLY'
|
||||
else:
|
||||
properties['packageContent'] = 'FULL_MESSAGE'
|
||||
return self._PostProperties(uri, properties)
|
||||
|
||||
def getMailboxExportRequestStatus(self, user, request_id):
|
||||
"""Gets the status of an mailbox export request
|
||||
|
||||
Args:
|
||||
user: string, the user whose mailbox were requested
|
||||
request_id: string, the request_id
|
||||
|
||||
Returns:
|
||||
A dict containing the result of the get operation."""
|
||||
|
||||
uri = self._serviceUrl('mail/export', user=user+'/'+request_id)
|
||||
try:
|
||||
return self._GetProperties(uri)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def getAllMailboxExportRequestsStatus(self):
|
||||
"""Gets the status of all mailbox export requests for the domain
|
||||
|
||||
Args:
|
||||
None
|
||||
|
||||
Returns:
|
||||
list results of the POST operation
|
||||
"""
|
||||
|
||||
uri = self._serviceUrl('mail/export')
|
||||
return self._GetPropertiesList(uri)
|
||||
|
||||
|
||||
def deleteMailboxExportRequest(self, user, request_id):
|
||||
"""Deletes the request for mailbox export
|
||||
|
||||
Args:
|
||||
user: string, the user whose mailbox were requested
|
||||
request_id: string, the request_id
|
||||
|
||||
Returns:
|
||||
Nothing
|
||||
"""
|
||||
|
||||
uri = self._serviceUrl('mail/export', user=user+'/'+request_id)
|
||||
try:
|
||||
return self._DeleteProperties(uri)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
874
src/gam/gdata/apps/contacts/__init__.py
Normal file
874
src/gam/gdata/apps/contacts/__init__.py
Normal file
@@ -0,0 +1,874 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2009 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Data model classes for parsing and generating XML for the Contacts API."""
|
||||
|
||||
import atom
|
||||
import gdata
|
||||
|
||||
## Constants from http://code.google.com/apis/gdata/elements.html ##
|
||||
REL_HOME = 'http://schemas.google.com/g/2005#home'
|
||||
REL_WORK = 'http://schemas.google.com/g/2005#work'
|
||||
REL_OTHER = 'http://schemas.google.com/g/2005#other'
|
||||
|
||||
IM_AIM = 'http://schemas.google.com/g/2005#AIM' # AOL Instant Messenger protocol
|
||||
IM_MSN = 'http://schemas.google.com/g/2005#MSN' # MSN Messenger protocol
|
||||
IM_YAHOO = 'http://schemas.google.com/g/2005#YAHOO' # Yahoo Messenger protocol
|
||||
IM_SKYPE = 'http://schemas.google.com/g/2005#SKYPE' # Skype protocol
|
||||
IM_QQ = 'http://schemas.google.com/g/2005#QQ' # QQ protocol
|
||||
IM_GOOGLE_TALK = 'http://schemas.google.com/g/2005#GOOGLE_TALK' # Google Talk protocol
|
||||
IM_ICQ = 'http://schemas.google.com/g/2005#ICQ' # ICQ protocol
|
||||
IM_JABBER = 'http://schemas.google.com/g/2005#JABBER' # Jabber protocol
|
||||
IM_NETMEETING = 'http://schemas.google.com/g/2005#NETMEETING' # NetMeeting
|
||||
|
||||
PHOTO_LINK_REL = 'http://schemas.google.com/contacts/2008/rel#photo'
|
||||
PHOTO_EDIT_LINK_REL = 'http://schemas.google.com/contacts/2008/rel#edit-photo'
|
||||
|
||||
# Different phone types, for more info see:
|
||||
# http://code.google.com/apis/gdata/docs/2.0/elements.html#gdPhoneNumber
|
||||
PHONE_ASSISTANT = 'http://schemas.google.com/g/2005#assistant'
|
||||
PHONE_CALLBACK = 'http://schemas.google.com/g/2005#callback'
|
||||
PHONE_CAR = 'http://schemas.google.com/g/2005#car'
|
||||
PHONE_COMPANY_MAIN = 'http://schemas.google.com/g/2005#company_main'
|
||||
PHONE_FAX = 'http://schemas.google.com/g/2005#fax'
|
||||
PHONE_GENERAL = 'http://schemas.google.com/g/2005#general'
|
||||
PHONE_HOME = REL_HOME
|
||||
PHONE_HOME_FAX = 'http://schemas.google.com/g/2005#home_fax'
|
||||
PHONE_INTERNAL = 'http://schemas.google.com/g/2005#internal-extension'
|
||||
PHONE_ISDN = 'http://schemas.google.com/g/2005#isdn'
|
||||
PHONE_MAIN = 'http://schemas.google.com/g/2005#main'
|
||||
PHONE_MOBILE = 'http://schemas.google.com/g/2005#mobile'
|
||||
PHONE_OTHER = REL_OTHER
|
||||
PHONE_OTHER_FAX = 'http://schemas.google.com/g/2005#other_fax'
|
||||
PHONE_PAGER = 'http://schemas.google.com/g/2005#pager'
|
||||
PHONE_RADIO = 'http://schemas.google.com/g/2005#radio'
|
||||
PHONE_SATELLITE = 'http://schemas.google.com/g/2005#satellite'
|
||||
PHONE_TELEX = 'http://schemas.google.com/g/2005#telex'
|
||||
PHONE_TTY_TDD = 'http://schemas.google.com/g/2005#tty_tdd'
|
||||
PHONE_VOIP = 'http://schemas.google.com/g/2005#voip'
|
||||
PHONE_WORK = REL_WORK
|
||||
PHONE_WORK_FAX = 'http://schemas.google.com/g/2005#work_fax'
|
||||
PHONE_WORK_MOBILE = 'http://schemas.google.com/g/2005#work_mobile'
|
||||
PHONE_WORK_PAGER = 'http://schemas.google.com/g/2005#work_pager'
|
||||
|
||||
MAIL_BOTH = 'http://schemas.google.com/g/2005#both'
|
||||
MAIL_LETTERS = 'http://schemas.google.com/g/2005#letters'
|
||||
MAIL_PARCELS = 'http://schemas.google.com/g/2005#parcels'
|
||||
MAIL_NEITHER = 'http://schemas.google.com/g/2005#neither'
|
||||
|
||||
GENERAL_ADDRESS = 'http://schemas.google.com/g/2005#general'
|
||||
LOCAL_ADDRESS = 'http://schemas.google.com/g/2005#local'
|
||||
|
||||
EXTERNAL_ID_ORGANIZATION = 'organization'
|
||||
|
||||
RELATION_MANAGER = 'manager'
|
||||
|
||||
CONTACTS_NAMESPACE = 'http://schemas.google.com/contact/2008'
|
||||
|
||||
|
||||
class GDataBase(atom.AtomBase):
|
||||
"""The Google Contacts intermediate class from atom.AtomBase."""
|
||||
_namespace = gdata.GDATA_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
|
||||
def __init__(self, text=None):
|
||||
atom.AtomBase.__init__(self, text=text)
|
||||
|
||||
class ContactsBase(GDataBase):
|
||||
"""The Google Contacts intermediate class for Contacts namespace."""
|
||||
_namespace = CONTACTS_NAMESPACE
|
||||
|
||||
class BillingInformation(ContactsBase):
|
||||
"""The gContact:billingInformation element."""
|
||||
_tag = 'billingInformation'
|
||||
|
||||
class Birthday(ContactsBase):
|
||||
"""The gContact:birthday element."""
|
||||
_tag = 'birthday'
|
||||
_children = ContactsBase._children.copy()
|
||||
_attributes = ContactsBase._attributes.copy()
|
||||
_attributes['when'] = 'when'
|
||||
|
||||
def __init__(self, when=None):
|
||||
ContactsBase.__init__(self)
|
||||
self.when = when
|
||||
|
||||
class CalendarLink(ContactsBase):
|
||||
"""The gContact:calendarLink element."""
|
||||
_tag = 'calendarLink'
|
||||
_children = ContactsBase._children.copy()
|
||||
_attributes = ContactsBase._attributes.copy()
|
||||
_attributes['href'] = 'href'
|
||||
_attributes['label'] = 'label'
|
||||
_attributes['primary'] = 'primary'
|
||||
_attributes['rel'] = 'rel'
|
||||
|
||||
def __init__(self, href=None, label=None, primary='false', rel=None):
|
||||
ContactsBase.__init__(self)
|
||||
self.href = href
|
||||
self.label = label
|
||||
self.primary = primary
|
||||
self.rel = rel
|
||||
|
||||
class Content(atom.AtomBase):
|
||||
"""The Google Contacts Content element."""
|
||||
_tag = 'content'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
|
||||
def __init__(self, text=None):
|
||||
atom.AtomBase.__init__(self, text=text)
|
||||
|
||||
class DirectoryServer(ContactsBase):
|
||||
"""The gContact:directoryServer element."""
|
||||
_tag = 'directoryServer'
|
||||
|
||||
class Email(GDataBase):
|
||||
"""The gd:email element."""
|
||||
_tag = 'email'
|
||||
_children = GDataBase._children.copy()
|
||||
_attributes = GDataBase._attributes.copy()
|
||||
_attributes['address'] = 'address'
|
||||
_attributes['primary'] = 'primary'
|
||||
_attributes['rel'] = 'rel'
|
||||
_attributes['label'] = 'label'
|
||||
|
||||
def __init__(self, label=None, rel=None, address=None, primary='false'):
|
||||
GDataBase.__init__(self)
|
||||
self.label = label
|
||||
self.rel = rel
|
||||
self.address = address
|
||||
self.primary = primary
|
||||
|
||||
class When(GDataBase):
|
||||
"""The Google Contacts when element."""
|
||||
_tag = 'when'
|
||||
_children = GDataBase._children.copy()
|
||||
_attributes = GDataBase._attributes.copy()
|
||||
_attributes['startTime'] = 'startTime'
|
||||
_attributes['label'] = 'label'
|
||||
|
||||
def __init__(self, startTime=None, label=None):
|
||||
GDataBase.__init__(self)
|
||||
self.startTime = startTime
|
||||
self.label = label
|
||||
|
||||
class Event(ContactsBase):
|
||||
"""The gContact:event element."""
|
||||
_tag = 'event'
|
||||
_children = ContactsBase._children.copy()
|
||||
_attributes = ContactsBase._attributes.copy()
|
||||
_attributes['label'] = 'label'
|
||||
_attributes['rel'] = 'rel'
|
||||
_children['{%s}when' % GDataBase._namespace] = ('when', When)
|
||||
|
||||
def __init__(self, label=None, rel=None, when=None):
|
||||
ContactsBase.__init__(self)
|
||||
self.label = label
|
||||
self.rel = rel
|
||||
self.when = when
|
||||
|
||||
def EventFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(Event, xml_string)
|
||||
|
||||
class ExternalId(ContactsBase):
|
||||
"""The gContact:externalId element."""
|
||||
_tag = 'externalId'
|
||||
_children = ContactsBase._children.copy()
|
||||
_attributes = ContactsBase._attributes.copy()
|
||||
_attributes['label'] = 'label'
|
||||
_attributes['rel'] = 'rel'
|
||||
_attributes['value'] = 'value'
|
||||
|
||||
def __init__(self, label=None, rel=None, value=None):
|
||||
ContactsBase.__init__(self)
|
||||
self.label = label
|
||||
self.rel = rel
|
||||
self.value = value
|
||||
|
||||
def ExternalIdFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(ExternalId, xml_string)
|
||||
|
||||
class Gender(ContactsBase):
|
||||
"""The gContact:gender element."""
|
||||
_tag = 'gender'
|
||||
_children = ContactsBase._children.copy()
|
||||
_attributes = ContactsBase._attributes.copy()
|
||||
_attributes['value'] = 'value'
|
||||
|
||||
def __init__(self, value=None):
|
||||
ContactsBase.__init__(self)
|
||||
self.value = value
|
||||
|
||||
class Hobby(ContactsBase):
|
||||
"""The gContact:hobby element."""
|
||||
_tag = 'hobby'
|
||||
|
||||
class IM(GDataBase):
|
||||
"""The gd:im element."""
|
||||
_tag = 'im'
|
||||
_children = GDataBase._children.copy()
|
||||
_attributes = GDataBase._attributes.copy()
|
||||
_attributes['address'] = 'address'
|
||||
_attributes['primary'] = 'primary'
|
||||
_attributes['protocol'] = 'protocol'
|
||||
_attributes['label'] = 'label'
|
||||
_attributes['rel'] = 'rel'
|
||||
|
||||
def __init__(self, primary='false', rel=None, address=None, protocol=None, label=None):
|
||||
GDataBase.__init__(self)
|
||||
self.protocol = protocol
|
||||
self.address = address
|
||||
self.primary = primary
|
||||
self.rel = rel
|
||||
self.label = label
|
||||
|
||||
class Initials(ContactsBase):
|
||||
"""The gContact:initials element."""
|
||||
_tag = 'initials'
|
||||
|
||||
class Jot(ContactsBase):
|
||||
"""The gContact:jot element."""
|
||||
_tag = 'jot'
|
||||
_children = ContactsBase._children.copy()
|
||||
_attributes = ContactsBase._attributes.copy()
|
||||
_attributes['rel'] = 'rel'
|
||||
|
||||
def __init__(self, rel=None, text=None):
|
||||
ContactsBase.__init__(self, text=text)
|
||||
self.rel = rel
|
||||
|
||||
class Language(ContactsBase):
|
||||
"""The gContact:language element."""
|
||||
_tag = 'language'
|
||||
_children = ContactsBase._children.copy()
|
||||
_attributes = ContactsBase._attributes.copy()
|
||||
_attributes['code'] = 'code'
|
||||
_attributes['label'] = 'label'
|
||||
|
||||
def __init__(self, code=None, label=None):
|
||||
ContactsBase.__init__(self)
|
||||
self.code = code
|
||||
self.label = label
|
||||
|
||||
class MaidenName(ContactsBase):
|
||||
"""The gContact:maidenName element."""
|
||||
_tag = 'maidenName'
|
||||
|
||||
class Mileage(ContactsBase):
|
||||
"""The gContact:mileage element."""
|
||||
_tag = 'mileage'
|
||||
|
||||
class NamePrefix(GDataBase):
|
||||
"""The gd:namePrefix element."""
|
||||
_tag = 'namePrefix'
|
||||
|
||||
class GivenName(GDataBase):
|
||||
"""The gd:givenName element."""
|
||||
_tag = 'givenName'
|
||||
|
||||
class AdditionalName(GDataBase):
|
||||
"""The gd:additionalName element."""
|
||||
_tag = 'additionalName'
|
||||
|
||||
class FamilyName(GDataBase):
|
||||
"""The gd:familyName element."""
|
||||
_tag = 'familyName'
|
||||
|
||||
class NameSuffix(GDataBase):
|
||||
"""The gd:nameSuffix element."""
|
||||
_tag = 'nameSuffix'
|
||||
|
||||
class FullName(GDataBase):
|
||||
"""The gd:fullName element."""
|
||||
_tag = 'fullName'
|
||||
|
||||
class Name(GDataBase):
|
||||
"""The gd:name element."""
|
||||
_tag = 'name'
|
||||
_children = GDataBase._children.copy()
|
||||
_attributes = GDataBase._attributes.copy()
|
||||
_children['{%s}namePrefix' % GDataBase._namespace] = ('name_prefix', NamePrefix)
|
||||
_children['{%s}givenName' % GDataBase._namespace] = ('given_name', GivenName)
|
||||
_children['{%s}additionalName' % GDataBase._namespace] = ('additional_name', AdditionalName)
|
||||
_children['{%s}familyName' % GDataBase._namespace] = ('family_name', FamilyName)
|
||||
_children['{%s}nameSuffix' % GDataBase._namespace] = ('name_suffix', NameSuffix)
|
||||
_children['{%s}fullName' % GDataBase._namespace] = ('full_name', FullName)
|
||||
|
||||
def __init__(self, given_name=None, additional_name=None, family_name=None,
|
||||
name_prefix=None, name_suffix=None, full_name=None,):
|
||||
GDataBase.__init__(self)
|
||||
self.given_name = given_name
|
||||
self.additional_name = additional_name
|
||||
self.family_name = family_name
|
||||
self.name_prefix = name_prefix
|
||||
self.name_suffix = name_suffix
|
||||
self.full_name = full_name
|
||||
|
||||
class Nickname(ContactsBase):
|
||||
"""The gContact:nickname element."""
|
||||
_tag = 'nickname'
|
||||
|
||||
class Occupation(ContactsBase):
|
||||
"""The gContact:occupation element."""
|
||||
_tag = 'occupation'
|
||||
|
||||
class PhoneNumber(GDataBase):
|
||||
"""The gd:phoneNumber element."""
|
||||
_tag = 'phoneNumber'
|
||||
_children = GDataBase._children.copy()
|
||||
_attributes = GDataBase._attributes.copy()
|
||||
_attributes['label'] = 'label'
|
||||
_attributes['rel'] = 'rel'
|
||||
_attributes['uri'] = 'uri'
|
||||
_attributes['primary'] = 'primary'
|
||||
|
||||
def __init__(self, label=None, rel=None, uri=None, primary='false', text=None):
|
||||
GDataBase.__init__(self, text=text)
|
||||
self.label = label
|
||||
self.rel = rel
|
||||
self.uri = uri
|
||||
self.primary = primary
|
||||
|
||||
class OrgName(GDataBase):
|
||||
"""The gd:orgName element."""
|
||||
_tag = 'orgName'
|
||||
|
||||
class OrgTitle(GDataBase):
|
||||
"""The gd:orgTitle element."""
|
||||
_tag = 'orgTitle'
|
||||
|
||||
class OrgSymbol(GDataBase):
|
||||
"""The gd:orgSymbol element."""
|
||||
_tag = 'orgSymbol'
|
||||
|
||||
class OrgDepartment(GDataBase):
|
||||
"""The gd:orgDepartment element."""
|
||||
_tag = 'orgDepartment'
|
||||
|
||||
class OrgJobDescription(GDataBase):
|
||||
"""The gd:orgJobDescription element."""
|
||||
_tag = 'orgJobDescription'
|
||||
|
||||
class Where(GDataBase):
|
||||
"""The gd:where element."""
|
||||
_tag = 'where'
|
||||
_children = GDataBase._children.copy()
|
||||
_attributes = GDataBase._attributes.copy()
|
||||
_attributes['valueString'] = 'value_string'
|
||||
|
||||
def __init__(self, value_string=None):
|
||||
GDataBase.__init__(self)
|
||||
self.value_string = value_string
|
||||
|
||||
class Organization(GDataBase):
|
||||
"""The gd:organization element."""
|
||||
_tag = 'organization'
|
||||
_children = GDataBase._children.copy()
|
||||
_attributes = GDataBase._attributes.copy()
|
||||
_attributes['label'] = 'label'
|
||||
_attributes['rel'] = 'rel'
|
||||
_attributes['primary'] = 'primary'
|
||||
_children['{%s}orgName' % GDataBase._namespace] = ('name', OrgName)
|
||||
_children['{%s}orgSymbol' % GDataBase._namespace] = ('symbol', OrgSymbol)
|
||||
_children['{%s}orgTitle' % GDataBase._namespace] = ('title', OrgTitle)
|
||||
_children['{%s}orgDepartment' % GDataBase._namespace] = ('department', OrgDepartment)
|
||||
_children['{%s}orgJobDescription' % GDataBase._namespace] = ('job_description', OrgJobDescription)
|
||||
_children['{%s}where' % GDataBase._namespace] = ('where', Where)
|
||||
|
||||
def __init__(self, label=None, rel=None, primary='false', name=None,
|
||||
title=None, symbol=None, department=None, job_description=None, where=None,):
|
||||
GDataBase.__init__(self)
|
||||
self.label = label
|
||||
self.rel = rel
|
||||
self.primary = primary
|
||||
self.name = name
|
||||
self.symbol = symbol
|
||||
self.title = title
|
||||
self.department = department
|
||||
self.job_description = job_description
|
||||
self.where = where
|
||||
|
||||
class PostalAddress(GDataBase):
|
||||
"""The gd:postalAddress element."""
|
||||
_tag = 'postalAddress'
|
||||
_children = GDataBase._children.copy()
|
||||
_attributes = GDataBase._attributes.copy()
|
||||
_attributes['label'] = 'label'
|
||||
_attributes['rel'] = 'rel'
|
||||
_attributes['primary'] = 'primary'
|
||||
|
||||
def __init__(self, primary=None, rel=None, label=None, text=None):
|
||||
GDataBase.__init__(self, text=text)
|
||||
self.label = label
|
||||
self.rel = rel
|
||||
self.primary = primary
|
||||
|
||||
class Priority(ContactsBase):
|
||||
"""The gContact:priority element."""
|
||||
_tag = 'priority'
|
||||
_children = ContactsBase._children.copy()
|
||||
_attributes = ContactsBase._attributes.copy()
|
||||
_attributes['rel'] = 'rel'
|
||||
|
||||
def __init__(self, rel=None):
|
||||
ContactsBase.__init__(self)
|
||||
self.rel = rel
|
||||
|
||||
class Relation(ContactsBase):
|
||||
"""The gContact:relation element."""
|
||||
_tag = 'relation'
|
||||
_children = ContactsBase._children.copy()
|
||||
_attributes = ContactsBase._attributes.copy()
|
||||
_attributes['label'] = 'label'
|
||||
_attributes['rel'] = 'rel'
|
||||
|
||||
def __init__(self, label=None, rel=None, text=None):
|
||||
ContactsBase.__init__(self, text=text)
|
||||
self.label = label
|
||||
self.rel = rel
|
||||
|
||||
def RelationFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(Relation, xml_string)
|
||||
|
||||
class Sensitivity(ContactsBase):
|
||||
"""The gContact:sensitivity element."""
|
||||
_tag = 'sensitivity'
|
||||
_children = ContactsBase._children.copy()
|
||||
_attributes = ContactsBase._attributes.copy()
|
||||
_attributes['rel'] = 'rel'
|
||||
|
||||
def __init__(self, rel=None):
|
||||
ContactsBase.__init__(self)
|
||||
self.rel = rel
|
||||
|
||||
class ShortName(ContactsBase):
|
||||
"""The gContact:shortName element."""
|
||||
_tag = 'shortName'
|
||||
|
||||
class Street(GDataBase):
|
||||
"""The gd:street element.
|
||||
Can be street, avenue, road, etc. This element also includes the
|
||||
house number and room/apartment/flat/floor number.
|
||||
"""
|
||||
_tag = 'street'
|
||||
|
||||
class PoBox(GDataBase):
|
||||
"""The gd:pobox element.
|
||||
Covers actual P.O. boxes, drawers, locked bags, etc. This is usually
|
||||
but not always mutually exclusive with street.
|
||||
"""
|
||||
_tag = 'pobox'
|
||||
|
||||
class Neighborhood(GDataBase):
|
||||
"""The gd:neighborhood element.
|
||||
This is used to disambiguate a street address when a city contains more
|
||||
than one street with the same name, or to specify a small place whose
|
||||
mail is routed through a larger postal town. In China it could be a
|
||||
county or a minor city.
|
||||
"""
|
||||
_tag = 'neighborhood'
|
||||
|
||||
class City(GDataBase):
|
||||
"""The gd:city element.
|
||||
Can be city, village, town, borough, etc. This is the postal town and
|
||||
not necessarily the place of residence or place of business.
|
||||
"""
|
||||
_tag = 'city'
|
||||
|
||||
class Region(GDataBase):
|
||||
"""The gd:region element.
|
||||
A state, province, county (in Ireland), Land (in Germany),
|
||||
departement (in France), etc.
|
||||
"""
|
||||
_tag = 'region'
|
||||
|
||||
class Postcode(GDataBase):
|
||||
"""The gd:postcode element.
|
||||
Postal code. Usually country-wide, but sometimes specific to the
|
||||
city (e.g. "2" in "Dublin 2, Ireland" addresses).
|
||||
"""
|
||||
_tag = 'postcode'
|
||||
|
||||
class Country(GDataBase):
|
||||
"""The gd:country element.
|
||||
The name or code of the country.
|
||||
"""
|
||||
_tag = 'country'
|
||||
|
||||
class FormattedAddress(GDataBase):
|
||||
"""The gd:formattedAddress element."""
|
||||
_tag = 'formattedAddress'
|
||||
|
||||
class StructuredPostalAddress(GDataBase):
|
||||
"""The gd:structuredPostalAddress element."""
|
||||
_tag = 'structuredPostalAddress'
|
||||
_children = GDataBase._children.copy()
|
||||
_attributes = GDataBase._attributes.copy()
|
||||
_attributes['label'] = 'label'
|
||||
_attributes['rel'] = 'rel'
|
||||
_attributes['primary'] = 'primary'
|
||||
_children['{%s}street' % GDataBase._namespace] = ('street', Street)
|
||||
_children['{%s}pobox' % GDataBase._namespace] = ('pobox', PoBox)
|
||||
_children['{%s}neighborhood' % GDataBase._namespace] = ('neighborhood', Neighborhood)
|
||||
_children['{%s}city' % GDataBase._namespace] = ('city', City)
|
||||
_children['{%s}region' % GDataBase._namespace] = ('region', Region)
|
||||
_children['{%s}postcode' % GDataBase._namespace] = ('postcode', Postcode)
|
||||
_children['{%s}country' % GDataBase._namespace] = ('country', Country)
|
||||
_children['{%s}formattedAddress' % GDataBase._namespace] = ('formatted_address', FormattedAddress)
|
||||
|
||||
def __init__(self, rel=None, label=None, primary='false',
|
||||
street=None,
|
||||
pobox=None,
|
||||
neighborhood=None,
|
||||
city=None,
|
||||
region=None,
|
||||
postcode=None,
|
||||
country=None,
|
||||
formatted_address=None):
|
||||
GDataBase.__init__(self)
|
||||
self.label = label
|
||||
self.rel = rel
|
||||
self.primary = primary
|
||||
self.street = street
|
||||
self.pobox = pobox
|
||||
self.neighborhood = neighborhood
|
||||
self.city = city
|
||||
self.region = region
|
||||
self.postcode = postcode
|
||||
self.country = country
|
||||
self.formatted_address = formatted_address
|
||||
|
||||
class Subject(ContactsBase):
|
||||
"""The gContact:Subject element."""
|
||||
_tag = 'subject'
|
||||
|
||||
class UserDefinedField(ContactsBase):
|
||||
"""The gContact:userDefinedField element."""
|
||||
_tag = 'userDefinedField'
|
||||
_children = ContactsBase._children.copy()
|
||||
_attributes = ContactsBase._attributes.copy()
|
||||
_attributes['key'] = 'key'
|
||||
_attributes['value'] = 'value'
|
||||
|
||||
def __init__(self, key=None, value=None):
|
||||
ContactsBase.__init__(self)
|
||||
self.key = key
|
||||
self.value = value
|
||||
|
||||
def UserDefinedFieldFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(UserDefinedField, xml_string)
|
||||
|
||||
class Website(ContactsBase):
|
||||
"""The gContact:Website element."""
|
||||
_tag = 'website'
|
||||
_children = ContactsBase._children.copy()
|
||||
_attributes = ContactsBase._attributes.copy()
|
||||
_attributes['href'] = 'href'
|
||||
_attributes['label'] = 'label'
|
||||
_attributes['primary'] = 'primary'
|
||||
_attributes['rel'] = 'rel'
|
||||
|
||||
def __init__(self, href=None, label=None, primary='false', rel=None):
|
||||
ContactsBase.__init__(self)
|
||||
self.href = href
|
||||
self.label = label
|
||||
self.primary = primary
|
||||
self.rel = rel
|
||||
|
||||
def WebsiteFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(Website, xml_string)
|
||||
|
||||
class PersonEntry(gdata.BatchEntry):
|
||||
"""Base class for ContactEntry and ProfileEntry."""
|
||||
_children = gdata.BatchEntry._children.copy()
|
||||
_children['{%s}billingInformation' % CONTACTS_NAMESPACE] = ('billingInformation', BillingInformation)
|
||||
_children['{%s}birthday' % CONTACTS_NAMESPACE] = ('birthday', Birthday)
|
||||
_children['{%s}calendarLink' % CONTACTS_NAMESPACE] = ('calendarLink', [CalendarLink])
|
||||
_children['{%s}content' % atom.ATOM_NAMESPACE] = ('content', Content)
|
||||
_children['{%s}directoryServer' % CONTACTS_NAMESPACE] = ('directoryServer', DirectoryServer)
|
||||
_children['{%s}email' % gdata.GDATA_NAMESPACE] = ('email', [Email])
|
||||
_children['{%s}event' % CONTACTS_NAMESPACE] = ('event', [Event])
|
||||
_children['{%s}externalId' % CONTACTS_NAMESPACE] = ('externalId', [ExternalId])
|
||||
_children['{%s}gender' % CONTACTS_NAMESPACE] = ('gender', Gender)
|
||||
_children['{%s}hobby' % CONTACTS_NAMESPACE] = ('hobby', [Hobby])
|
||||
_children['{%s}im' % gdata.GDATA_NAMESPACE] = ('im', [IM])
|
||||
_children['{%s}initials' % CONTACTS_NAMESPACE] = ('initials', Initials)
|
||||
_children['{%s}jot' % CONTACTS_NAMESPACE] = ('jot', [Jot])
|
||||
_children['{%s}language' % CONTACTS_NAMESPACE] = ('language', Language)
|
||||
_children['{%s}maidenName' % CONTACTS_NAMESPACE] = ('maidenName', MaidenName)
|
||||
_children['{%s}mileage' % CONTACTS_NAMESPACE] = ('mileage', Mileage)
|
||||
_children['{%s}name' % gdata.GDATA_NAMESPACE] = ('name', Name)
|
||||
_children['{%s}nickname' % CONTACTS_NAMESPACE] = ('nickname', Nickname)
|
||||
_children['{%s}occupation' % CONTACTS_NAMESPACE] = ('occupation', Occupation)
|
||||
_children['{%s}organization' % gdata.GDATA_NAMESPACE] = ('organization', [Organization])
|
||||
_children['{%s}phoneNumber' % gdata.GDATA_NAMESPACE] = ('phoneNumber', [PhoneNumber])
|
||||
_children['{%s}postalAddress' % gdata.GDATA_NAMESPACE] = ('postalAddress', [PostalAddress])
|
||||
_children['{%s}priority' % CONTACTS_NAMESPACE] = ('priority', Priority)
|
||||
_children['{%s}relation' % CONTACTS_NAMESPACE] = ('relation', [Relation])
|
||||
_children['{%s}sensitivity' % CONTACTS_NAMESPACE] = ('sensitivity', Sensitivity)
|
||||
_children['{%s}shortName' % CONTACTS_NAMESPACE] = ('shortName', ShortName)
|
||||
_children['{%s}structuredPostalAddress' % gdata.GDATA_NAMESPACE] = ('structuredPostalAddress', [StructuredPostalAddress])
|
||||
_children['{%s}subject' % CONTACTS_NAMESPACE] = ('subject', Subject)
|
||||
_children['{%s}userDefinedField' % CONTACTS_NAMESPACE] = ('userDefinedField', [UserDefinedField])
|
||||
_children['{%s}website' % CONTACTS_NAMESPACE] = ('website', [Website])
|
||||
_children['{%s}where' % gdata.GDATA_NAMESPACE] = ('where', Where)
|
||||
|
||||
_attributes = gdata.BatchEntry._attributes.copy()
|
||||
_attributes['{%s}etag' % gdata.GDATA_NAMESPACE] = 'etag'
|
||||
|
||||
def __init__(self,
|
||||
billingInformation=None,
|
||||
birthday=None,
|
||||
calendarLink=None,
|
||||
content=None,
|
||||
directoryServer=None,
|
||||
email=None,
|
||||
event=None,
|
||||
externalId=None,
|
||||
gender=None,
|
||||
hobby=None,
|
||||
im=None,
|
||||
initials=None,
|
||||
jot=None,
|
||||
language=None,
|
||||
maidenName=None,
|
||||
mileage=None,
|
||||
name=None,
|
||||
nickname=None,
|
||||
occupation=None,
|
||||
organization=None,
|
||||
phoneNumber=None,
|
||||
postalAddress=None,
|
||||
priority=None,
|
||||
relation=None,
|
||||
sensitivity=None,
|
||||
shortName=None,
|
||||
structuredPostalAddress=None,
|
||||
subject=None,
|
||||
text=None,
|
||||
title=None,
|
||||
userDefinedField=None,
|
||||
website=None,
|
||||
where=None,
|
||||
etag=None):
|
||||
gdata.BatchEntry.__init__(self)
|
||||
self.billingInformation = billingInformation
|
||||
self.birthday = birthday
|
||||
self.calendarLink = calendarLink or []
|
||||
self.content = content
|
||||
self.directoryServer = directoryServer
|
||||
self.email = email or []
|
||||
self.event = event or []
|
||||
self.externalId = externalId or []
|
||||
self.gender = gender
|
||||
self.hobby = hobby or []
|
||||
self.im = im or []
|
||||
self.initials = initials
|
||||
self.jot = jot or []
|
||||
self.language = language
|
||||
self.maidenName = maidenName
|
||||
self.mileage = mileage
|
||||
self.name = name
|
||||
self.nickname = nickname
|
||||
self.occupation = occupation
|
||||
self.organization = organization or []
|
||||
self.phoneNumber = phoneNumber or []
|
||||
self.postalAddress = postalAddress or []
|
||||
self.priority = priority
|
||||
self.relation = relation or []
|
||||
self.sensitivity = sensitivity
|
||||
self.shortName = shortName
|
||||
self.structuredPostalAddress = structuredPostalAddress or []
|
||||
self.subject = subject
|
||||
self.text = text
|
||||
self.userDefinedField = userDefinedField or []
|
||||
self.website = website or []
|
||||
self.where = where
|
||||
self.extension_attributes = {}
|
||||
self.extension_elements = []
|
||||
self.etag = etag
|
||||
|
||||
class Deleted(GDataBase):
|
||||
"""The gd:Deleted element."""
|
||||
_tag = 'deleted'
|
||||
|
||||
class GroupMembershipInfo(ContactsBase):
|
||||
"""The Google Contacts GroupMembershipInfo element."""
|
||||
_tag = 'groupMembershipInfo'
|
||||
_children = ContactsBase._children.copy()
|
||||
_attributes = ContactsBase._attributes.copy()
|
||||
_attributes['deleted'] = 'deleted'
|
||||
_attributes['href'] = 'href'
|
||||
|
||||
def __init__(self, deleted=None, href=None, text=None):
|
||||
ContactsBase.__init__(self, text=text)
|
||||
self.deleted = deleted
|
||||
self.href = href
|
||||
|
||||
class ContactEntry(PersonEntry):
|
||||
"""Represents a contact."""
|
||||
_tag = 'entry'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = PersonEntry._children.copy()
|
||||
|
||||
_children['{%s}deleted' % gdata.GDATA_NAMESPACE] = ('deleted', Deleted)
|
||||
_children['{%s}groupMembershipInfo' % CONTACTS_NAMESPACE] = ('groupMembershipInfo', [GroupMembershipInfo])
|
||||
_children['{%s}extendedProperty' % gdata.GDATA_NAMESPACE] = ('extended_property', [gdata.ExtendedProperty])
|
||||
# Overwrite the organization rule in PersonEntry so that a ContactEntry
|
||||
# may only contain one <gd:organization> element.
|
||||
#_children['{%s}organization' % gdata.GDATA_NAMESPACE] = ('organization', Organization)
|
||||
|
||||
def __init__(self,
|
||||
billingInformation=None,
|
||||
birthday=None,
|
||||
calendarLink=None,
|
||||
content=None,
|
||||
deleted=None,
|
||||
directoryServer=None,
|
||||
email=None,
|
||||
event=None,
|
||||
extended_property=None,
|
||||
externalId=None,
|
||||
gender=None,
|
||||
groupMembershipInfo=None,
|
||||
hobby=None,
|
||||
im=None,
|
||||
initials=None,
|
||||
jot=None,
|
||||
language=None,
|
||||
maidenName=None,
|
||||
mileage=None,
|
||||
name=None,
|
||||
nickname=None,
|
||||
occupation=None,
|
||||
organization=None,
|
||||
phoneNumber=None,
|
||||
postalAddress=None,
|
||||
priority=None,
|
||||
relation=None,
|
||||
sensitivity=None,
|
||||
shortName=None,
|
||||
structuredPostalAddress=None,
|
||||
subject=None,
|
||||
text=None,
|
||||
title=None,
|
||||
userDefinedField=None,
|
||||
website=None,
|
||||
where=None,
|
||||
etag=None):
|
||||
PersonEntry.__init__(self,
|
||||
billingInformation=billingInformation,
|
||||
birthday=birthday,
|
||||
calendarLink=calendarLink,
|
||||
content=content,
|
||||
directoryServer=directoryServer,
|
||||
email=email,
|
||||
event=event,
|
||||
externalId=externalId,
|
||||
gender=gender,
|
||||
hobby=hobby,
|
||||
im=im,
|
||||
initials=initials,
|
||||
jot=jot,
|
||||
language=language,
|
||||
maidenName=maidenName,
|
||||
mileage=mileage,
|
||||
name=name,
|
||||
nickname=nickname,
|
||||
occupation=occupation,
|
||||
organization=organization,
|
||||
phoneNumber=phoneNumber,
|
||||
postalAddress=postalAddress,
|
||||
priority=priority,
|
||||
relation=relation,
|
||||
sensitivity=sensitivity,
|
||||
shortName=shortName,
|
||||
structuredPostalAddress=structuredPostalAddress,
|
||||
subject=subject,
|
||||
text=text,
|
||||
title=title,
|
||||
userDefinedField=userDefinedField,
|
||||
website=website,
|
||||
where=where,
|
||||
etag=etag)
|
||||
self.deleted = deleted
|
||||
self.extended_property = extended_property or []
|
||||
self.groupMembershipInfo = groupMembershipInfo or []
|
||||
|
||||
def GetPhotoLink(self):
|
||||
for a_link in self.link:
|
||||
if a_link.rel == PHOTO_LINK_REL:
|
||||
return a_link
|
||||
return None
|
||||
|
||||
def GetPhotoEditLink(self):
|
||||
for a_link in self.link:
|
||||
if a_link.rel == PHOTO_EDIT_LINK_REL:
|
||||
return a_link
|
||||
return None
|
||||
|
||||
def ContactEntryFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(ContactEntry, xml_string)
|
||||
|
||||
class ContactsFeed(gdata.BatchFeed, gdata.LinkFinder):
|
||||
"""A Google contacts feed flavor of an Atom Feed."""
|
||||
_tag = 'feed'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.BatchFeed._children.copy()
|
||||
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [ContactEntry])
|
||||
|
||||
def __init__(self):
|
||||
gdata.BatchFeed.__init__(self)
|
||||
|
||||
def ContactsFeedFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(ContactsFeed, xml_string)
|
||||
|
||||
class GroupEntry(gdata.BatchEntry):
|
||||
"""Represents a contact group."""
|
||||
_children = gdata.BatchEntry._children.copy()
|
||||
_children['{%s}deleted' % gdata.GDATA_NAMESPACE] = ('deleted', Deleted)
|
||||
_children['{%s}extendedProperty' % gdata.GDATA_NAMESPACE] = ('extended_property', [gdata.ExtendedProperty])
|
||||
|
||||
_attributes = gdata.BatchEntry._attributes.copy()
|
||||
_attributes['{%s}etag' % gdata.GDATA_NAMESPACE] = 'etag'
|
||||
|
||||
def __init__(self,
|
||||
title=None,
|
||||
deleted=None,
|
||||
extended_property=None,
|
||||
etag=None):
|
||||
gdata.BatchEntry.__init__(self)
|
||||
self.title = title
|
||||
self.deleted = deleted
|
||||
self.extended_property = extended_property or []
|
||||
self.etag = etag
|
||||
|
||||
def GroupEntryFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(GroupEntry, xml_string)
|
||||
|
||||
class GroupsFeed(gdata.BatchFeed):
|
||||
"""A Google contact groups feed flavor of an Atom Feed."""
|
||||
_tag = 'feed'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.BatchFeed._children.copy()
|
||||
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [GroupEntry])
|
||||
|
||||
def __init__(self):
|
||||
gdata.BatchFeed.__init__(self)
|
||||
|
||||
def GroupsFeedFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(GroupsFeed, xml_string)
|
||||
355
src/gam/gdata/apps/contacts/service.py
Normal file
355
src/gam/gdata/apps/contacts/service.py
Normal file
@@ -0,0 +1,355 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2009 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""ContactsService extends the GDataService for Google Contacts operations.
|
||||
|
||||
ContactsService: Provides methods to query feeds and manipulate items.
|
||||
Extends GDataService.
|
||||
"""
|
||||
|
||||
|
||||
import gdata.apps
|
||||
import gdata.apps.service
|
||||
import gdata.service
|
||||
|
||||
|
||||
class ContactsService(gdata.service.GDataService):
|
||||
"""Client for the Google Contacts service."""
|
||||
|
||||
def __init__(self, email=None, password=None, source=None,
|
||||
server='www.google.com', additional_headers=None,
|
||||
contact_list='default', contactFeed=True, **kwargs):
|
||||
"""Creates a client for the Contacts service.
|
||||
|
||||
Args:
|
||||
email: string (optional) The user's email address, used for
|
||||
authentication.
|
||||
password: string (optional) The user's password.
|
||||
source: string (optional) The name of the user's application.
|
||||
server: string (optional) The name of the server to which a connection
|
||||
will be opened. Default value: 'www.google.com'.
|
||||
contact_list: string (optional) The name of the default contact list to
|
||||
use when no URI is specified to the methods of the service.
|
||||
Default value: 'default' (the logged in user's contact list).
|
||||
contactFeed: Boolean (optional) Is this contacts feed or a gal feed
|
||||
Default value: True (the logged in user's contact list).
|
||||
**kwargs: The other parameters to pass to gdata.service.GDataService
|
||||
constructor.
|
||||
"""
|
||||
|
||||
self.contact_list = contact_list
|
||||
self.feed_type = ['gal', 'contacts'][contactFeed]
|
||||
if additional_headers == None:
|
||||
additional_headers = {}
|
||||
additional_headers['GData-Version'] = ['1.1', '3.1'][contactFeed]
|
||||
gdata.service.GDataService.__init__(self,
|
||||
email=email, password=password, service='cp', source=source,
|
||||
server=server, additional_headers=additional_headers, **kwargs)
|
||||
self.ssl = True
|
||||
self.port = 443
|
||||
|
||||
def _CleanUri(self, uri):
|
||||
"""Sanitizes a feed URI.
|
||||
|
||||
Args:
|
||||
uri: The URI to sanitize, can be relative or absolute.
|
||||
|
||||
Returns:
|
||||
The given URI without its https://server prefix, if any.
|
||||
Keeps the leading slash of the URI.
|
||||
"""
|
||||
url_prefix = 'https://%s' % self.server
|
||||
if uri.startswith(url_prefix):
|
||||
uri = uri[len(url_prefix):]
|
||||
return uri
|
||||
|
||||
def GetContactFeedUri(self, contact_list=None, projection='full', contactId=None):
|
||||
"""Builds a contact feed URI. """
|
||||
contact_list = contact_list or self.contact_list
|
||||
uri = 'https://{0}/m8/feeds/{1}/{2}/{3}'.format(self.server, self.feed_type, contact_list, projection)
|
||||
if contactId:
|
||||
uri += '/{0}'.format(contactId)
|
||||
return uri
|
||||
|
||||
def GetContactsFeed(self, uri=None,
|
||||
extra_headers=None, url_params=None, escape_params=True):
|
||||
uri = uri or self.GetContactFeedUri()
|
||||
try:
|
||||
return self.Get(uri,
|
||||
url_params=url_params, extra_headers=extra_headers, escape_params=escape_params,
|
||||
converter=gdata.apps.contacts.ContactsFeedFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def GetContact(self, uri):
|
||||
try:
|
||||
return self.Get(uri, converter=gdata.apps.contacts.ContactEntryFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def CreateContact(self, new_contact, insert_uri=None, url_params=None,
|
||||
escape_params=True):
|
||||
"""Adds an new contact to Google Contacts.
|
||||
|
||||
Args:
|
||||
new_contact: atom.Entry or subclass A new contact which is to be added to
|
||||
Google Contacts.
|
||||
insert_uri: the URL to post new contacts to the feed
|
||||
url_params: dict (optional) Additional URL parameters to be included
|
||||
in the insertion request.
|
||||
escape_params: boolean (optional) If true, the url_parameters will be
|
||||
escaped before they are included in the request.
|
||||
|
||||
Returns:
|
||||
On successful insert, an entry containing the contact created
|
||||
On failure, a RequestError is raised of the form:
|
||||
{'status': HTTP status code from server,
|
||||
'reason': HTTP reason from the server,
|
||||
'body': HTTP body of the server's response}
|
||||
"""
|
||||
insert_uri = insert_uri or self.GetContactFeedUri()
|
||||
try:
|
||||
return self.Post(new_contact, insert_uri, url_params=url_params,
|
||||
escape_params=escape_params,
|
||||
converter=gdata.apps.contacts.ContactEntryFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def UpdateContact(self, edit_uri, updated_contact, extra_headers=None, url_params=None,
|
||||
escape_params=True):
|
||||
"""Updates an existing contact.
|
||||
|
||||
Args:
|
||||
edit_uri: string The edit link URI for the element being updated
|
||||
updated_contact: string, atom.Entry or subclass containing
|
||||
the Atom Entry which will replace the contact which is
|
||||
stored at the edit_url
|
||||
url_params: dict (optional) Additional URL parameters to be included
|
||||
in the update request.
|
||||
escape_params: boolean (optional) If true, the url_parameters will be
|
||||
escaped before they are included in the request.
|
||||
|
||||
Returns:
|
||||
On successful update, a httplib.HTTPResponse containing the server's
|
||||
response to the PUT request.
|
||||
On failure, a RequestError is raised of the form:
|
||||
{'status': HTTP status code from server,
|
||||
'reason': HTTP reason from the server,
|
||||
'body': HTTP body of the server's response}
|
||||
"""
|
||||
try:
|
||||
return self.Put(updated_contact, self._CleanUri(edit_uri),
|
||||
url_params=url_params, extra_headers=extra_headers,
|
||||
escape_params=escape_params,
|
||||
converter=gdata.apps.contacts.ContactEntryFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def DeleteContact(self, edit_uri, extra_headers=None,
|
||||
url_params=None, escape_params=True):
|
||||
"""Removes an contact with the specified ID from Google Contacts.
|
||||
|
||||
Args:
|
||||
edit_uri: string The edit URL of the entry to be deleted. Example:
|
||||
'/m8/feeds/contacts/default/full/xxx/yyy'
|
||||
extra_headers: dict (optional)
|
||||
url_params: dict (optional) Additional URL parameters to be included
|
||||
in the deletion request.
|
||||
escape_params: boolean (optional) If true, the url_parameters will be
|
||||
escaped before they are included in the request.
|
||||
|
||||
Returns:
|
||||
On successful delete, a httplib.HTTPResponse containing the server's
|
||||
response to the DELETE request.
|
||||
On failure, a RequestError is raised of the form:
|
||||
{'status': HTTP status code from server,
|
||||
'reason': HTTP reason from the server,
|
||||
'body': HTTP body of the server's response}
|
||||
"""
|
||||
try:
|
||||
return self.Delete(self._CleanUri(edit_uri),
|
||||
url_params=url_params, escape_params=escape_params, extra_headers=extra_headers)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def ChangePhoto(self, media, contact_entry_or_url, content_type=None,
|
||||
content_length=None, extra_headers=None):
|
||||
"""Change the photo for the contact by uploading a new photo.
|
||||
|
||||
Performs a PUT against the photo edit URL to send the binary data for the
|
||||
photo.
|
||||
|
||||
Args:
|
||||
media: filename, file-like-object, or a gdata.MediaSource object to send.
|
||||
contact_entry_or_url: ContactEntry or str If it is a ContactEntry, this
|
||||
method will search for an edit photo link URL and
|
||||
perform a PUT to the URL.
|
||||
content_type: str (optional) the mime type for the photo data. This is
|
||||
necessary if media is a file or file name, but if media
|
||||
is a MediaSource object then the media object can contain
|
||||
the mime type. If media_type is set, it will override the
|
||||
mime type in the media object.
|
||||
content_length: int or str (optional) Specifying the content length is
|
||||
only required if media is a file-like object. If media
|
||||
is a filename, the length is determined using
|
||||
os.path.getsize. If media is a MediaSource object, it is
|
||||
assumed that it already contains the content length.
|
||||
extra_headers: dict (optional)
|
||||
"""
|
||||
if isinstance(contact_entry_or_url, gdata.apps.contacts.ContactEntry):
|
||||
# url = contact_entry_or_url.GetPhotoEditLink().href
|
||||
url = contact_entry_or_url.GetPhotoLink().href
|
||||
else:
|
||||
url = contact_entry_or_url
|
||||
if isinstance(media, gdata.MediaSource):
|
||||
payload = media
|
||||
# If the media object is a file-like object, then use it as the file
|
||||
# handle in the in the MediaSource.
|
||||
elif hasattr(media, 'read'):
|
||||
payload = gdata.MediaSource(file_handle=media,
|
||||
content_type=content_type, content_length=content_length)
|
||||
# Assume that the media object is a file name.
|
||||
else:
|
||||
payload = gdata.MediaSource(content_type=content_type,
|
||||
content_length=content_length, file_path=media)
|
||||
return self.Put(payload, url, extra_headers=extra_headers)
|
||||
|
||||
def GetPhoto(self, contact_entry_or_url):
|
||||
"""Retrives the binary data for the contact's profile photo as a string.
|
||||
|
||||
Args:
|
||||
contact_entry_or_url: a gdata.apps.contacts.ContactEntry object or a string
|
||||
containing the photo link's URL. If the contact entry does not
|
||||
contain a photo link, the image will not be fetched and this method
|
||||
will return None.
|
||||
"""
|
||||
# TODO: add the ability to write out the binary image data to a file,
|
||||
# reading and writing a chunk at a time to avoid potentially using up
|
||||
# large amounts of memory.
|
||||
url = None
|
||||
if isinstance(contact_entry_or_url, gdata.apps.contacts.ContactEntry):
|
||||
photo_link = contact_entry_or_url.GetPhotoLink()
|
||||
if photo_link:
|
||||
url = photo_link.href
|
||||
else:
|
||||
url = contact_entry_or_url
|
||||
if url:
|
||||
try:
|
||||
return self.Get(url, converter=str)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
else:
|
||||
return None
|
||||
|
||||
def DeletePhoto(self, contact_entry_or_url, extra_headers=None):
|
||||
"""Deletes the contact's profile photo.
|
||||
|
||||
Args:
|
||||
contact_entry_or_url: a gdata.apps.contacts.ContactEntry object or a string
|
||||
containing the photo link's URL.
|
||||
will return None.
|
||||
extra_headers: dict (optional)
|
||||
"""
|
||||
url = None
|
||||
if isinstance(contact_entry_or_url, gdata.apps.contacts.ContactEntry):
|
||||
# url = contact_entry_or_url.GetPhotoEditLink().href
|
||||
url = contact_entry_or_url.GetPhotoLink().href
|
||||
else:
|
||||
url = contact_entry_or_url
|
||||
if url:
|
||||
self.Delete(url, extra_headers=extra_headers)
|
||||
|
||||
def ExecuteBatch(self, batch_feed, url,
|
||||
converter=gdata.apps.contacts.ContactsFeedFromString):
|
||||
"""Sends a batch request feed to the server.
|
||||
|
||||
Args:
|
||||
batch_feed: gdata.apps.contacts.ContactFeed A feed containing batch
|
||||
request entries. Each entry contains the operation to be performed
|
||||
on the data contained in the entry. For example an entry with an
|
||||
operation type of insert will be used as if the individual entry
|
||||
had been inserted.
|
||||
url: str The batch URL to which these operations should be applied.
|
||||
converter: Function (optional) The function used to convert the server's
|
||||
response to an object. The default value is ContactsFeedFromString.
|
||||
|
||||
Returns:
|
||||
The results of the batch request's execution on the server. If the
|
||||
default converter is used, this is stored in a ContactsFeed.
|
||||
"""
|
||||
return self.Post(batch_feed, url, converter=converter)
|
||||
|
||||
def GetContactGroupFeedUri(self, contact_list=None, projection='full', groupId=None):
|
||||
"""Builds a contact feed URI. """
|
||||
contact_list = contact_list or self.contact_list
|
||||
uri = 'https://{0}/m8/feeds/groups/{1}/{2}'.format(self.server, contact_list, projection)
|
||||
if groupId:
|
||||
uri += '/{0}'.format(groupId)
|
||||
return uri
|
||||
|
||||
def GetGroupsFeed(self, uri=None,
|
||||
extra_headers=None, url_params=None, escape_params=True):
|
||||
uri = uri or self.GetContactGroupFeedUri()
|
||||
try:
|
||||
return self.Get(uri,
|
||||
url_params=url_params, extra_headers=extra_headers, escape_params=escape_params,
|
||||
converter=gdata.apps.contacts.GroupsFeedFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def GetGroup(self, uri):
|
||||
try:
|
||||
return self.Get(uri, converter=gdata.apps.contacts.GroupEntryFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def CreateGroup(self, new_group, insert_uri=None, url_params=None,
|
||||
escape_params=True):
|
||||
insert_uri = insert_uri or self.GetContactGroupFeedUri()
|
||||
try:
|
||||
return self.Post(new_group, insert_uri, url_params=url_params,
|
||||
escape_params=escape_params,
|
||||
converter=gdata.apps.contacts.GroupEntryFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def UpdateGroup(self, edit_uri, updated_group, extra_headers=None, url_params=None,
|
||||
escape_params=True):
|
||||
try:
|
||||
return self.Put(updated_group, self._CleanUri(edit_uri),
|
||||
url_params=url_params, extra_headers=extra_headers,
|
||||
escape_params=escape_params,
|
||||
converter=gdata.apps.contacts.GroupEntryFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def DeleteGroup(self, edit_uri, extra_headers=None,
|
||||
url_params=None, escape_params=True):
|
||||
try:
|
||||
return self.Delete(self._CleanUri(edit_uri),
|
||||
url_params=url_params, escape_params=escape_params, extra_headers=extra_headers)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
class ContactsQuery(gdata.service.Query):
|
||||
|
||||
def __init__(self, feed=None, text_query=None, params=None,
|
||||
categories=None, group=None):
|
||||
self.feed = feed or '/m8/feeds/contacts/default/full'
|
||||
if group:
|
||||
self['group'] = group
|
||||
gdata.service.Query.__init__(self, feed=self.feed, text_query=text_query,
|
||||
params=params, categories=categories)
|
||||
544
src/gam/gdata/apps/service.py
Normal file
544
src/gam/gdata/apps/service.py
Normal file
@@ -0,0 +1,544 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2007 SIOS Technology, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__author__ = 'tmatsuo@sios.com (Takashi MATSUO)'
|
||||
|
||||
import lxml.etree as ElementTree
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
import gdata
|
||||
import atom.service
|
||||
import gdata.service
|
||||
import gdata.apps
|
||||
import atom
|
||||
|
||||
API_VER="2.0"
|
||||
HTTP_OK=200
|
||||
|
||||
UNKOWN_ERROR=1000
|
||||
USER_DELETED_RECENTLY=1100
|
||||
USER_SUSPENDED=1101
|
||||
DOMAIN_USER_LIMIT_EXCEEDED=1200
|
||||
DOMAIN_ALIAS_LIMIT_EXCEEDED=1201
|
||||
DOMAIN_SUSPENDED=1202
|
||||
DOMAIN_FEATURE_UNAVAILABLE=1203
|
||||
ENTITY_EXISTS=1300
|
||||
ENTITY_DOES_NOT_EXIST=1301
|
||||
ENTITY_NAME_IS_RESERVED=1302
|
||||
ENTITY_NAME_NOT_VALID=1303
|
||||
INVALID_GIVEN_NAME=1400
|
||||
INVALID_FAMILY_NAME=1401
|
||||
INVALID_PASSWORD=1402
|
||||
INVALID_USERNAME=1403
|
||||
INVALID_HASH_FUNCTION_NAME=1404
|
||||
INVALID_HASH_DIGGEST_LENGTH=1405
|
||||
INVALID_EMAIL_ADDRESS=1406
|
||||
INVALID_QUERY_PARAMETER_VALUE=1407
|
||||
TOO_MANY_RECIPIENTS_ON_EMAIL_LIST=1500
|
||||
|
||||
DEFAULT_QUOTA_LIMIT='2048'
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AppsForYourDomainException(Error):
|
||||
|
||||
def __init__(self, response):
|
||||
|
||||
Error.__init__(self, response)
|
||||
try:
|
||||
self.element_tree = ElementTree.fromstring(response['body'])
|
||||
self.error_code = int(self.element_tree[0].attrib['errorCode'])
|
||||
self.reason = self.element_tree[0].attrib['reason']
|
||||
self.invalidInput = self.element_tree[0].attrib['invalidInput']
|
||||
except:
|
||||
self.error_code = 600
|
||||
|
||||
class AppsService(gdata.service.GDataService):
|
||||
"""Client for the Google Apps Provisioning service."""
|
||||
|
||||
def __init__(self, email=None, password=None, domain=None, source=None,
|
||||
server='apps-apis.google.com', additional_headers=None,
|
||||
**kwargs):
|
||||
"""Creates a client for the Google Apps Provisioning service.
|
||||
|
||||
Args:
|
||||
email: string (optional) The user's email address, used for
|
||||
authentication.
|
||||
password: string (optional) The user's password.
|
||||
domain: string (optional) The Google Apps domain name.
|
||||
source: string (optional) The name of the user's application.
|
||||
server: string (optional) The name of the server to which a connection
|
||||
will be opened. Default value: 'apps-apis.google.com'.
|
||||
**kwargs: The other parameters to pass to gdata.service.GDataService
|
||||
constructor.
|
||||
"""
|
||||
gdata.service.GDataService.__init__(
|
||||
self, email=email, password=password, service='apps', source=source,
|
||||
server=server, additional_headers=additional_headers, **kwargs)
|
||||
self.ssl = True
|
||||
self.port = 443
|
||||
self.domain = domain
|
||||
|
||||
def _baseURL(self):
|
||||
return "/a/feeds/%s" % self.domain
|
||||
|
||||
def AddAllElementsFromAllPages(self, link_finder, func):
|
||||
"""retrieve all pages and add all elements"""
|
||||
next = link_finder.GetNextLink()
|
||||
while next is not None:
|
||||
next_feed = self.Get(next.href, converter=func)
|
||||
for a_entry in next_feed.entry:
|
||||
link_finder.entry.append(a_entry)
|
||||
next = next_feed.GetNextLink()
|
||||
return link_finder
|
||||
|
||||
def RetrievePageOfEmailLists(self, start_email_list_name=None,
|
||||
num_retries=gdata.service.DEFAULT_NUM_RETRIES,
|
||||
delay=gdata.service.DEFAULT_DELAY,
|
||||
backoff=gdata.service.DEFAULT_BACKOFF):
|
||||
"""Retrieve one page of email list"""
|
||||
uri = "%s/emailList/%s" % (self._baseURL(), API_VER)
|
||||
if start_email_list_name is not None:
|
||||
uri += "?startEmailListName=%s" % start_email_list_name
|
||||
try:
|
||||
return gdata.apps.EmailListFeedFromString(str(self.GetWithRetries(
|
||||
uri, num_retries=num_retries, delay=delay, backoff=backoff)))
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
def GetGeneratorForAllEmailLists(
|
||||
self, num_retries=gdata.service.DEFAULT_NUM_RETRIES,
|
||||
delay=gdata.service.DEFAULT_DELAY, backoff=gdata.service.DEFAULT_BACKOFF):
|
||||
"""Retrieve a generator for all emaillists in this domain."""
|
||||
first_page = self.RetrievePageOfEmailLists(num_retries=num_retries,
|
||||
delay=delay,
|
||||
backoff=backoff)
|
||||
return self.GetGeneratorFromLinkFinder(
|
||||
first_page, gdata.apps.EmailListRecipientFeedFromString,
|
||||
num_retries=num_retries, delay=delay, backoff=backoff)
|
||||
|
||||
def RetrieveAllEmailLists(self):
|
||||
"""Retrieve all email list of a domain."""
|
||||
|
||||
ret = self.RetrievePageOfEmailLists()
|
||||
# pagination
|
||||
return self.AddAllElementsFromAllPages(
|
||||
ret, gdata.apps.EmailListFeedFromString)
|
||||
|
||||
def RetrieveEmailList(self, list_name):
|
||||
"""Retreive a single email list by the list's name."""
|
||||
|
||||
uri = "%s/emailList/%s/%s" % (
|
||||
self._baseURL(), API_VER, list_name)
|
||||
try:
|
||||
return self.Get(uri, converter=gdata.apps.EmailListEntryFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
def RetrieveEmailLists(self, recipient):
|
||||
"""Retrieve All Email List Subscriptions for an Email Address."""
|
||||
|
||||
uri = "%s/emailList/%s?recipient=%s" % (
|
||||
self._baseURL(), API_VER, recipient)
|
||||
try:
|
||||
ret = gdata.apps.EmailListFeedFromString(str(self.Get(uri)))
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
# pagination
|
||||
return self.AddAllElementsFromAllPages(
|
||||
ret, gdata.apps.EmailListFeedFromString)
|
||||
|
||||
def RemoveRecipientFromEmailList(self, recipient, list_name):
|
||||
"""Remove recipient from email list."""
|
||||
|
||||
uri = "%s/emailList/%s/%s/recipient/%s" % (
|
||||
self._baseURL(), API_VER, list_name, recipient)
|
||||
try:
|
||||
self.Delete(uri)
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
def RetrievePageOfRecipients(self, list_name, start_recipient=None,
|
||||
num_retries=gdata.service.DEFAULT_NUM_RETRIES,
|
||||
delay=gdata.service.DEFAULT_DELAY,
|
||||
backoff=gdata.service.DEFAULT_BACKOFF):
|
||||
"""Retrieve one page of recipient of an email list. """
|
||||
|
||||
uri = "%s/emailList/%s/%s/recipient" % (
|
||||
self._baseURL(), API_VER, list_name)
|
||||
|
||||
if start_recipient is not None:
|
||||
uri += "?startRecipient=%s" % start_recipient
|
||||
try:
|
||||
return gdata.apps.EmailListRecipientFeedFromString(str(
|
||||
self.GetWithRetries(
|
||||
uri, num_retries=num_retries, delay=delay, backoff=backoff)))
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
def GetGeneratorForAllRecipients(
|
||||
self, list_name, num_retries=gdata.service.DEFAULT_NUM_RETRIES,
|
||||
delay=gdata.service.DEFAULT_DELAY, backoff=gdata.service.DEFAULT_BACKOFF):
|
||||
"""Retrieve a generator for all recipients of a particular emaillist."""
|
||||
first_page = self.RetrievePageOfRecipients(list_name,
|
||||
num_retries=num_retries,
|
||||
delay=delay,
|
||||
backoff=backoff)
|
||||
return self.GetGeneratorFromLinkFinder(
|
||||
first_page, gdata.apps.EmailListRecipientFeedFromString,
|
||||
num_retries=num_retries, delay=delay, backoff=backoff)
|
||||
|
||||
def RetrieveAllRecipients(self, list_name):
|
||||
"""Retrieve all recipient of an email list."""
|
||||
|
||||
ret = self.RetrievePageOfRecipients(list_name)
|
||||
# pagination
|
||||
return self.AddAllElementsFromAllPages(
|
||||
ret, gdata.apps.EmailListRecipientFeedFromString)
|
||||
|
||||
def AddRecipientToEmailList(self, recipient, list_name):
|
||||
"""Add a recipient to a email list."""
|
||||
|
||||
uri = "%s/emailList/%s/%s/recipient" % (
|
||||
self._baseURL(), API_VER, list_name)
|
||||
recipient_entry = gdata.apps.EmailListRecipientEntry()
|
||||
recipient_entry.who = gdata.apps.Who(email=recipient)
|
||||
|
||||
try:
|
||||
return gdata.apps.EmailListRecipientEntryFromString(
|
||||
str(self.Post(recipient_entry, uri)))
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
def DeleteEmailList(self, list_name):
|
||||
"""Delete a email list"""
|
||||
|
||||
uri = "%s/emailList/%s/%s" % (self._baseURL(), API_VER, list_name)
|
||||
try:
|
||||
self.Delete(uri)
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
def CreateEmailList(self, list_name):
|
||||
"""Create a email list. """
|
||||
|
||||
uri = "%s/emailList/%s" % (self._baseURL(), API_VER)
|
||||
email_list_entry = gdata.apps.EmailListEntry()
|
||||
email_list_entry.email_list = gdata.apps.EmailList(name=list_name)
|
||||
try:
|
||||
return gdata.apps.EmailListEntryFromString(
|
||||
str(self.Post(email_list_entry, uri)))
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
def DeleteNickname(self, nickname):
|
||||
"""Delete a nickname"""
|
||||
|
||||
uri = "%s/nickname/%s/%s" % (self._baseURL(), API_VER, nickname)
|
||||
try:
|
||||
self.Delete(uri)
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
def RetrievePageOfNicknames(self, start_nickname=None,
|
||||
num_retries=gdata.service.DEFAULT_NUM_RETRIES,
|
||||
delay=gdata.service.DEFAULT_DELAY,
|
||||
backoff=gdata.service.DEFAULT_BACKOFF):
|
||||
"""Retrieve one page of nicknames in the domain"""
|
||||
|
||||
uri = "%s/nickname/%s" % (self._baseURL(), API_VER)
|
||||
if start_nickname is not None:
|
||||
uri += "?startNickname=%s" % start_nickname
|
||||
try:
|
||||
return gdata.apps.NicknameFeedFromString(str(self.GetWithRetries(
|
||||
uri, num_retries=num_retries, delay=delay, backoff=backoff)))
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
def GetGeneratorForAllNicknames(
|
||||
self, num_retries=gdata.service.DEFAULT_NUM_RETRIES,
|
||||
delay=gdata.service.DEFAULT_DELAY, backoff=gdata.service.DEFAULT_BACKOFF):
|
||||
"""Retrieve a generator for all nicknames in this domain."""
|
||||
first_page = self.RetrievePageOfNicknames(num_retries=num_retries,
|
||||
delay=delay,
|
||||
backoff=backoff)
|
||||
return self.GetGeneratorFromLinkFinder(
|
||||
first_page, gdata.apps.NicknameFeedFromString, num_retries=num_retries,
|
||||
delay=delay, backoff=backoff)
|
||||
|
||||
def RetrieveAllNicknames(self):
|
||||
"""Retrieve all nicknames in the domain"""
|
||||
|
||||
ret = self.RetrievePageOfNicknames()
|
||||
# pagination
|
||||
return self.AddAllElementsFromAllPages(
|
||||
ret, gdata.apps.NicknameFeedFromString)
|
||||
|
||||
def GetGeneratorForAllNicknamesOfAUser(
|
||||
self, user_name, num_retries=gdata.service.DEFAULT_NUM_RETRIES,
|
||||
delay=gdata.service.DEFAULT_DELAY, backoff=gdata.service.DEFAULT_BACKOFF):
|
||||
"""Retrieve a generator for all nicknames of a particular user."""
|
||||
uri = "%s/nickname/%s?username=%s" % (self._baseURL(), API_VER, user_name)
|
||||
try:
|
||||
first_page = gdata.apps.NicknameFeedFromString(str(self.GetWithRetries(
|
||||
uri, num_retries=num_retries, delay=delay, backoff=backoff)))
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
return self.GetGeneratorFromLinkFinder(
|
||||
first_page, gdata.apps.NicknameFeedFromString, num_retries=num_retries,
|
||||
delay=delay, backoff=backoff)
|
||||
|
||||
def RetrieveNicknames(self, user_name):
|
||||
"""Retrieve nicknames of the user"""
|
||||
|
||||
uri = "%s/nickname/%s?username=%s" % (self._baseURL(), API_VER, user_name)
|
||||
try:
|
||||
ret = gdata.apps.NicknameFeedFromString(str(self.Get(uri)))
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
# pagination
|
||||
return self.AddAllElementsFromAllPages(
|
||||
ret, gdata.apps.NicknameFeedFromString)
|
||||
|
||||
def RetrieveNickname(self, nickname):
|
||||
"""Retrieve a nickname.
|
||||
|
||||
Args:
|
||||
nickname: string The nickname to retrieve
|
||||
|
||||
Returns:
|
||||
gdata.apps.NicknameEntry
|
||||
"""
|
||||
|
||||
uri = "%s/nickname/%s/%s" % (self._baseURL(), API_VER, nickname)
|
||||
try:
|
||||
return gdata.apps.NicknameEntryFromString(str(self.Get(uri)))
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
def CreateNickname(self, user_name, nickname):
|
||||
"""Create a nickname"""
|
||||
|
||||
uri = "%s/nickname/%s" % (self._baseURL(), API_VER)
|
||||
nickname_entry = gdata.apps.NicknameEntry()
|
||||
nickname_entry.login = gdata.apps.Login(user_name=user_name)
|
||||
nickname_entry.nickname = gdata.apps.Nickname(name=nickname)
|
||||
|
||||
try:
|
||||
return gdata.apps.NicknameEntryFromString(
|
||||
str(self.Post(nickname_entry, uri)))
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
def DeleteUser(self, user_name):
|
||||
"""Delete a user account"""
|
||||
|
||||
uri = "%s/user/%s/%s" % (self._baseURL(), API_VER, user_name)
|
||||
try:
|
||||
return self.Delete(uri)
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
def UpdateUser(self, user_name, user_entry):
|
||||
"""Update a user account."""
|
||||
|
||||
uri = "%s/user/%s/%s" % (self._baseURL(), API_VER, user_name)
|
||||
try:
|
||||
return gdata.apps.UserEntryFromString(str(self.Put(user_entry, uri)))
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
def CreateUser(self, user_name, family_name, given_name, password,
|
||||
suspended='false', quota_limit=None,
|
||||
password_hash_function=None,
|
||||
change_password=None):
|
||||
"""Create a user account. """
|
||||
|
||||
uri = "%s/user/%s" % (self._baseURL(), API_VER)
|
||||
user_entry = gdata.apps.UserEntry()
|
||||
user_entry.login = gdata.apps.Login(
|
||||
user_name=user_name, password=password, suspended=suspended,
|
||||
hash_function_name=password_hash_function,
|
||||
change_password=change_password)
|
||||
user_entry.name = gdata.apps.Name(family_name=family_name,
|
||||
given_name=given_name)
|
||||
if quota_limit is not None:
|
||||
user_entry.quota = gdata.apps.Quota(limit=str(quota_limit))
|
||||
|
||||
try:
|
||||
return gdata.apps.UserEntryFromString(str(self.Post(user_entry, uri)))
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
def SuspendUser(self, user_name):
|
||||
user_entry = self.RetrieveUser(user_name)
|
||||
if user_entry.login.suspended != 'true':
|
||||
user_entry.login.suspended = 'true'
|
||||
user_entry = self.UpdateUser(user_name, user_entry)
|
||||
return user_entry
|
||||
|
||||
def RestoreUser(self, user_name):
|
||||
user_entry = self.RetrieveUser(user_name)
|
||||
if user_entry.login.suspended != 'false':
|
||||
user_entry.login.suspended = 'false'
|
||||
user_entry = self.UpdateUser(user_name, user_entry)
|
||||
return user_entry
|
||||
|
||||
def RetrieveUser(self, user_name):
|
||||
"""Retrieve an user account.
|
||||
|
||||
Args:
|
||||
user_name: string The user name to retrieve
|
||||
|
||||
Returns:
|
||||
gdata.apps.UserEntry
|
||||
"""
|
||||
|
||||
uri = "%s/user/%s/%s" % (self._baseURL(), API_VER, user_name)
|
||||
try:
|
||||
return gdata.apps.UserEntryFromString(str(self.Get(uri)))
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
def RetrievePageOfUsers(self, start_username=None,
|
||||
num_retries=gdata.service.DEFAULT_NUM_RETRIES,
|
||||
delay=gdata.service.DEFAULT_DELAY,
|
||||
backoff=gdata.service.DEFAULT_BACKOFF):
|
||||
"""Retrieve one page of users in this domain."""
|
||||
|
||||
uri = "%s/user/%s" % (self._baseURL(), API_VER)
|
||||
if start_username is not None:
|
||||
uri += "?startUsername=%s" % start_username
|
||||
try:
|
||||
return gdata.apps.UserFeedFromString(str(self.GetWithRetries(
|
||||
uri, num_retries=num_retries, delay=delay, backoff=backoff)))
|
||||
except gdata.service.RequestError as e:
|
||||
raise AppsForYourDomainException(e.args[0])
|
||||
|
||||
def GetGeneratorForAllUsers(self,
|
||||
num_retries=gdata.service.DEFAULT_NUM_RETRIES,
|
||||
delay=gdata.service.DEFAULT_DELAY,
|
||||
backoff=gdata.service.DEFAULT_BACKOFF):
|
||||
"""Retrieve a generator for all users in this domain."""
|
||||
first_page = self.RetrievePageOfUsers(num_retries=num_retries, delay=delay,
|
||||
backoff=backoff)
|
||||
return self.GetGeneratorFromLinkFinder(
|
||||
first_page, gdata.apps.UserFeedFromString, num_retries=num_retries,
|
||||
delay=delay, backoff=backoff)
|
||||
|
||||
def RetrieveAllUsers(self):
|
||||
"""Retrieve all users in this domain. OBSOLETE"""
|
||||
|
||||
ret = self.RetrievePageOfUsers()
|
||||
# pagination
|
||||
return self.AddAllElementsFromAllPages(
|
||||
ret, gdata.apps.UserFeedFromString)
|
||||
|
||||
|
||||
class PropertyService(gdata.service.GDataService):
|
||||
"""Client for the Google Apps Property service."""
|
||||
|
||||
def __init__(self, email=None, password=None, domain=None, source=None,
|
||||
server='apps-apis.google.com', additional_headers=None):
|
||||
gdata.service.GDataService.__init__(self, email=email, password=password,
|
||||
service='apps', source=source,
|
||||
server=server,
|
||||
additional_headers=additional_headers)
|
||||
self.ssl = True
|
||||
self.port = 443
|
||||
self.domain = domain
|
||||
|
||||
def AddAllElementsFromAllPages(self, link_finder, func):
|
||||
"""retrieve all pages and add all elements"""
|
||||
next = link_finder.GetNextLink()
|
||||
count = 0
|
||||
while next is not None:
|
||||
next_feed = self.Get(next.href, converter=func)
|
||||
count = count + len(next_feed.entry)
|
||||
for a_entry in next_feed.entry:
|
||||
link_finder.entry.append(a_entry)
|
||||
next = next_feed.GetNextLink()
|
||||
return link_finder
|
||||
|
||||
def _GetPropertyEntry(self, properties):
|
||||
property_entry = gdata.apps.PropertyEntry()
|
||||
property = []
|
||||
for name, value in properties.items():
|
||||
if name is not None and value is not None:
|
||||
property.append(gdata.apps.Property(name=name, value=value))
|
||||
property_entry.property = property
|
||||
return property_entry
|
||||
|
||||
def _PropertyEntry2Dict(self, property_entry):
|
||||
properties = {}
|
||||
for i, property in enumerate(property_entry.property):
|
||||
properties[property.name] = property.value
|
||||
return properties
|
||||
|
||||
def _GetPropertyFeed(self, uri):
|
||||
try:
|
||||
return gdata.apps.PropertyFeedFromString(str(self.Get(uri)))
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def _GetPropertiesList(self, uri):
|
||||
property_feed = self._GetPropertyFeed(uri)
|
||||
# pagination
|
||||
property_feed = self.AddAllElementsFromAllPages(
|
||||
property_feed, gdata.apps.PropertyFeedFromString)
|
||||
properties_list = []
|
||||
for property_entry in property_feed.entry:
|
||||
properties_list.append(self._PropertyEntry2Dict(property_entry))
|
||||
return properties_list
|
||||
|
||||
def _GetProperties(self, uri):
|
||||
try:
|
||||
return self._PropertyEntry2Dict(gdata.apps.PropertyEntryFromString(
|
||||
str(self.Get(uri))))
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def _PostProperties(self, uri, properties):
|
||||
property_entry = self._GetPropertyEntry(properties)
|
||||
try:
|
||||
return self._PropertyEntry2Dict(gdata.apps.PropertyEntryFromString(
|
||||
str(self.Post(property_entry, uri))))
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def _PutProperties(self, uri, properties):
|
||||
property_entry = self._GetPropertyEntry(properties)
|
||||
try:
|
||||
return self._PropertyEntry2Dict(gdata.apps.PropertyEntryFromString(
|
||||
str(self.Put(property_entry, uri))))
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
def _DeleteProperties(self, uri):
|
||||
try:
|
||||
self.Delete(uri)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
|
||||
def _bool2str(b):
|
||||
if b is None:
|
||||
return None
|
||||
return str(b is True).lower()
|
||||
283
src/gam/gdata/apps/sites/__init__.py
Normal file
283
src/gam/gdata/apps/sites/__init__.py
Normal file
@@ -0,0 +1,283 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2009 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Data model classes for parsing and generating XML for the Sites Data API."""
|
||||
|
||||
import atom
|
||||
import gdata
|
||||
|
||||
# XML Namespaces used in Google Sites entities.
|
||||
SITES_NAMESPACE = 'http://schemas.google.com/sites/2008'
|
||||
SITES_TEMPLATE = '{http://schemas.google.com/sites/2008}%s'
|
||||
SPREADSHEETS_NAMESPACE = 'http://schemas.google.com/spreadsheets/2006'
|
||||
SPREADSHEETS_TEMPLATE = '{http://schemas.google.com/spreadsheets/2006}%s'
|
||||
GACL_NAMESPACE = 'http://schemas.google.com/acl/2007'
|
||||
GACL_TEMPLATE = '{http://schemas.google.com/acl/2007}%s'
|
||||
DC_TERMS_TEMPLATE = '{http://purl.org/dc/terms}%s'
|
||||
THR_TERMS_TEMPLATE = '{http://purl.org/syndication/thread/1.0}%s'
|
||||
XHTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'
|
||||
XHTML_TEMPLATE = '{http://www.w3.org/1999/xhtml}%s'
|
||||
|
||||
SITES_INVITE_LINK_REL = SITES_NAMESPACE + '#invite'
|
||||
SITES_PARENT_LINK_REL = SITES_NAMESPACE + '#parent'
|
||||
SITES_REVISION_LINK_REL = SITES_NAMESPACE + '#revision'
|
||||
SITES_SOURCE_LINK_REL = SITES_NAMESPACE + '#source'
|
||||
SITES_TEMPLATE_LINK_REL = SITES_NAMESPACE + '#template'
|
||||
|
||||
ALTERNATE_REL = 'alternate'
|
||||
WEB_ADDRESS_MAPPING_REL = 'webAddressMapping'
|
||||
|
||||
SITES_KIND_SCHEME = 'http://schemas.google.com/g/2005#kind'
|
||||
ANNOUNCEMENT_KIND_TERM = SITES_NAMESPACE + '#announcement'
|
||||
ANNOUNCEMENT_PAGE_KIND_TERM = SITES_NAMESPACE + '#announcementspage'
|
||||
ATTACHMENT_KIND_TERM = SITES_NAMESPACE + '#attachment'
|
||||
COMMENT_KIND_TERM = SITES_NAMESPACE + '#comment'
|
||||
FILECABINET_KIND_TERM = SITES_NAMESPACE + '#filecabinet'
|
||||
LISTITEM_KIND_TERM = SITES_NAMESPACE + '#listitem'
|
||||
LISTPAGE_KIND_TERM = SITES_NAMESPACE + '#listpage'
|
||||
WEBPAGE_KIND_TERM = SITES_NAMESPACE + '#webpage'
|
||||
WEBATTACHMENT_KIND_TERM = SITES_NAMESPACE + '#webattachment'
|
||||
FOLDER_KIND_TERM = SITES_NAMESPACE + '#folder'
|
||||
TAG_KIND_TERM = SITES_NAMESPACE + '#tag'
|
||||
|
||||
SUPPORT_KINDS = [
|
||||
'announcement', 'announcementspage', 'attachment', 'comment', 'filecabinet',
|
||||
'listitem', 'listpage', 'webpage', 'webattachment', 'tag'
|
||||
]
|
||||
|
||||
|
||||
class GDataBase(atom.AtomBase):
|
||||
"""The Google Sites intermediate class from atom.AtomBase."""
|
||||
_namespace = gdata.GDATA_NAMESPACE
|
||||
_children = atom.AtomBase._children.copy()
|
||||
_attributes = atom.AtomBase._attributes.copy()
|
||||
|
||||
def __init__(self, text=None):
|
||||
atom.AtomBase.__init__(self, text=text)
|
||||
|
||||
class SitesBase(GDataBase):
|
||||
_namespace = SITES_NAMESPACE
|
||||
|
||||
class SiteName(SitesBase):
|
||||
"""Google Sites <sites:siteName>."""
|
||||
_tag = 'siteName'
|
||||
|
||||
class Theme(SitesBase):
|
||||
"""Google Sites <sites:theme>."""
|
||||
_tag = 'theme'
|
||||
|
||||
class SiteEntry(gdata.BatchEntry):
|
||||
"""Google Sites Site Feed Entry."""
|
||||
_tag = 'entry'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.BatchEntry._children.copy()
|
||||
|
||||
_children['{%s}siteName' % SITES_NAMESPACE] = ('siteName', SiteName)
|
||||
_children['{%s}theme' % SITES_NAMESPACE] = ('theme', Theme)
|
||||
_attributes = gdata.BatchEntry._attributes.copy()
|
||||
_attributes['{%s}etag' % gdata.GDATA_NAMESPACE] = 'etag'
|
||||
|
||||
def __init__(self, siteName=None, title=None, summary=None, theme=None, sourceSite=None, category=None, etag=None):
|
||||
gdata.BatchEntry.__init__(self, category=category)
|
||||
self.siteName = siteName
|
||||
self.title = title
|
||||
self.summary = summary
|
||||
self.theme = theme
|
||||
if sourceSite is not None:
|
||||
sourceLink = atom.Link(href=sourceSite, rel=SITES_SOURCE_LINK_REL, link_type='application/atom+xml')
|
||||
self.link.append(sourceLink)
|
||||
self.etag = etag
|
||||
|
||||
def find_alternate_link(self):
|
||||
for link in self.link:
|
||||
if link.rel == ALTERNATE_REL and link.href:
|
||||
return link.href
|
||||
return None
|
||||
|
||||
FindAlternateLink = find_alternate_link
|
||||
|
||||
def find_source_link(self):
|
||||
for link in self.link:
|
||||
if link.rel == SITES_SOURCE_LINK_REL and link.href:
|
||||
return link.href
|
||||
return None
|
||||
|
||||
FindSourceLink = find_source_link
|
||||
|
||||
def find_webaddress_mappings(self):
|
||||
mappingLinks = []
|
||||
for link in self.link:
|
||||
if link.rel == WEB_ADDRESS_MAPPING_REL and link.href:
|
||||
mappingLinks.append(link.href)
|
||||
return mappingLinks
|
||||
|
||||
FindWebAddressMappings = find_webaddress_mappings
|
||||
|
||||
def SiteEntryFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(SiteEntry, xml_string)
|
||||
|
||||
class SiteFeed(gdata.BatchFeed, gdata.LinkFinder):
|
||||
"""A Google Sites feed flavor of an Atom Feed."""
|
||||
_tag = 'feed'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.BatchFeed._children.copy()
|
||||
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [SiteEntry])
|
||||
|
||||
def __init__(self):
|
||||
gdata.BatchFeed.__init__(self)
|
||||
|
||||
def SiteFeedFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(SiteFeed, xml_string)
|
||||
|
||||
class AclBase(GDataBase):
|
||||
_namespace = GACL_NAMESPACE
|
||||
|
||||
class AclRole(AclBase):
|
||||
"""Describes the role of an entry in an access control list."""
|
||||
_tag = 'role'
|
||||
_children = AclBase._children.copy()
|
||||
_attributes = AclBase._attributes.copy()
|
||||
_attributes['value'] = 'value'
|
||||
|
||||
def __init__(self, value=None):
|
||||
AclBase.__init__(self)
|
||||
self.value = value
|
||||
|
||||
class AclAdditionalRole(AclBase):
|
||||
"""Describes an additionalRole element."""
|
||||
_tag = 'additionalRole'
|
||||
_children = AclBase._children.copy()
|
||||
_attributes = AclBase._attributes.copy()
|
||||
_attributes['value'] = 'value'
|
||||
|
||||
def __init__(self, value=None):
|
||||
AclBase.__init__(self)
|
||||
self.value = value
|
||||
|
||||
class AclScope(AclBase):
|
||||
"""Describes the scope of an entry in an access control list."""
|
||||
_tag = 'scope'
|
||||
_children = AclBase._children.copy()
|
||||
_attributes = AclBase._attributes.copy()
|
||||
_attributes['type'] = 'type'
|
||||
_attributes['value'] = 'value'
|
||||
|
||||
def __init__(self, stype=None, value=None):
|
||||
AclBase.__init__(self)
|
||||
self.type = stype
|
||||
self.value = value
|
||||
|
||||
|
||||
class AclWithKey(AclBase):
|
||||
"""Describes a key that can be used to access a document."""
|
||||
_tag = 'withKey'
|
||||
_children = AclBase._children.copy()
|
||||
_children['{%s}role' % GACL_NAMESPACE] = ('role', AclRole)
|
||||
_children['{%s}additionalRole' % GACL_NAMESPACE] = ('additionalRole', AclAdditionalRole)
|
||||
_attributes = AclBase._attributes.copy()
|
||||
_attributes['key'] = 'key'
|
||||
|
||||
def __init__(self, key=None, role=None, additionalRole=None):
|
||||
AclBase.__init__(self)
|
||||
self.key = key
|
||||
self.role = role
|
||||
self.additionalRole = additionalRole
|
||||
|
||||
class AclEntry(gdata.BatchEntry):
|
||||
"""Describes an entry in a feed of an access control list (ACL)."""
|
||||
_tag = 'entry'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.BatchEntry._children.copy()
|
||||
|
||||
_children['{%s}role' % GACL_NAMESPACE] = ('role', AclRole)
|
||||
_children['{%s}additionalRole' % GACL_NAMESPACE] = ('additionalRole', AclAdditionalRole)
|
||||
_children['{%s}scope' % GACL_NAMESPACE] = ('scope', AclScope)
|
||||
_children['{%s}withKey' % GACL_NAMESPACE] = ('withKey', AclWithKey)
|
||||
_attributes = gdata.BatchEntry._attributes.copy()
|
||||
_attributes['{%s}etag' % gdata.GDATA_NAMESPACE] = 'etag'
|
||||
|
||||
def __init__(self, role=None, additionalRole=None, scope=None, withKey=None, etag=None):
|
||||
gdata.BatchEntry.__init__(self)
|
||||
self.role = role
|
||||
self.additionalRole = additionalRole
|
||||
self.scope = scope
|
||||
self.withKey = withKey
|
||||
self.etag = etag
|
||||
|
||||
def find_invite_link(self):
|
||||
for link in self.link:
|
||||
if link.rel == SITES_INVITE_LINK_REL and link.href:
|
||||
return link.href
|
||||
return None
|
||||
|
||||
FindInviteLink = find_invite_link
|
||||
|
||||
def AclEntryFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(AclEntry, xml_string)
|
||||
|
||||
class AclFeed(gdata.BatchFeed, gdata.LinkFinder):
|
||||
"""Describes a feed of an access control list (ACL)."""
|
||||
_tag = 'feed'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.BatchFeed._children.copy()
|
||||
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [AclEntry])
|
||||
|
||||
def __init__(self):
|
||||
gdata.BatchFeed.__init__(self)
|
||||
|
||||
def AclFeedFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(AclFeed, xml_string)
|
||||
|
||||
class ActivityEntry(gdata.BatchEntry):
|
||||
"""Describes an entry in a feed of site activity (changes)."""
|
||||
_tag = 'entry'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.BatchEntry._children.copy()
|
||||
_attributes = gdata.BatchEntry._attributes.copy()
|
||||
|
||||
def __init__(self):
|
||||
gdata.BatchEntry.__init__(self)
|
||||
|
||||
def __find_category_scheme(self, scheme):
|
||||
for category in self.category:
|
||||
if category.scheme == scheme:
|
||||
return category
|
||||
return None
|
||||
|
||||
def kind(self):
|
||||
kind = self.__find_category_scheme(SITES_KIND_SCHEME)
|
||||
if kind is not None:
|
||||
return kind.term[len(SITES_NAMESPACE) + 1:]
|
||||
else:
|
||||
return None
|
||||
|
||||
Kind = kind
|
||||
|
||||
def ActivityEntryFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(ActivityEntry, xml_string)
|
||||
|
||||
class ActivityFeed(gdata.BatchFeed, gdata.LinkFinder):
|
||||
"""Describes a feed of site activity (changes)."""
|
||||
_tag = 'feed'
|
||||
_namespace = atom.ATOM_NAMESPACE
|
||||
_children = gdata.BatchFeed._children.copy()
|
||||
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [ActivityEntry])
|
||||
|
||||
def __init__(self):
|
||||
gdata.BatchFeed.__init__(self)
|
||||
|
||||
def ActivityFeedFromString(xml_string):
|
||||
return atom.CreateClassFromXMLString(ActivityFeed, xml_string)
|
||||
246
src/gam/gdata/apps/sites/service.py
Normal file
246
src/gam/gdata/apps/sites/service.py
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright 2009 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""SitesService extends the GDataService for Google Sites API calls."""
|
||||
|
||||
import gdata.apps
|
||||
import gdata.apps.service
|
||||
import gdata.service
|
||||
|
||||
|
||||
# Feed URI templates
|
||||
CONTENT_FEED_TEMPLATE = '/feeds/content/%s/%s/'
|
||||
REVISION_FEED_TEMPLATE = '/feeds/revision/%s/%s/'
|
||||
ACTIVITY_FEED_TEMPLATE = '/feeds/activity/%s/%s/'
|
||||
ACTIVITY_ENTRY_TEMPLATE = '/feeds/activity/%s/%s/%s'
|
||||
SITE_FEED_TEMPLATE = '/feeds/site/%s/'
|
||||
ACL_FEED_TEMPLATE = '/feeds/acl/site/%s/%s'
|
||||
ACL_ENTRY_TEMPLATE = '/feeds/acl/site/%s/%s/%s'
|
||||
|
||||
|
||||
class SitesService(gdata.service.GDataService):
|
||||
"""Client extension for the Google Sites API service."""
|
||||
|
||||
def __init__(self,
|
||||
source=None, server='sites.google.com', additional_headers=None,
|
||||
**kwargs):
|
||||
"""Constructs a new client for the Sites API.
|
||||
|
||||
Args:
|
||||
site: string (optional) Name (webspace) of the Google Site
|
||||
domain: string (optional) Domain of the (Google Apps hosted) Site.
|
||||
If no domain is given, the Site is assumed to be a consumer Google
|
||||
Site, in which case the value 'site' is used.
|
||||
source: string (optional) The name of the user's application.
|
||||
server: string (optional) The name of the server to which a connection
|
||||
will be opened. Default value: 'sites..google.com'.
|
||||
**kwargs: The other parameters to pass to gdata.service.GDataService
|
||||
constructor.
|
||||
"""
|
||||
if additional_headers == None:
|
||||
additional_headers = {}
|
||||
additional_headers['GData-Version'] = '1.4'
|
||||
gdata.service.GDataService.__init__(self,
|
||||
source=source, server=server, additional_headers=additional_headers,
|
||||
**kwargs)
|
||||
self.ssl = True
|
||||
self.port = 443
|
||||
|
||||
def make_site_feed_uri(self, domain=None, site=None):
|
||||
if not domain:
|
||||
domain = 'site'
|
||||
if not site:
|
||||
return SITE_FEED_TEMPLATE % domain
|
||||
return (SITE_FEED_TEMPLATE % domain) + site
|
||||
|
||||
MakeSiteFeedUri = make_site_feed_uri
|
||||
|
||||
def get_site_feed(self, uri=None, domain=None, site=None,
|
||||
extra_headers=None, url_params=None, escape_params=True):
|
||||
|
||||
uri = uri or self.make_site_feed_uri(domain=domain, site=site)
|
||||
try:
|
||||
return self.Get(uri,
|
||||
url_params=url_params, extra_headers=extra_headers, escape_params=escape_params,
|
||||
converter=gdata.apps.sites.SiteFeedFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
GetSiteFeed = get_site_feed
|
||||
|
||||
def create_site(self, siteentry=None, uri=None, domain=None, site=None,
|
||||
extra_headers=None, url_params=None, escape_params=True):
|
||||
|
||||
if uri is None:
|
||||
uri = self.make_site_feed_uri(domain=domain, site=site)
|
||||
try:
|
||||
return self.Post(siteentry, uri,
|
||||
url_params=url_params, extra_headers=extra_headers, escape_params=escape_params,
|
||||
converter=gdata.apps.sites.SiteEntryFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
CreateSite = create_site
|
||||
|
||||
def get_site(self, uri=None, domain=None, site=None,
|
||||
extra_headers=None, url_params=None, escape_params=True):
|
||||
|
||||
uri = uri or self.make_site_feed_uri(domain=domain, site=site)
|
||||
try:
|
||||
return self.Get(uri,
|
||||
url_params=url_params, extra_headers=extra_headers, escape_params=escape_params,
|
||||
converter=gdata.apps.sites.SiteEntryFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
GetSite = get_site
|
||||
|
||||
def update_site(self, siteentry=None, uri=None, domain=None, site=None,
|
||||
extra_headers=None, url_params=None, escape_params=True):
|
||||
|
||||
uri = uri or self.make_site_feed_uri(domain=domain, site=site)
|
||||
try:
|
||||
return self.Put(siteentry, uri,
|
||||
url_params=url_params, extra_headers=extra_headers, escape_params=escape_params,
|
||||
converter=gdata.apps.sites.SiteEntryFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
UpdateSite = update_site
|
||||
|
||||
def make_acl_feed_uri(self, domain=None, site=None):
|
||||
return ACL_FEED_TEMPLATE % (domain, site)
|
||||
|
||||
MakeAclFeedUri = make_acl_feed_uri
|
||||
|
||||
def get_acl_feed(self, uri=None, domain=None, site=None,
|
||||
extra_headers=None, url_params=None, escape_params=True):
|
||||
|
||||
uri = uri or self.make_acl_feed_uri(domain=domain, site=site)
|
||||
try:
|
||||
return self.Get(uri,
|
||||
url_params=url_params, extra_headers=extra_headers, escape_params=escape_params,
|
||||
converter=gdata.apps.sites.AclFeedFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
GetAclFeed = get_acl_feed
|
||||
|
||||
def make_acl_entry_uri(self, domain=None, site=None, ruleId=None):
|
||||
return ACL_ENTRY_TEMPLATE % (domain, site, ruleId)
|
||||
|
||||
MakeAclEntryUri = make_acl_entry_uri
|
||||
|
||||
def create_acl_entry(self, aclentry=None, uri=None, domain=None, site=None,
|
||||
extra_headers=None, url_params=None, escape_params=True):
|
||||
|
||||
uri = uri or self.make_acl_feed_uri(domain=domain, site=site)
|
||||
try:
|
||||
return self.Post(aclentry, uri,
|
||||
url_params=url_params, extra_headers=extra_headers, escape_params=escape_params,
|
||||
converter=gdata.apps.sites.AclEntryFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
CreateAclEntry = create_acl_entry
|
||||
|
||||
def get_acl_entry(self, uri=None, domain=None, site=None, ruleId=None,
|
||||
extra_headers=None, url_params=None, escape_params=True):
|
||||
|
||||
uri = uri or self.make_acl_entry_uri(domain=domain, site=site, ruleId=ruleId)
|
||||
try:
|
||||
return self.Get(uri,
|
||||
url_params=url_params, extra_headers=extra_headers, escape_params=escape_params,
|
||||
converter=gdata.apps.sites.AclEntryFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
GetAclEntry = get_acl_entry
|
||||
|
||||
def update_acl_entry(self, aclentry=None, uri=None, domain=None, site=None, ruleId=None,
|
||||
extra_headers=None, url_params=None, escape_params=True):
|
||||
|
||||
uri = uri or self.make_acl_entry_uri(domain=domain, site=site, ruleId=ruleId)
|
||||
try:
|
||||
return self.Put(aclentry, uri,
|
||||
url_params=url_params, extra_headers=extra_headers, escape_params=escape_params,
|
||||
converter=gdata.apps.sites.AclEntryFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
UpdateAclEntry = update_acl_entry
|
||||
|
||||
def delete_acl_entry(self, uri=None, domain=None, site=None, ruleId=None,
|
||||
extra_headers=None, url_params=None, escape_params=True):
|
||||
|
||||
uri = uri or self.make_acl_entry_uri(domain=domain, site=site, ruleId=ruleId)
|
||||
try:
|
||||
return self.Delete(uri,
|
||||
url_params=url_params, escape_params=escape_params, extra_headers=extra_headers)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
DeleteAclEntry = delete_acl_entry
|
||||
|
||||
def make_activity_feed_uri(self, domain=None, site=None):
|
||||
return ACTIVITY_FEED_TEMPLATE % (domain, site)
|
||||
|
||||
MakeActivityFeedUri = make_activity_feed_uri
|
||||
|
||||
def get_activity_feed(self, uri=None, domain=None, site=None,
|
||||
extra_headers=None, url_params=None, escape_params=True):
|
||||
|
||||
uri = uri or self.make_activity_feed_uri(domain=domain, site=site)
|
||||
try:
|
||||
return self.Get(uri,
|
||||
url_params=url_params, extra_headers=extra_headers, escape_params=escape_params,
|
||||
converter=gdata.apps.sites.ActivityFeedFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
GetActivityFeed = get_activity_feed
|
||||
|
||||
def make_activity_entry_uri(self, domain=None, site=None, activityId=None):
|
||||
return ACTIVITY_ENTRY_TEMPLATE % (domain, site, activityId)
|
||||
|
||||
MakeActivityEntryUri = make_activity_entry_uri
|
||||
|
||||
def get_activity_entry(self, uri=None, domain=None, site=None, activityId=None,
|
||||
extra_headers=None, url_params=None, escape_params=True):
|
||||
|
||||
uri = uri or self.make_activity_entry_uri(domain=domain, site=site, activityId=activityId)
|
||||
try:
|
||||
return self.Get(uri,
|
||||
url_params=url_params, extra_headers=extra_headers, escape_params=escape_params,
|
||||
converter=gdata.apps.sites.ActivityEntryFromString)
|
||||
except gdata.service.RequestError as e:
|
||||
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
|
||||
|
||||
GetActivityEntry = get_activity_entry
|
||||
|
||||
class SitesQuery(gdata.service.Query):
|
||||
|
||||
def make_site_feed_uri(self, domain=None, site=None):
|
||||
if not domain:
|
||||
domain = 'site'
|
||||
if not site:
|
||||
return SITE_FEED_TEMPLATE % domain
|
||||
return (SITE_FEED_TEMPLATE % domain) + site
|
||||
|
||||
def __init__(self, feed=None, domain=None, site=None, params=None):
|
||||
self.feed = feed or self.make_site_feed_uri(domain=domain, site=site)
|
||||
gdata.service.Query.__init__(self, feed=self.feed, params=params)
|
||||
|
||||
1714
src/gam/gdata/service.py
Normal file
1714
src/gam/gdata/service.py
Normal file
File diff suppressed because it is too large
Load Diff
247
src/gam/gdata/urlfetch.py
Normal file
247
src/gam/gdata/urlfetch.py
Normal file
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
"""Provides HTTP functions for gdata.service to use on Google App Engine
|
||||
|
||||
AppEngineHttpClient: Provides an HTTP request method which uses App Engine's
|
||||
urlfetch API. Set the http_client member of a GDataService object to an
|
||||
instance of an AppEngineHttpClient to allow the gdata library to run on
|
||||
Google App Engine.
|
||||
|
||||
run_on_appengine: Function which will modify an existing GDataService object
|
||||
to allow it to run on App Engine. It works by creating a new instance of
|
||||
the AppEngineHttpClient and replacing the GDataService object's
|
||||
http_client.
|
||||
|
||||
HttpRequest: Function that wraps google.appengine.api.urlfetch.Fetch in a
|
||||
common interface which is used by gdata.service.GDataService. In other
|
||||
words, this module can be used as the gdata service request handler so
|
||||
that all HTTP requests will be performed by the hosting Google App Engine
|
||||
server.
|
||||
"""
|
||||
|
||||
|
||||
__author__ = 'api.jscudder (Jeff Scudder)'
|
||||
|
||||
|
||||
import io
|
||||
import atom.service
|
||||
import atom.http_interface
|
||||
from google.appengine.api import urlfetch
|
||||
|
||||
|
||||
def run_on_appengine(gdata_service):
|
||||
"""Modifies a GDataService object to allow it to run on App Engine.
|
||||
|
||||
Args:
|
||||
gdata_service: An instance of AtomService, GDataService, or any
|
||||
of their subclasses which has an http_client member.
|
||||
"""
|
||||
gdata_service.http_client = AppEngineHttpClient()
|
||||
|
||||
|
||||
class AppEngineHttpClient(atom.http_interface.GenericHttpClient):
|
||||
def __init__(self, headers=None):
|
||||
self.debug = False
|
||||
self.headers = headers or {}
|
||||
|
||||
def request(self, operation, url, data=None, headers=None):
|
||||
"""Performs an HTTP call to the server, supports GET, POST, PUT, and
|
||||
DELETE.
|
||||
|
||||
Usage example, perform and HTTP GET on http://www.google.com/:
|
||||
import atom.http
|
||||
client = atom.http.HttpClient()
|
||||
http_response = client.request('GET', 'http://www.google.com/')
|
||||
|
||||
Args:
|
||||
operation: str The HTTP operation to be performed. This is usually one
|
||||
of 'GET', 'POST', 'PUT', or 'DELETE'
|
||||
data: filestream, list of parts, or other object which can be converted
|
||||
to a string. Should be set to None when performing a GET or DELETE.
|
||||
If data is a file-like object which can be read, this method will
|
||||
read a chunk of 100K bytes at a time and send them.
|
||||
If the data is a list of parts to be sent, each part will be
|
||||
evaluated and sent.
|
||||
url: The full URL to which the request should be sent. Can be a string
|
||||
or atom.url.Url.
|
||||
headers: dict of strings. HTTP headers which should be sent
|
||||
in the request.
|
||||
"""
|
||||
all_headers = self.headers.copy()
|
||||
if headers:
|
||||
all_headers.update(headers)
|
||||
|
||||
# Construct the full payload.
|
||||
# Assume that data is None or a string.
|
||||
data_str = data
|
||||
if data:
|
||||
if isinstance(data, list):
|
||||
# If data is a list of different objects, convert them all to strings
|
||||
# and join them together.
|
||||
converted_parts = [__ConvertDataPart(x) for x in data]
|
||||
data_str = ''.join(converted_parts)
|
||||
else:
|
||||
data_str = __ConvertDataPart(data)
|
||||
|
||||
# If the list of headers does not include a Content-Length, attempt to
|
||||
# calculate it based on the data object.
|
||||
if data and 'Content-Length' not in all_headers:
|
||||
all_headers['Content-Length'] = len(data_str)
|
||||
|
||||
# Set the content type to the default value if none was set.
|
||||
if 'Content-Type' not in all_headers:
|
||||
all_headers['Content-Type'] = 'application/atom+xml'
|
||||
|
||||
# Lookup the urlfetch operation which corresponds to the desired HTTP verb.
|
||||
if operation == 'GET':
|
||||
method = urlfetch.GET
|
||||
elif operation == 'POST':
|
||||
method = urlfetch.POST
|
||||
elif operation == 'PUT':
|
||||
method = urlfetch.PUT
|
||||
elif operation == 'DELETE':
|
||||
method = urlfetch.DELETE
|
||||
else:
|
||||
method = None
|
||||
return HttpResponse(urlfetch.Fetch(url=str(url), payload=data_str,
|
||||
method=method, headers=all_headers))
|
||||
|
||||
|
||||
def HttpRequest(service, operation, data, uri, extra_headers=None,
|
||||
url_params=None, escape_params=True, content_type='application/atom+xml'):
|
||||
"""Performs an HTTP call to the server, supports GET, POST, PUT, and DELETE.
|
||||
|
||||
This function is deprecated, use AppEngineHttpClient.request instead.
|
||||
|
||||
To use this module with gdata.service, you can set this module to be the
|
||||
http_request_handler so that HTTP requests use Google App Engine's urlfetch.
|
||||
import gdata.service
|
||||
import gdata.urlfetch
|
||||
gdata.service.http_request_handler = gdata.urlfetch
|
||||
|
||||
Args:
|
||||
service: atom.AtomService object which contains some of the parameters
|
||||
needed to make the request. The following members are used to
|
||||
construct the HTTP call: server (str), additional_headers (dict),
|
||||
port (int), and ssl (bool).
|
||||
operation: str The HTTP operation to be performed. This is usually one of
|
||||
'GET', 'POST', 'PUT', or 'DELETE'
|
||||
data: filestream, list of parts, or other object which can be
|
||||
converted to a string.
|
||||
Should be set to None when performing a GET or PUT.
|
||||
If data is a file-like object which can be read, this method will read
|
||||
a chunk of 100K bytes at a time and send them.
|
||||
If the data is a list of parts to be sent, each part will be evaluated
|
||||
and sent.
|
||||
uri: The beginning of the URL to which the request should be sent.
|
||||
Examples: '/', '/base/feeds/snippets',
|
||||
'/m8/feeds/contacts/default/base'
|
||||
extra_headers: dict of strings. HTTP headers which should be sent
|
||||
in the request. These headers are in addition to those stored in
|
||||
service.additional_headers.
|
||||
url_params: dict of strings. Key value pairs to be added to the URL as
|
||||
URL parameters. For example {'foo':'bar', 'test':'param'} will
|
||||
become ?foo=bar&test=param.
|
||||
escape_params: bool default True. If true, the keys and values in
|
||||
url_params will be URL escaped when the form is constructed
|
||||
(Special characters converted to %XX form.)
|
||||
content_type: str The MIME type for the data being sent. Defaults to
|
||||
'application/atom+xml', this is only used if data is set.
|
||||
"""
|
||||
full_uri = atom.service.BuildUri(uri, url_params, escape_params)
|
||||
(server, port, ssl, partial_uri) = atom.service.ProcessUrl(service, full_uri)
|
||||
# Construct the full URL for the request.
|
||||
if ssl:
|
||||
full_url = 'https://%s%s' % (server, partial_uri)
|
||||
else:
|
||||
full_url = 'http://%s%s' % (server, partial_uri)
|
||||
|
||||
# Construct the full payload.
|
||||
# Assume that data is None or a string.
|
||||
data_str = data
|
||||
if data:
|
||||
if isinstance(data, list):
|
||||
# If data is a list of different objects, convert them all to strings
|
||||
# and join them together.
|
||||
converted_parts = [__ConvertDataPart(x) for x in data]
|
||||
data_str = ''.join(converted_parts)
|
||||
else:
|
||||
data_str = __ConvertDataPart(data)
|
||||
|
||||
# Construct the dictionary of HTTP headers.
|
||||
headers = {}
|
||||
if isinstance(service.additional_headers, dict):
|
||||
headers = service.additional_headers.copy()
|
||||
if isinstance(extra_headers, dict):
|
||||
for header, value in extra_headers.items():
|
||||
headers[header] = value
|
||||
# Add the content type header (we don't need to calculate content length,
|
||||
# since urlfetch.Fetch will calculate for us).
|
||||
if content_type:
|
||||
headers['Content-Type'] = content_type
|
||||
|
||||
# Lookup the urlfetch operation which corresponds to the desired HTTP verb.
|
||||
if operation == 'GET':
|
||||
method = urlfetch.GET
|
||||
elif operation == 'POST':
|
||||
method = urlfetch.POST
|
||||
elif operation == 'PUT':
|
||||
method = urlfetch.PUT
|
||||
elif operation == 'DELETE':
|
||||
method = urlfetch.DELETE
|
||||
else:
|
||||
method = None
|
||||
return HttpResponse(urlfetch.Fetch(url=full_url, payload=data_str,
|
||||
method=method, headers=headers))
|
||||
|
||||
|
||||
def __ConvertDataPart(data):
|
||||
if not data or isinstance(data, str):
|
||||
return data
|
||||
elif hasattr(data, 'read'):
|
||||
# data is a file like object, so read it completely.
|
||||
return data.read()
|
||||
# The data object was not a file.
|
||||
# Try to convert to a string and send the data.
|
||||
return str(data)
|
||||
|
||||
|
||||
class HttpResponse(object):
|
||||
"""Translates a urlfetch resoinse to look like an hhtplib resoinse.
|
||||
|
||||
Used to allow the resoinse from HttpRequest to be usable by gdata.service
|
||||
methods.
|
||||
"""
|
||||
|
||||
def __init__(self, urlfetch_response):
|
||||
self.body = io.StringIO(urlfetch_response.content)
|
||||
self.headers = urlfetch_response.headers
|
||||
self.status = urlfetch_response.status_code
|
||||
self.reason = ''
|
||||
|
||||
def read(self, length=None):
|
||||
if not length:
|
||||
return self.body.read()
|
||||
else:
|
||||
return self.body.read(length)
|
||||
|
||||
def getheader(self, name):
|
||||
if name not in self.headers:
|
||||
return self.headers[name.lower()]
|
||||
return self.headers[name]
|
||||
|
||||
@@ -12,16 +12,16 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__version__ = "1.5.2"
|
||||
|
||||
# Set default logging handler to avoid "No handler found" warnings.
|
||||
import logging
|
||||
|
||||
try: # Python 2.7+
|
||||
from logging import NullHandler
|
||||
except ImportError:
|
||||
|
||||
class NullHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
|
||||
logging.getLogger(__name__).addHandler(NullHandler())
|
||||
167
src/gam/googleapiclient/_auth.py
Normal file
167
src/gam/googleapiclient/_auth.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Helpers for authentication using oauth2client or google-auth."""
|
||||
|
||||
import httplib2
|
||||
|
||||
try:
|
||||
import google.auth
|
||||
import google.auth.credentials
|
||||
|
||||
HAS_GOOGLE_AUTH = True
|
||||
except ImportError: # pragma: NO COVER
|
||||
HAS_GOOGLE_AUTH = False
|
||||
|
||||
try:
|
||||
import google_auth_httplib2
|
||||
except ImportError: # pragma: NO COVER
|
||||
google_auth_httplib2 = None
|
||||
|
||||
try:
|
||||
import oauth2client
|
||||
import oauth2client.client
|
||||
|
||||
HAS_OAUTH2CLIENT = True
|
||||
except ImportError: # pragma: NO COVER
|
||||
HAS_OAUTH2CLIENT = False
|
||||
|
||||
|
||||
def credentials_from_file(filename, scopes=None, quota_project_id=None):
|
||||
"""Returns credentials loaded from a file."""
|
||||
if HAS_GOOGLE_AUTH:
|
||||
credentials, _ = google.auth.load_credentials_from_file(
|
||||
filename, scopes=scopes, quota_project_id=quota_project_id
|
||||
)
|
||||
return credentials
|
||||
else:
|
||||
raise EnvironmentError(
|
||||
"client_options.credentials_file is only supported in google-auth."
|
||||
)
|
||||
|
||||
|
||||
def default_credentials(scopes=None, quota_project_id=None):
|
||||
"""Returns Application Default Credentials."""
|
||||
if HAS_GOOGLE_AUTH:
|
||||
credentials, _ = google.auth.default(
|
||||
scopes=scopes, quota_project_id=quota_project_id
|
||||
)
|
||||
return credentials
|
||||
elif HAS_OAUTH2CLIENT:
|
||||
if scopes is not None or quota_project_id is not None:
|
||||
raise EnvironmentError(
|
||||
"client_options.scopes and client_options.quota_project_id are not supported in oauth2client."
|
||||
"Please install google-auth."
|
||||
)
|
||||
return oauth2client.client.GoogleCredentials.get_application_default()
|
||||
else:
|
||||
raise EnvironmentError(
|
||||
"No authentication library is available. Please install either "
|
||||
"google-auth or oauth2client."
|
||||
)
|
||||
|
||||
|
||||
def with_scopes(credentials, scopes):
|
||||
"""Scopes the credentials if necessary.
|
||||
|
||||
Args:
|
||||
credentials (Union[
|
||||
google.auth.credentials.Credentials,
|
||||
oauth2client.client.Credentials]): The credentials to scope.
|
||||
scopes (Sequence[str]): The list of scopes.
|
||||
|
||||
Returns:
|
||||
Union[google.auth.credentials.Credentials,
|
||||
oauth2client.client.Credentials]: The scoped credentials.
|
||||
"""
|
||||
if HAS_GOOGLE_AUTH and isinstance(credentials, google.auth.credentials.Credentials):
|
||||
return google.auth.credentials.with_scopes_if_required(credentials, scopes)
|
||||
else:
|
||||
try:
|
||||
if credentials.create_scoped_required():
|
||||
return credentials.create_scoped(scopes)
|
||||
else:
|
||||
return credentials
|
||||
except AttributeError:
|
||||
return credentials
|
||||
|
||||
|
||||
def authorized_http(credentials):
|
||||
"""Returns an http client that is authorized with the given credentials.
|
||||
|
||||
Args:
|
||||
credentials (Union[
|
||||
google.auth.credentials.Credentials,
|
||||
oauth2client.client.Credentials]): The credentials to use.
|
||||
|
||||
Returns:
|
||||
Union[httplib2.Http, google_auth_httplib2.AuthorizedHttp]: An
|
||||
authorized http client.
|
||||
"""
|
||||
from googleapiclient.http import build_http
|
||||
|
||||
if HAS_GOOGLE_AUTH and isinstance(credentials, google.auth.credentials.Credentials):
|
||||
if google_auth_httplib2 is None:
|
||||
raise ValueError(
|
||||
"Credentials from google.auth specified, but "
|
||||
"google-api-python-client is unable to use these credentials "
|
||||
"unless google-auth-httplib2 is installed. Please install "
|
||||
"google-auth-httplib2."
|
||||
)
|
||||
return google_auth_httplib2.AuthorizedHttp(credentials, http=build_http())
|
||||
else:
|
||||
return credentials.authorize(build_http())
|
||||
|
||||
|
||||
def refresh_credentials(credentials):
|
||||
# Refresh must use a new http instance, as the one associated with the
|
||||
# credentials could be a AuthorizedHttp or an oauth2client-decorated
|
||||
# Http instance which would cause a weird recursive loop of refreshing
|
||||
# and likely tear a hole in spacetime.
|
||||
refresh_http = httplib2.Http()
|
||||
if HAS_GOOGLE_AUTH and isinstance(credentials, google.auth.credentials.Credentials):
|
||||
request = google_auth_httplib2.Request(refresh_http)
|
||||
return credentials.refresh(request)
|
||||
else:
|
||||
return credentials.refresh(refresh_http)
|
||||
|
||||
|
||||
def apply_credentials(credentials, headers):
|
||||
# oauth2client and google-auth have the same interface for this.
|
||||
if not is_valid(credentials):
|
||||
refresh_credentials(credentials)
|
||||
return credentials.apply(headers)
|
||||
|
||||
|
||||
def is_valid(credentials):
|
||||
if HAS_GOOGLE_AUTH and isinstance(credentials, google.auth.credentials.Credentials):
|
||||
return credentials.valid
|
||||
else:
|
||||
return (
|
||||
credentials.access_token is not None
|
||||
and not credentials.access_token_expired
|
||||
)
|
||||
|
||||
|
||||
def get_credentials_from_http(http):
|
||||
if http is None:
|
||||
return None
|
||||
elif hasattr(http.request, "credentials"):
|
||||
return http.request.credentials
|
||||
elif hasattr(http, "credentials") and not isinstance(
|
||||
http.credentials, httplib2.Credentials
|
||||
):
|
||||
return http.credentials
|
||||
else:
|
||||
return None
|
||||
@@ -1,10 +1,10 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -12,46 +12,36 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Common utility library."""
|
||||
"""Helper functions for commonly used utilities."""
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
|
||||
import six
|
||||
from six.moves import urllib
|
||||
|
||||
|
||||
__author__ = [
|
||||
'rafek@google.com (Rafe Kaplan)',
|
||||
'guido@google.com (Guido van Rossum)',
|
||||
]
|
||||
|
||||
__all__ = [
|
||||
'positional',
|
||||
'POSITIONAL_WARNING',
|
||||
'POSITIONAL_EXCEPTION',
|
||||
'POSITIONAL_IGNORE',
|
||||
]
|
||||
import urllib
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
POSITIONAL_WARNING = 'WARNING'
|
||||
POSITIONAL_EXCEPTION = 'EXCEPTION'
|
||||
POSITIONAL_IGNORE = 'IGNORE'
|
||||
POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION,
|
||||
POSITIONAL_IGNORE])
|
||||
POSITIONAL_WARNING = "WARNING"
|
||||
POSITIONAL_EXCEPTION = "EXCEPTION"
|
||||
POSITIONAL_IGNORE = "IGNORE"
|
||||
POSITIONAL_SET = frozenset(
|
||||
[POSITIONAL_WARNING, POSITIONAL_EXCEPTION, POSITIONAL_IGNORE]
|
||||
)
|
||||
|
||||
positional_parameters_enforcement = POSITIONAL_WARNING
|
||||
|
||||
_SYM_LINK_MESSAGE = "File: {0}: Is a symbolic link."
|
||||
_IS_DIR_MESSAGE = "{0}: Is a directory"
|
||||
_MISSING_FILE_MESSAGE = "Cannot access {0}: No such file or directory"
|
||||
|
||||
|
||||
def positional(max_positional_args):
|
||||
"""A decorator to declare that only the first N arguments my be positional.
|
||||
"""A decorator to declare that only the first N arguments may be positional.
|
||||
|
||||
This decorator makes it easy to support Python 3 style keyword-only
|
||||
parameters. For example, in Python 3 it is possible to write::
|
||||
|
||||
def fn(pos1, *, kwonly1=None, kwonly1=None):
|
||||
def fn(pos1, *, kwonly1=None, kwonly2=None):
|
||||
...
|
||||
|
||||
All named parameters after ``*`` must be a keyword::
|
||||
@@ -96,14 +86,14 @@ def positional(max_positional_args):
|
||||
...
|
||||
|
||||
The positional decorator behavior is controlled by
|
||||
``util.positional_parameters_enforcement``, which may be set to
|
||||
``_helpers.positional_parameters_enforcement``, which may be set to
|
||||
``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
|
||||
``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do
|
||||
nothing, respectively, if a declaration is violated.
|
||||
|
||||
Args:
|
||||
max_positional_arguments: Maximum number of positional arguments. All
|
||||
parameters after the this index must be
|
||||
parameters after this index must be
|
||||
keyword only.
|
||||
|
||||
Returns:
|
||||
@@ -111,9 +101,9 @@ def positional(max_positional_args):
|
||||
from being used as positional parameters.
|
||||
|
||||
Raises:
|
||||
TypeError: if a key-word only argument is provided as a positional
|
||||
TypeError: if a keyword-only argument is provided as a positional
|
||||
parameter, but only if
|
||||
util.positional_parameters_enforcement is set to
|
||||
_helpers.positional_parameters_enforcement is set to
|
||||
POSITIONAL_EXCEPTION.
|
||||
"""
|
||||
|
||||
@@ -121,66 +111,81 @@ def positional(max_positional_args):
|
||||
@functools.wraps(wrapped)
|
||||
def positional_wrapper(*args, **kwargs):
|
||||
if len(args) > max_positional_args:
|
||||
plural_s = ''
|
||||
plural_s = ""
|
||||
if max_positional_args != 1:
|
||||
plural_s = 's'
|
||||
message = ('{function}() takes at most {args_max} positional '
|
||||
'argument{plural} ({args_given} given)'.format(
|
||||
function=wrapped.__name__,
|
||||
args_max=max_positional_args,
|
||||
args_given=len(args),
|
||||
plural=plural_s))
|
||||
plural_s = "s"
|
||||
message = (
|
||||
"{function}() takes at most {args_max} positional "
|
||||
"argument{plural} ({args_given} given)".format(
|
||||
function=wrapped.__name__,
|
||||
args_max=max_positional_args,
|
||||
args_given=len(args),
|
||||
plural=plural_s,
|
||||
)
|
||||
)
|
||||
if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
|
||||
raise TypeError(message)
|
||||
elif positional_parameters_enforcement == POSITIONAL_WARNING:
|
||||
logger.warning(message)
|
||||
return wrapped(*args, **kwargs)
|
||||
|
||||
return positional_wrapper
|
||||
|
||||
if isinstance(max_positional_args, six.integer_types):
|
||||
if isinstance(max_positional_args, int):
|
||||
return positional_decorator
|
||||
else:
|
||||
args, _, _, defaults = inspect.getargspec(max_positional_args)
|
||||
args, _, _, defaults, _, _, _ = inspect.getfullargspec(max_positional_args)
|
||||
return positional(len(args) - len(defaults))(max_positional_args)
|
||||
|
||||
|
||||
def scopes_to_string(scopes):
|
||||
"""Converts scope value to a string.
|
||||
|
||||
If scopes is a string then it is simply passed through. If scopes is an
|
||||
iterable then a string is returned that is all the individual scopes
|
||||
concatenated with spaces.
|
||||
def parse_unique_urlencoded(content):
|
||||
"""Parses unique key-value parameters from urlencoded content.
|
||||
|
||||
Args:
|
||||
scopes: string or iterable of strings, the scopes.
|
||||
content: string, URL-encoded key-value pairs.
|
||||
|
||||
Returns:
|
||||
The scopes formatted as a single string.
|
||||
dict, The key-value pairs from ``content``.
|
||||
|
||||
Raises:
|
||||
ValueError: if one of the keys is repeated.
|
||||
"""
|
||||
if isinstance(scopes, six.string_types):
|
||||
return scopes
|
||||
else:
|
||||
return ' '.join(scopes)
|
||||
urlencoded_params = urllib.parse.parse_qs(content)
|
||||
params = {}
|
||||
for key, value in urlencoded_params.items():
|
||||
if len(value) != 1:
|
||||
msg = "URL-encoded content contains a repeated value:" "%s -> %s" % (
|
||||
key,
|
||||
", ".join(value),
|
||||
)
|
||||
raise ValueError(msg)
|
||||
params[key] = value[0]
|
||||
return params
|
||||
|
||||
|
||||
def string_to_scopes(scopes):
|
||||
"""Converts stringifed scope value to a list.
|
||||
def update_query_params(uri, params):
|
||||
"""Updates a URI with new query parameters.
|
||||
|
||||
If scopes is a list then it is simply passed through. If scopes is an
|
||||
string then a list of each individual scope is returned.
|
||||
If a given key from ``params`` is repeated in the ``uri``, then
|
||||
the URI will be considered invalid and an error will occur.
|
||||
|
||||
If the URI is valid, then each value from ``params`` will
|
||||
replace the corresponding value in the query parameters (if
|
||||
it exists).
|
||||
|
||||
Args:
|
||||
scopes: a string or iterable of strings, the scopes.
|
||||
uri: string, A valid URI, with potential existing query parameters.
|
||||
params: dict, A dictionary of query parameters.
|
||||
|
||||
Returns:
|
||||
The scopes in a list.
|
||||
The same URI but with the new query parameters added.
|
||||
"""
|
||||
if not scopes:
|
||||
return []
|
||||
if isinstance(scopes, six.string_types):
|
||||
return scopes.split(' ')
|
||||
else:
|
||||
return scopes
|
||||
parts = urllib.parse.urlparse(uri)
|
||||
query_params = parse_unique_urlencoded(parts.query)
|
||||
query_params.update(params)
|
||||
new_query = urllib.parse.urlencode(query_params)
|
||||
new_parts = parts._replace(query=new_query)
|
||||
return urllib.parse.urlunparse(new_parts)
|
||||
|
||||
|
||||
def _add_query_parameter(url, name, value):
|
||||
@@ -199,8 +204,4 @@ def _add_query_parameter(url, name, value):
|
||||
if value is None:
|
||||
return url
|
||||
else:
|
||||
parsed = list(urllib.parse.urlparse(url))
|
||||
q = dict(urllib.parse.parse_qsl(parsed[4]))
|
||||
q[name] = value
|
||||
parsed[4] = urllib.parse.urlencode(q)
|
||||
return urllib.parse.urlunparse(parsed)
|
||||
return update_query_params(url, {name: value})
|
||||
315
src/gam/googleapiclient/channel.py
Normal file
315
src/gam/googleapiclient/channel.py
Normal file
@@ -0,0 +1,315 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Channel notifications support.
|
||||
|
||||
Classes and functions to support channel subscriptions and notifications
|
||||
on those channels.
|
||||
|
||||
Notes:
|
||||
- This code is based on experimental APIs and is subject to change.
|
||||
- Notification does not do deduplication of notification ids, that's up to
|
||||
the receiver.
|
||||
- Storing the Channel between calls is up to the caller.
|
||||
|
||||
|
||||
Example setting up a channel:
|
||||
|
||||
# Create a new channel that gets notifications via webhook.
|
||||
channel = new_webhook_channel("https://example.com/my_web_hook")
|
||||
|
||||
# Store the channel, keyed by 'channel.id'. Store it before calling the
|
||||
# watch method because notifications may start arriving before the watch
|
||||
# method returns.
|
||||
...
|
||||
|
||||
resp = service.objects().watchAll(
|
||||
bucket="some_bucket_id", body=channel.body()).execute()
|
||||
channel.update(resp)
|
||||
|
||||
# Store the channel, keyed by 'channel.id'. Store it after being updated
|
||||
# since the resource_id value will now be correct, and that's needed to
|
||||
# stop a subscription.
|
||||
...
|
||||
|
||||
|
||||
An example Webhook implementation using webapp2. Note that webapp2 puts
|
||||
headers in a case insensitive dictionary, as headers aren't guaranteed to
|
||||
always be upper case.
|
||||
|
||||
id = self.request.headers[X_GOOG_CHANNEL_ID]
|
||||
|
||||
# Retrieve the channel by id.
|
||||
channel = ...
|
||||
|
||||
# Parse notification from the headers, including validating the id.
|
||||
n = notification_from_headers(channel, self.request.headers)
|
||||
|
||||
# Do app specific stuff with the notification here.
|
||||
if n.resource_state == 'sync':
|
||||
# Code to handle sync state.
|
||||
elif n.resource_state == 'exists':
|
||||
# Code to handle the exists state.
|
||||
elif n.resource_state == 'not_exists':
|
||||
# Code to handle the not exists state.
|
||||
|
||||
|
||||
Example of unsubscribing.
|
||||
|
||||
service.channels().stop(channel.body()).execute()
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from googleapiclient import _helpers as util
|
||||
from googleapiclient import errors
|
||||
|
||||
# The unix time epoch starts at midnight 1970.
|
||||
EPOCH = datetime.datetime(1970, 1, 1)
|
||||
|
||||
# Map the names of the parameters in the JSON channel description to
|
||||
# the parameter names we use in the Channel class.
|
||||
CHANNEL_PARAMS = {
|
||||
"address": "address",
|
||||
"id": "id",
|
||||
"expiration": "expiration",
|
||||
"params": "params",
|
||||
"resourceId": "resource_id",
|
||||
"resourceUri": "resource_uri",
|
||||
"type": "type",
|
||||
"token": "token",
|
||||
}
|
||||
|
||||
X_GOOG_CHANNEL_ID = "X-GOOG-CHANNEL-ID"
|
||||
X_GOOG_MESSAGE_NUMBER = "X-GOOG-MESSAGE-NUMBER"
|
||||
X_GOOG_RESOURCE_STATE = "X-GOOG-RESOURCE-STATE"
|
||||
X_GOOG_RESOURCE_URI = "X-GOOG-RESOURCE-URI"
|
||||
X_GOOG_RESOURCE_ID = "X-GOOG-RESOURCE-ID"
|
||||
|
||||
|
||||
def _upper_header_keys(headers):
|
||||
new_headers = {}
|
||||
for k, v in headers.items():
|
||||
new_headers[k.upper()] = v
|
||||
return new_headers
|
||||
|
||||
|
||||
class Notification(object):
|
||||
"""A Notification from a Channel.
|
||||
|
||||
Notifications are not usually constructed directly, but are returned
|
||||
from functions like notification_from_headers().
|
||||
|
||||
Attributes:
|
||||
message_number: int, The unique id number of this notification.
|
||||
state: str, The state of the resource being monitored.
|
||||
uri: str, The address of the resource being monitored.
|
||||
resource_id: str, The unique identifier of the version of the resource at
|
||||
this event.
|
||||
"""
|
||||
|
||||
@util.positional(5)
|
||||
def __init__(self, message_number, state, resource_uri, resource_id):
|
||||
"""Notification constructor.
|
||||
|
||||
Args:
|
||||
message_number: int, The unique id number of this notification.
|
||||
state: str, The state of the resource being monitored. Can be one
|
||||
of "exists", "not_exists", or "sync".
|
||||
resource_uri: str, The address of the resource being monitored.
|
||||
resource_id: str, The identifier of the watched resource.
|
||||
"""
|
||||
self.message_number = message_number
|
||||
self.state = state
|
||||
self.resource_uri = resource_uri
|
||||
self.resource_id = resource_id
|
||||
|
||||
|
||||
class Channel(object):
|
||||
"""A Channel for notifications.
|
||||
|
||||
Usually not constructed directly, instead it is returned from helper
|
||||
functions like new_webhook_channel().
|
||||
|
||||
Attributes:
|
||||
type: str, The type of delivery mechanism used by this channel. For
|
||||
example, 'web_hook'.
|
||||
id: str, A UUID for the channel.
|
||||
token: str, An arbitrary string associated with the channel that
|
||||
is delivered to the target address with each event delivered
|
||||
over this channel.
|
||||
address: str, The address of the receiving entity where events are
|
||||
delivered. Specific to the channel type.
|
||||
expiration: int, The time, in milliseconds from the epoch, when this
|
||||
channel will expire.
|
||||
params: dict, A dictionary of string to string, with additional parameters
|
||||
controlling delivery channel behavior.
|
||||
resource_id: str, An opaque id that identifies the resource that is
|
||||
being watched. Stable across different API versions.
|
||||
resource_uri: str, The canonicalized ID of the watched resource.
|
||||
"""
|
||||
|
||||
@util.positional(5)
|
||||
def __init__(
|
||||
self,
|
||||
type,
|
||||
id,
|
||||
token,
|
||||
address,
|
||||
expiration=None,
|
||||
params=None,
|
||||
resource_id="",
|
||||
resource_uri="",
|
||||
):
|
||||
"""Create a new Channel.
|
||||
|
||||
In user code, this Channel constructor will not typically be called
|
||||
manually since there are functions for creating channels for each specific
|
||||
type with a more customized set of arguments to pass.
|
||||
|
||||
Args:
|
||||
type: str, The type of delivery mechanism used by this channel. For
|
||||
example, 'web_hook'.
|
||||
id: str, A UUID for the channel.
|
||||
token: str, An arbitrary string associated with the channel that
|
||||
is delivered to the target address with each event delivered
|
||||
over this channel.
|
||||
address: str, The address of the receiving entity where events are
|
||||
delivered. Specific to the channel type.
|
||||
expiration: int, The time, in milliseconds from the epoch, when this
|
||||
channel will expire.
|
||||
params: dict, A dictionary of string to string, with additional parameters
|
||||
controlling delivery channel behavior.
|
||||
resource_id: str, An opaque id that identifies the resource that is
|
||||
being watched. Stable across different API versions.
|
||||
resource_uri: str, The canonicalized ID of the watched resource.
|
||||
"""
|
||||
self.type = type
|
||||
self.id = id
|
||||
self.token = token
|
||||
self.address = address
|
||||
self.expiration = expiration
|
||||
self.params = params
|
||||
self.resource_id = resource_id
|
||||
self.resource_uri = resource_uri
|
||||
|
||||
def body(self):
|
||||
"""Build a body from the Channel.
|
||||
|
||||
Constructs a dictionary that's appropriate for passing into watch()
|
||||
methods as the value of body argument.
|
||||
|
||||
Returns:
|
||||
A dictionary representation of the channel.
|
||||
"""
|
||||
result = {
|
||||
"id": self.id,
|
||||
"token": self.token,
|
||||
"type": self.type,
|
||||
"address": self.address,
|
||||
}
|
||||
if self.params:
|
||||
result["params"] = self.params
|
||||
if self.resource_id:
|
||||
result["resourceId"] = self.resource_id
|
||||
if self.resource_uri:
|
||||
result["resourceUri"] = self.resource_uri
|
||||
if self.expiration:
|
||||
result["expiration"] = self.expiration
|
||||
|
||||
return result
|
||||
|
||||
def update(self, resp):
|
||||
"""Update a channel with information from the response of watch().
|
||||
|
||||
When a request is sent to watch() a resource, the response returned
|
||||
from the watch() request is a dictionary with updated channel information,
|
||||
such as the resource_id, which is needed when stopping a subscription.
|
||||
|
||||
Args:
|
||||
resp: dict, The response from a watch() method.
|
||||
"""
|
||||
for json_name, param_name in CHANNEL_PARAMS.items():
|
||||
value = resp.get(json_name)
|
||||
if value is not None:
|
||||
setattr(self, param_name, value)
|
||||
|
||||
|
||||
def notification_from_headers(channel, headers):
|
||||
"""Parse a notification from the webhook request headers, validate
|
||||
the notification, and return a Notification object.
|
||||
|
||||
Args:
|
||||
channel: Channel, The channel that the notification is associated with.
|
||||
headers: dict, A dictionary like object that contains the request headers
|
||||
from the webhook HTTP request.
|
||||
|
||||
Returns:
|
||||
A Notification object.
|
||||
|
||||
Raises:
|
||||
errors.InvalidNotificationError if the notification is invalid.
|
||||
ValueError if the X-GOOG-MESSAGE-NUMBER can't be converted to an int.
|
||||
"""
|
||||
headers = _upper_header_keys(headers)
|
||||
channel_id = headers[X_GOOG_CHANNEL_ID]
|
||||
if channel.id != channel_id:
|
||||
raise errors.InvalidNotificationError(
|
||||
"Channel id mismatch: %s != %s" % (channel.id, channel_id)
|
||||
)
|
||||
else:
|
||||
message_number = int(headers[X_GOOG_MESSAGE_NUMBER])
|
||||
state = headers[X_GOOG_RESOURCE_STATE]
|
||||
resource_uri = headers[X_GOOG_RESOURCE_URI]
|
||||
resource_id = headers[X_GOOG_RESOURCE_ID]
|
||||
return Notification(message_number, state, resource_uri, resource_id)
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def new_webhook_channel(url, token=None, expiration=None, params=None):
|
||||
"""Create a new webhook Channel.
|
||||
|
||||
Args:
|
||||
url: str, URL to post notifications to.
|
||||
token: str, An arbitrary string associated with the channel that
|
||||
is delivered to the target address with each notification delivered
|
||||
over this channel.
|
||||
expiration: datetime.datetime, A time in the future when the channel
|
||||
should expire. Can also be None if the subscription should use the
|
||||
default expiration. Note that different services may have different
|
||||
limits on how long a subscription lasts. Check the response from the
|
||||
watch() method to see the value the service has set for an expiration
|
||||
time.
|
||||
params: dict, Extra parameters to pass on channel creation. Currently
|
||||
not used for webhook channels.
|
||||
"""
|
||||
expiration_ms = 0
|
||||
if expiration:
|
||||
delta = expiration - EPOCH
|
||||
expiration_ms = (
|
||||
delta.microseconds / 1000 + (delta.seconds + delta.days * 24 * 3600) * 1000
|
||||
)
|
||||
if expiration_ms < 0:
|
||||
expiration_ms = 0
|
||||
|
||||
return Channel(
|
||||
"web_hook",
|
||||
str(uuid.uuid4()),
|
||||
token,
|
||||
url,
|
||||
expiration=expiration_ms,
|
||||
params=params,
|
||||
)
|
||||
1662
src/gam/googleapiclient/discovery.py
Normal file
1662
src/gam/googleapiclient/discovery.py
Normal file
File diff suppressed because it is too large
Load Diff
78
src/gam/googleapiclient/discovery_cache/__init__.py
Normal file
78
src/gam/googleapiclient/discovery_cache/__init__.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Caching utility for the discovery document."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DISCOVERY_DOC_MAX_AGE = 60 * 60 * 24 # 1 day
|
||||
DISCOVERY_DOC_DIR = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)), "documents"
|
||||
)
|
||||
|
||||
|
||||
def autodetect():
|
||||
"""Detects an appropriate cache module and returns it.
|
||||
|
||||
Returns:
|
||||
googleapiclient.discovery_cache.base.Cache, a cache object which
|
||||
is auto detected, or None if no cache object is available.
|
||||
"""
|
||||
if "GAE_ENV" in os.environ:
|
||||
try:
|
||||
from . import appengine_memcache
|
||||
|
||||
return appengine_memcache.cache
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from . import file_cache
|
||||
|
||||
return file_cache.cache
|
||||
except Exception:
|
||||
LOGGER.info(
|
||||
"file_cache is only supported with oauth2client<4.0.0", exc_info=False
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def get_static_doc(serviceName, version):
|
||||
"""Retrieves the discovery document from the directory defined in
|
||||
DISCOVERY_DOC_DIR corresponding to the serviceName and version provided.
|
||||
|
||||
Args:
|
||||
serviceName: string, name of the service.
|
||||
version: string, the version of the service.
|
||||
|
||||
Returns:
|
||||
A string containing the contents of the JSON discovery document,
|
||||
otherwise None if the JSON discovery document was not found.
|
||||
"""
|
||||
|
||||
content = None
|
||||
doc_name = "{}.{}.json".format(serviceName, version)
|
||||
|
||||
try:
|
||||
with open(os.path.join(DISCOVERY_DOC_DIR, doc_name), "r") as f:
|
||||
content = f.read()
|
||||
except FileNotFoundError:
|
||||
# File does not exist. Nothing to do here.
|
||||
pass
|
||||
|
||||
return content
|
||||
@@ -23,33 +23,33 @@ from google.appengine.api import memcache
|
||||
from . import base
|
||||
from ..discovery_cache import DISCOVERY_DOC_MAX_AGE
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NAMESPACE = 'google-api-client'
|
||||
NAMESPACE = "google-api-client"
|
||||
|
||||
|
||||
class Cache(base.Cache):
|
||||
"""A cache with app engine memcache API."""
|
||||
"""A cache with app engine memcache API."""
|
||||
|
||||
def __init__(self, max_age):
|
||||
"""Constructor.
|
||||
def __init__(self, max_age):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
max_age: Cache expiration in seconds.
|
||||
"""
|
||||
self._max_age = max_age
|
||||
Args:
|
||||
max_age: Cache expiration in seconds.
|
||||
"""
|
||||
self._max_age = max_age
|
||||
|
||||
def get(self, url):
|
||||
try:
|
||||
return memcache.get(url, namespace=NAMESPACE)
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
def get(self, url):
|
||||
try:
|
||||
return memcache.get(url, namespace=NAMESPACE)
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
|
||||
def set(self, url, content):
|
||||
try:
|
||||
memcache.set(url, content, time=int(self._max_age), namespace=NAMESPACE)
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
|
||||
def set(self, url, content):
|
||||
try:
|
||||
memcache.set(url, content, time=int(self._max_age), namespace=NAMESPACE)
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
|
||||
cache = Cache(max_age=DISCOVERY_DOC_MAX_AGE)
|
||||
@@ -18,28 +18,29 @@ import abc
|
||||
|
||||
|
||||
class Cache(object):
|
||||
"""A base abstract cache class."""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
"""A base abstract cache class."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get(self, url):
|
||||
"""Gets the content from the memcache with a given key.
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
Args:
|
||||
url: string, the key for the cache.
|
||||
@abc.abstractmethod
|
||||
def get(self, url):
|
||||
"""Gets the content from the memcache with a given key.
|
||||
|
||||
Returns:
|
||||
object, the value in the cache for the given key, or None if the key is
|
||||
not in the cache.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
Args:
|
||||
url: string, the key for the cache.
|
||||
|
||||
@abc.abstractmethod
|
||||
def set(self, url, content):
|
||||
"""Sets the given key and content in the cache.
|
||||
Returns:
|
||||
object, the value in the cache for the given key, or None if the key is
|
||||
not in the cache.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
Args:
|
||||
url: string, the key for the cache.
|
||||
content: string, the discovery document.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
@abc.abstractmethod
|
||||
def set(self, url, content):
|
||||
"""Sets the given key and content in the cache.
|
||||
|
||||
Args:
|
||||
url: string, the key for the cache.
|
||||
content: string, the discovery document.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
145
src/gam/googleapiclient/discovery_cache/file_cache.py
Normal file
145
src/gam/googleapiclient/discovery_cache/file_cache.py
Normal file
@@ -0,0 +1,145 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""File based cache for the discovery document.
|
||||
|
||||
The cache is stored in a single file so that multiple processes can
|
||||
share the same cache. It locks the file whenever accessing to the
|
||||
file. When the cache content is corrupted, it will be initialized with
|
||||
an empty cache.
|
||||
"""
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
try:
|
||||
from oauth2client.contrib.locked_file import LockedFile
|
||||
except ImportError:
|
||||
# oauth2client < 2.0.0
|
||||
try:
|
||||
from oauth2client.locked_file import LockedFile
|
||||
except ImportError:
|
||||
# oauth2client > 4.0.0 or google-auth
|
||||
raise ImportError(
|
||||
"file_cache is unavailable when using oauth2client >= 4.0.0 or google-auth"
|
||||
)
|
||||
|
||||
from . import base
|
||||
from ..discovery_cache import DISCOVERY_DOC_MAX_AGE
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
FILENAME = "google-api-python-client-discovery-doc.cache"
|
||||
EPOCH = datetime.datetime(1970, 1, 1)
|
||||
|
||||
|
||||
def _to_timestamp(date):
|
||||
try:
|
||||
return (date - EPOCH).total_seconds()
|
||||
except AttributeError:
|
||||
# The following is the equivalent of total_seconds() in Python2.6.
|
||||
# See also: https://docs.python.org/2/library/datetime.html
|
||||
delta = date - EPOCH
|
||||
return (
|
||||
delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 10**6
|
||||
) / 10**6
|
||||
|
||||
|
||||
def _read_or_initialize_cache(f):
|
||||
f.file_handle().seek(0)
|
||||
try:
|
||||
cache = json.load(f.file_handle())
|
||||
except Exception:
|
||||
# This means it opens the file for the first time, or the cache is
|
||||
# corrupted, so initializing the file with an empty dict.
|
||||
cache = {}
|
||||
f.file_handle().truncate(0)
|
||||
f.file_handle().seek(0)
|
||||
json.dump(cache, f.file_handle())
|
||||
return cache
|
||||
|
||||
|
||||
class Cache(base.Cache):
|
||||
"""A file based cache for the discovery documents."""
|
||||
|
||||
def __init__(self, max_age):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
max_age: Cache expiration in seconds.
|
||||
"""
|
||||
self._max_age = max_age
|
||||
self._file = os.path.join(tempfile.gettempdir(), FILENAME)
|
||||
f = LockedFile(self._file, "a+", "r")
|
||||
try:
|
||||
f.open_and_lock()
|
||||
if f.is_locked():
|
||||
_read_or_initialize_cache(f)
|
||||
# If we can not obtain the lock, other process or thread must
|
||||
# have initialized the file.
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
finally:
|
||||
f.unlock_and_close()
|
||||
|
||||
def get(self, url):
|
||||
f = LockedFile(self._file, "r+", "r")
|
||||
try:
|
||||
f.open_and_lock()
|
||||
if f.is_locked():
|
||||
cache = _read_or_initialize_cache(f)
|
||||
if url in cache:
|
||||
content, t = cache.get(url, (None, 0))
|
||||
if _to_timestamp(datetime.datetime.now()) < t + self._max_age:
|
||||
return content
|
||||
return None
|
||||
else:
|
||||
LOGGER.debug("Could not obtain a lock for the cache file.")
|
||||
return None
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
finally:
|
||||
f.unlock_and_close()
|
||||
|
||||
def set(self, url, content):
|
||||
f = LockedFile(self._file, "r+", "r")
|
||||
try:
|
||||
f.open_and_lock()
|
||||
if f.is_locked():
|
||||
cache = _read_or_initialize_cache(f)
|
||||
cache[url] = (content, _to_timestamp(datetime.datetime.now()))
|
||||
# Remove stale cache.
|
||||
for k, (_, timestamp) in list(cache.items()):
|
||||
if (
|
||||
_to_timestamp(datetime.datetime.now())
|
||||
>= timestamp + self._max_age
|
||||
):
|
||||
del cache[k]
|
||||
f.file_handle().truncate(0)
|
||||
f.file_handle().seek(0)
|
||||
json.dump(cache, f.file_handle())
|
||||
else:
|
||||
LOGGER.debug("Could not obtain a lock for the cache file.")
|
||||
except Exception as e:
|
||||
LOGGER.warning(e, exc_info=True)
|
||||
finally:
|
||||
f.unlock_and_close()
|
||||
|
||||
|
||||
cache = Cache(max_age=DISCOVERY_DOC_MAX_AGE)
|
||||
197
src/gam/googleapiclient/errors.py
Normal file
197
src/gam/googleapiclient/errors.py
Normal file
@@ -0,0 +1,197 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Errors for the library.
|
||||
|
||||
All exceptions defined by the library
|
||||
should be defined in this file.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "jcgregorio@google.com (Joe Gregorio)"
|
||||
|
||||
import json
|
||||
|
||||
from googleapiclient import _helpers as util
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Base error for this module."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class HttpError(Error):
|
||||
"""HTTP data was invalid or unexpected."""
|
||||
|
||||
@util.positional(3)
|
||||
def __init__(self, resp, content, uri=None):
|
||||
self.resp = resp
|
||||
if not isinstance(content, bytes):
|
||||
raise TypeError("HTTP content should be bytes")
|
||||
self.content = content
|
||||
self.uri = uri
|
||||
self.error_details = ""
|
||||
self.reason = self._get_reason()
|
||||
|
||||
@property
|
||||
def status_code(self):
|
||||
"""Return the HTTP status code from the response content."""
|
||||
return self.resp.status
|
||||
|
||||
def _get_reason(self):
|
||||
"""Calculate the reason for the error from the response content."""
|
||||
reason = self.resp.reason
|
||||
try:
|
||||
try:
|
||||
data = json.loads(self.content.decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
# In case it is not json
|
||||
data = self.content.decode("utf-8")
|
||||
if isinstance(data, dict):
|
||||
reason = data["error"]["message"]
|
||||
error_detail_keyword = next(
|
||||
(
|
||||
kw
|
||||
for kw in ["detail", "details", "errors", "message"]
|
||||
if kw in data["error"]
|
||||
),
|
||||
"",
|
||||
)
|
||||
if error_detail_keyword:
|
||||
self.error_details = data["error"][error_detail_keyword]
|
||||
elif isinstance(data, list) and len(data) > 0:
|
||||
first_error = data[0]
|
||||
reason = first_error["error"]["message"]
|
||||
if "details" in first_error["error"]:
|
||||
self.error_details = first_error["error"]["details"]
|
||||
else:
|
||||
self.error_details = data
|
||||
except (ValueError, KeyError, TypeError):
|
||||
pass
|
||||
if reason is None:
|
||||
reason = ""
|
||||
return reason.strip()
|
||||
|
||||
def __repr__(self):
|
||||
if self.error_details:
|
||||
return '<HttpError %s when requesting %s returned "%s". Details: "%s">' % (
|
||||
self.resp.status,
|
||||
self.uri,
|
||||
self.reason,
|
||||
self.error_details,
|
||||
)
|
||||
elif self.uri:
|
||||
return '<HttpError %s when requesting %s returned "%s">' % (
|
||||
self.resp.status,
|
||||
self.uri,
|
||||
self.reason,
|
||||
)
|
||||
else:
|
||||
return '<HttpError %s "%s">' % (self.resp.status, self.reason)
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
|
||||
class InvalidJsonError(Error):
|
||||
"""The JSON returned could not be parsed."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnknownFileType(Error):
|
||||
"""File type unknown or unexpected."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnknownLinkType(Error):
|
||||
"""Link type unknown or unexpected."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnknownApiNameOrVersion(Error):
|
||||
"""No API with that name and version exists."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnacceptableMimeTypeError(Error):
|
||||
"""That is an unacceptable mimetype for this operation."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MediaUploadSizeError(Error):
|
||||
"""Media is larger than the method can accept."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ResumableUploadError(HttpError):
|
||||
"""Error occurred during resumable upload."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidChunkSizeError(Error):
|
||||
"""The given chunksize is not valid."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidNotificationError(Error):
|
||||
"""The channel Notification is invalid."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BatchError(HttpError):
|
||||
"""Error occurred during batch operations."""
|
||||
|
||||
@util.positional(2)
|
||||
def __init__(self, reason, resp=None, content=None):
|
||||
self.resp = resp
|
||||
self.content = content
|
||||
self.reason = reason
|
||||
|
||||
def __repr__(self):
|
||||
if getattr(self.resp, "status", None) is None:
|
||||
return '<BatchError "%s">' % (self.reason)
|
||||
else:
|
||||
return '<BatchError %s "%s">' % (self.resp.status, self.reason)
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
|
||||
class UnexpectedMethodError(Error):
|
||||
"""Exception raised by RequestMockBuilder on unexpected calls."""
|
||||
|
||||
@util.positional(1)
|
||||
def __init__(self, methodId=None):
|
||||
"""Constructor for an UnexpectedMethodError."""
|
||||
super(UnexpectedMethodError, self).__init__(
|
||||
"Received unexpected call %s" % methodId
|
||||
)
|
||||
|
||||
|
||||
class UnexpectedBodyError(Error):
|
||||
"""Exception raised by RequestMockBuilder on unexpected bodies."""
|
||||
|
||||
def __init__(self, expected, provided):
|
||||
"""Constructor for an UnexpectedMethodError."""
|
||||
super(UnexpectedBodyError, self).__init__(
|
||||
"Expected: [%s] - Provided: [%s]" % (expected, provided)
|
||||
)
|
||||
1962
src/gam/googleapiclient/http.py
Normal file
1962
src/gam/googleapiclient/http.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,14 +22,14 @@ Contents:
|
||||
from a list of candidates.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
from functools import reduce
|
||||
import six
|
||||
|
||||
__version__ = '0.1.3'
|
||||
__author__ = 'Joe Gregorio'
|
||||
__email__ = 'joe@bitworking.org'
|
||||
__license__ = 'MIT License'
|
||||
__credits__ = ''
|
||||
from functools import reduce
|
||||
|
||||
__version__ = "0.1.3"
|
||||
__author__ = "Joe Gregorio"
|
||||
__email__ = "joe@bitworking.org"
|
||||
__license__ = "MIT License"
|
||||
__credits__ = ""
|
||||
|
||||
|
||||
def parse_mime_type(mime_type):
|
||||
@@ -41,17 +41,17 @@ def parse_mime_type(mime_type):
|
||||
into:
|
||||
|
||||
('application', 'xhtml', {'q', '0.5'})
|
||||
"""
|
||||
parts = mime_type.split(';')
|
||||
params = dict([tuple([s.strip() for s in param.split('=', 1)])\
|
||||
for param in parts[1:]
|
||||
])
|
||||
"""
|
||||
parts = mime_type.split(";")
|
||||
params = dict(
|
||||
[tuple([s.strip() for s in param.split("=", 1)]) for param in parts[1:]]
|
||||
)
|
||||
full_type = parts[0].strip()
|
||||
# Java URLConnection class sends an Accept header that includes a
|
||||
# single '*'. Turn it into a legal wildcard.
|
||||
if full_type == '*':
|
||||
full_type = '*/*'
|
||||
(type, subtype) = full_type.split('/')
|
||||
if full_type == "*":
|
||||
full_type = "*/*"
|
||||
(type, subtype) = full_type.split("/")
|
||||
|
||||
return (type.strip(), subtype.strip(), params)
|
||||
|
||||
@@ -71,10 +71,14 @@ def parse_media_range(range):
|
||||
necessary.
|
||||
"""
|
||||
(type, subtype, params) = parse_mime_type(range)
|
||||
if 'q' not in params or not params['q'] or \
|
||||
not float(params['q']) or float(params['q']) > 1\
|
||||
or float(params['q']) < 0:
|
||||
params['q'] = '1'
|
||||
if (
|
||||
"q" not in params
|
||||
or not params["q"]
|
||||
or not float(params["q"])
|
||||
or float(params["q"]) > 1
|
||||
or float(params["q"]) < 0
|
||||
):
|
||||
params["q"] = "1"
|
||||
|
||||
return (type, subtype, params)
|
||||
|
||||
@@ -90,25 +94,28 @@ def fitness_and_quality_parsed(mime_type, parsed_ranges):
|
||||
"""
|
||||
best_fitness = -1
|
||||
best_fit_q = 0
|
||||
(target_type, target_subtype, target_params) =\
|
||||
parse_media_range(mime_type)
|
||||
(target_type, target_subtype, target_params) = parse_media_range(mime_type)
|
||||
for (type, subtype, params) in parsed_ranges:
|
||||
type_match = (type == target_type or\
|
||||
type == '*' or\
|
||||
target_type == '*')
|
||||
subtype_match = (subtype == target_subtype or\
|
||||
subtype == '*' or\
|
||||
target_subtype == '*')
|
||||
type_match = type == target_type or type == "*" or target_type == "*"
|
||||
subtype_match = (
|
||||
subtype == target_subtype or subtype == "*" or target_subtype == "*"
|
||||
)
|
||||
if type_match and subtype_match:
|
||||
param_matches = reduce(lambda x, y: x + y, [1 for (key, value) in \
|
||||
six.iteritems(target_params) if key != 'q' and \
|
||||
key in params and value == params[key]], 0)
|
||||
param_matches = reduce(
|
||||
lambda x, y: x + y,
|
||||
[
|
||||
1
|
||||
for (key, value) in target_params.items()
|
||||
if key != "q" and key in params and value == params[key]
|
||||
],
|
||||
0,
|
||||
)
|
||||
fitness = (type == target_type) and 100 or 0
|
||||
fitness += (subtype == target_subtype) and 10 or 0
|
||||
fitness += param_matches
|
||||
if fitness > best_fitness:
|
||||
best_fitness = fitness
|
||||
best_fit_q = params['q']
|
||||
best_fit_q = params["q"]
|
||||
|
||||
return best_fitness, float(best_fit_q)
|
||||
|
||||
@@ -137,7 +144,7 @@ def quality(mime_type, ranges):
|
||||
0.7
|
||||
|
||||
"""
|
||||
parsed_ranges = [parse_media_range(r) for r in ranges.split(',')]
|
||||
parsed_ranges = [parse_media_range(r) for r in ranges.split(",")]
|
||||
|
||||
return quality_parsed(mime_type, parsed_ranges)
|
||||
|
||||
@@ -156,17 +163,18 @@ def best_match(supported, header):
|
||||
'text/*;q=0.5,*/*; q=0.1')
|
||||
'text/xml'
|
||||
"""
|
||||
split_header = _filter_blank(header.split(','))
|
||||
split_header = _filter_blank(header.split(","))
|
||||
parsed_header = [parse_media_range(r) for r in split_header]
|
||||
weighted_matches = []
|
||||
pos = 0
|
||||
for mime_type in supported:
|
||||
weighted_matches.append((fitness_and_quality_parsed(mime_type,
|
||||
parsed_header), pos, mime_type))
|
||||
weighted_matches.append(
|
||||
(fitness_and_quality_parsed(mime_type, parsed_header), pos, mime_type)
|
||||
)
|
||||
pos += 1
|
||||
weighted_matches.sort()
|
||||
|
||||
return weighted_matches[-1][0][1] and weighted_matches[-1][2] or ''
|
||||
return weighted_matches[-1][0][1] and weighted_matches[-1][2] or ""
|
||||
|
||||
|
||||
def _filter_blank(i):
|
||||
429
src/gam/googleapiclient/model.py
Normal file
429
src/gam/googleapiclient/model.py
Normal file
@@ -0,0 +1,429 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Model objects for requests and responses.
|
||||
|
||||
Each API may support one or more serializations, such
|
||||
as JSON, Atom, etc. The model classes are responsible
|
||||
for converting between the wire format and the Python
|
||||
object representation.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "jcgregorio@google.com (Joe Gregorio)"
|
||||
|
||||
import json
|
||||
import logging
|
||||
import platform
|
||||
import urllib
|
||||
import warnings
|
||||
|
||||
from googleapiclient import version as googleapiclient_version
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
try:
|
||||
from google.api_core.version_header import API_VERSION_METADATA_KEY
|
||||
|
||||
HAS_API_VERSION = True
|
||||
except ImportError:
|
||||
HAS_API_VERSION = False
|
||||
|
||||
_LIBRARY_VERSION = googleapiclient_version.__version__
|
||||
_PY_VERSION = platform.python_version()
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
dump_request_response = False
|
||||
|
||||
|
||||
def _abstract():
|
||||
raise NotImplementedError("You need to override this function")
|
||||
|
||||
|
||||
class Model(object):
|
||||
"""Model base class.
|
||||
|
||||
All Model classes should implement this interface.
|
||||
The Model serializes and de-serializes between a wire
|
||||
format such as JSON and a Python object representation.
|
||||
"""
|
||||
|
||||
def request(self, headers, path_params, query_params, body_value):
|
||||
"""Updates outgoing requests with a serialized body.
|
||||
|
||||
Args:
|
||||
headers: dict, request headers
|
||||
path_params: dict, parameters that appear in the request path
|
||||
query_params: dict, parameters that appear in the query
|
||||
body_value: object, the request body as a Python object, which must be
|
||||
serializable.
|
||||
Returns:
|
||||
A tuple of (headers, path_params, query, body)
|
||||
|
||||
headers: dict, request headers
|
||||
path_params: dict, parameters that appear in the request path
|
||||
query: string, query part of the request URI
|
||||
body: string, the body serialized in the desired wire format.
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
def response(self, resp, content):
|
||||
"""Convert the response wire format into a Python object.
|
||||
|
||||
Args:
|
||||
resp: httplib2.Response, the HTTP response headers and status
|
||||
content: string, the body of the HTTP response
|
||||
|
||||
Returns:
|
||||
The body de-serialized as a Python object.
|
||||
|
||||
Raises:
|
||||
googleapiclient.errors.HttpError if a non 2xx response is received.
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
|
||||
class BaseModel(Model):
|
||||
"""Base model class.
|
||||
|
||||
Subclasses should provide implementations for the "serialize" and
|
||||
"deserialize" methods, as well as values for the following class attributes.
|
||||
|
||||
Attributes:
|
||||
accept: The value to use for the HTTP Accept header.
|
||||
content_type: The value to use for the HTTP Content-type header.
|
||||
no_content_response: The value to return when deserializing a 204 "No
|
||||
Content" response.
|
||||
alt_param: The value to supply as the "alt" query parameter for requests.
|
||||
"""
|
||||
|
||||
accept = None
|
||||
content_type = None
|
||||
no_content_response = None
|
||||
alt_param = None
|
||||
|
||||
def _log_request(self, headers, path_params, query, body):
|
||||
"""Logs debugging information about the request if requested."""
|
||||
if dump_request_response:
|
||||
LOGGER.info("--request-start--")
|
||||
LOGGER.info("-headers-start-")
|
||||
for h, v in headers.items():
|
||||
LOGGER.info("%s: %s", h, v)
|
||||
LOGGER.info("-headers-end-")
|
||||
LOGGER.info("-path-parameters-start-")
|
||||
for h, v in path_params.items():
|
||||
LOGGER.info("%s: %s", h, v)
|
||||
LOGGER.info("-path-parameters-end-")
|
||||
LOGGER.info("body: %s", body)
|
||||
LOGGER.info("query: %s", query)
|
||||
LOGGER.info("--request-end--")
|
||||
|
||||
def request(self, headers, path_params, query_params, body_value, api_version=None):
|
||||
"""Updates outgoing requests with a serialized body.
|
||||
|
||||
Args:
|
||||
headers: dict, request headers
|
||||
path_params: dict, parameters that appear in the request path
|
||||
query_params: dict, parameters that appear in the query
|
||||
body_value: object, the request body as a Python object, which must be
|
||||
serializable by json.
|
||||
api_version: str, The precise API version represented by this request,
|
||||
which will result in an API Version header being sent along with the
|
||||
HTTP request.
|
||||
Returns:
|
||||
A tuple of (headers, path_params, query, body)
|
||||
|
||||
headers: dict, request headers
|
||||
path_params: dict, parameters that appear in the request path
|
||||
query: string, query part of the request URI
|
||||
body: string, the body serialized as JSON
|
||||
"""
|
||||
query = self._build_query(query_params)
|
||||
headers["accept"] = self.accept
|
||||
headers["accept-encoding"] = "gzip, deflate"
|
||||
if "user-agent" in headers:
|
||||
headers["user-agent"] += " "
|
||||
else:
|
||||
headers["user-agent"] = ""
|
||||
headers["user-agent"] += "(gzip)"
|
||||
if "x-goog-api-client" in headers:
|
||||
headers["x-goog-api-client"] += " "
|
||||
else:
|
||||
headers["x-goog-api-client"] = ""
|
||||
headers["x-goog-api-client"] += "gdcl/%s gl-python/%s" % (
|
||||
_LIBRARY_VERSION,
|
||||
_PY_VERSION,
|
||||
)
|
||||
|
||||
if api_version and HAS_API_VERSION:
|
||||
headers[API_VERSION_METADATA_KEY] = api_version
|
||||
elif api_version:
|
||||
warnings.warn(
|
||||
"The `api_version` argument is ignored as a newer version of "
|
||||
"`google-api-core` is required to use this feature."
|
||||
"Please upgrade `google-api-core` to 2.19.0 or newer."
|
||||
)
|
||||
|
||||
if body_value is not None:
|
||||
headers["content-type"] = self.content_type
|
||||
body_value = self.serialize(body_value)
|
||||
self._log_request(headers, path_params, query, body_value)
|
||||
return (headers, path_params, query, body_value)
|
||||
|
||||
def _build_query(self, params):
|
||||
"""Builds a query string.
|
||||
|
||||
Args:
|
||||
params: dict, the query parameters
|
||||
|
||||
Returns:
|
||||
The query parameters properly encoded into an HTTP URI query string.
|
||||
"""
|
||||
if self.alt_param is not None:
|
||||
params.update({"alt": self.alt_param})
|
||||
astuples = []
|
||||
for key, value in params.items():
|
||||
if type(value) == type([]):
|
||||
for x in value:
|
||||
x = x.encode("utf-8")
|
||||
astuples.append((key, x))
|
||||
else:
|
||||
if isinstance(value, str) and callable(value.encode):
|
||||
value = value.encode("utf-8")
|
||||
astuples.append((key, value))
|
||||
return "?" + urllib.parse.urlencode(astuples)
|
||||
|
||||
def _log_response(self, resp, content):
|
||||
"""Logs debugging information about the response if requested."""
|
||||
if dump_request_response:
|
||||
LOGGER.info("--response-start--")
|
||||
for h, v in resp.items():
|
||||
LOGGER.info("%s: %s", h, v)
|
||||
if content:
|
||||
LOGGER.info(content)
|
||||
LOGGER.info("--response-end--")
|
||||
|
||||
def response(self, resp, content):
|
||||
"""Convert the response wire format into a Python object.
|
||||
|
||||
Args:
|
||||
resp: httplib2.Response, the HTTP response headers and status
|
||||
content: string, the body of the HTTP response
|
||||
|
||||
Returns:
|
||||
The body de-serialized as a Python object.
|
||||
|
||||
Raises:
|
||||
googleapiclient.errors.HttpError if a non 2xx response is received.
|
||||
"""
|
||||
self._log_response(resp, content)
|
||||
# Error handling is TBD, for example, do we retry
|
||||
# for some operation/error combinations?
|
||||
if resp.status < 300:
|
||||
if resp.status == 204:
|
||||
# A 204: No Content response should be treated differently
|
||||
# to all the other success states
|
||||
return self.no_content_response
|
||||
return self.deserialize(content)
|
||||
else:
|
||||
LOGGER.debug("Content from bad request was: %r" % content)
|
||||
raise HttpError(resp, content)
|
||||
|
||||
def serialize(self, body_value):
|
||||
"""Perform the actual Python object serialization.
|
||||
|
||||
Args:
|
||||
body_value: object, the request body as a Python object.
|
||||
|
||||
Returns:
|
||||
string, the body in serialized form.
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
def deserialize(self, content):
|
||||
"""Perform the actual deserialization from response string to Python
|
||||
object.
|
||||
|
||||
Args:
|
||||
content: string, the body of the HTTP response
|
||||
|
||||
Returns:
|
||||
The body de-serialized as a Python object.
|
||||
"""
|
||||
_abstract()
|
||||
|
||||
|
||||
class JsonModel(BaseModel):
|
||||
"""Model class for JSON.
|
||||
|
||||
Serializes and de-serializes between JSON and the Python
|
||||
object representation of HTTP request and response bodies.
|
||||
"""
|
||||
|
||||
accept = "application/json"
|
||||
content_type = "application/json"
|
||||
alt_param = "json"
|
||||
|
||||
def __init__(self, data_wrapper=False):
|
||||
"""Construct a JsonModel.
|
||||
|
||||
Args:
|
||||
data_wrapper: boolean, wrap requests and responses in a data wrapper
|
||||
"""
|
||||
self._data_wrapper = data_wrapper
|
||||
|
||||
def serialize(self, body_value):
|
||||
if (
|
||||
isinstance(body_value, dict)
|
||||
and "data" not in body_value
|
||||
and self._data_wrapper
|
||||
):
|
||||
body_value = {"data": body_value}
|
||||
return json.dumps(body_value)
|
||||
|
||||
def deserialize(self, content):
|
||||
try:
|
||||
content = content.decode("utf-8")
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
body = json.loads(content)
|
||||
except json.decoder.JSONDecodeError:
|
||||
body = content
|
||||
else:
|
||||
if self._data_wrapper and "data" in body:
|
||||
body = body["data"]
|
||||
return body
|
||||
|
||||
@property
|
||||
def no_content_response(self):
|
||||
return {}
|
||||
|
||||
|
||||
class RawModel(JsonModel):
|
||||
"""Model class for requests that don't return JSON.
|
||||
|
||||
Serializes and de-serializes between JSON and the Python
|
||||
object representation of HTTP request, and returns the raw bytes
|
||||
of the response body.
|
||||
"""
|
||||
|
||||
accept = "*/*"
|
||||
content_type = "application/json"
|
||||
alt_param = None
|
||||
|
||||
def deserialize(self, content):
|
||||
return content
|
||||
|
||||
@property
|
||||
def no_content_response(self):
|
||||
return ""
|
||||
|
||||
|
||||
class MediaModel(JsonModel):
|
||||
"""Model class for requests that return Media.
|
||||
|
||||
Serializes and de-serializes between JSON and the Python
|
||||
object representation of HTTP request, and returns the raw bytes
|
||||
of the response body.
|
||||
"""
|
||||
|
||||
accept = "*/*"
|
||||
content_type = "application/json"
|
||||
alt_param = "media"
|
||||
|
||||
def deserialize(self, content):
|
||||
return content
|
||||
|
||||
@property
|
||||
def no_content_response(self):
|
||||
return ""
|
||||
|
||||
|
||||
class ProtocolBufferModel(BaseModel):
|
||||
"""Model class for protocol buffers.
|
||||
|
||||
Serializes and de-serializes the binary protocol buffer sent in the HTTP
|
||||
request and response bodies.
|
||||
"""
|
||||
|
||||
accept = "application/x-protobuf"
|
||||
content_type = "application/x-protobuf"
|
||||
alt_param = "proto"
|
||||
|
||||
def __init__(self, protocol_buffer):
|
||||
"""Constructs a ProtocolBufferModel.
|
||||
|
||||
The serialized protocol buffer returned in an HTTP response will be
|
||||
de-serialized using the given protocol buffer class.
|
||||
|
||||
Args:
|
||||
protocol_buffer: The protocol buffer class used to de-serialize a
|
||||
response from the API.
|
||||
"""
|
||||
self._protocol_buffer = protocol_buffer
|
||||
|
||||
def serialize(self, body_value):
|
||||
return body_value.SerializeToString()
|
||||
|
||||
def deserialize(self, content):
|
||||
return self._protocol_buffer.FromString(content)
|
||||
|
||||
@property
|
||||
def no_content_response(self):
|
||||
return self._protocol_buffer()
|
||||
|
||||
|
||||
def makepatch(original, modified):
|
||||
"""Create a patch object.
|
||||
|
||||
Some methods support PATCH, an efficient way to send updates to a resource.
|
||||
This method allows the easy construction of patch bodies by looking at the
|
||||
differences between a resource before and after it was modified.
|
||||
|
||||
Args:
|
||||
original: object, the original deserialized resource
|
||||
modified: object, the modified deserialized resource
|
||||
Returns:
|
||||
An object that contains only the changes from original to modified, in a
|
||||
form suitable to pass to a PATCH method.
|
||||
|
||||
Example usage:
|
||||
item = service.activities().get(postid=postid, userid=userid).execute()
|
||||
original = copy.deepcopy(item)
|
||||
item['object']['content'] = 'This is updated.'
|
||||
service.activities.patch(postid=postid, userid=userid,
|
||||
body=makepatch(original, item)).execute()
|
||||
"""
|
||||
patch = {}
|
||||
for key, original_value in original.items():
|
||||
modified_value = modified.get(key, None)
|
||||
if modified_value is None:
|
||||
# Use None to signal that the element is deleted
|
||||
patch[key] = None
|
||||
elif original_value != modified_value:
|
||||
if type(original_value) == type({}):
|
||||
# Recursively descend objects
|
||||
patch[key] = makepatch(original_value, modified_value)
|
||||
else:
|
||||
# In the case of simple types or arrays we just replace
|
||||
patch[key] = modified_value
|
||||
else:
|
||||
# Don't add anything to patch if there's no change
|
||||
pass
|
||||
for key in modified:
|
||||
if key not in original:
|
||||
patch[key] = modified[key]
|
||||
|
||||
return patch
|
||||
317
src/gam/googleapiclient/schema.py
Normal file
317
src/gam/googleapiclient/schema.py
Normal file
@@ -0,0 +1,317 @@
|
||||
# Copyright 2014 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Schema processing for discovery based APIs
|
||||
|
||||
Schemas holds an APIs discovery schemas. It can return those schema as
|
||||
deserialized JSON objects, or pretty print them as prototype objects that
|
||||
conform to the schema.
|
||||
|
||||
For example, given the schema:
|
||||
|
||||
schema = \"\"\"{
|
||||
"Foo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"etag": {
|
||||
"type": "string",
|
||||
"description": "ETag of the collection."
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"description": "Type of the collection ('calendar#acl').",
|
||||
"default": "calendar#acl"
|
||||
},
|
||||
"nextPageToken": {
|
||||
"type": "string",
|
||||
"description": "Token used to access the next
|
||||
page of this result. Omitted if no further results are available."
|
||||
}
|
||||
}
|
||||
}
|
||||
}\"\"\"
|
||||
|
||||
s = Schemas(schema)
|
||||
print s.prettyPrintByName('Foo')
|
||||
|
||||
Produces the following output:
|
||||
|
||||
{
|
||||
"nextPageToken": "A String", # Token used to access the
|
||||
# next page of this result. Omitted if no further results are available.
|
||||
"kind": "A String", # Type of the collection ('calendar#acl').
|
||||
"etag": "A String", # ETag of the collection.
|
||||
},
|
||||
|
||||
The constructor takes a discovery document in which to look up named schema.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
# TODO(jcgregorio) support format, enum, minimum, maximum
|
||||
|
||||
__author__ = "jcgregorio@google.com (Joe Gregorio)"
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from googleapiclient import _helpers as util
|
||||
|
||||
|
||||
class Schemas(object):
|
||||
"""Schemas for an API."""
|
||||
|
||||
def __init__(self, discovery):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
discovery: object, Deserialized discovery document from which we pull
|
||||
out the named schema.
|
||||
"""
|
||||
self.schemas = discovery.get("schemas", {})
|
||||
|
||||
# Cache of pretty printed schemas.
|
||||
self.pretty = {}
|
||||
|
||||
@util.positional(2)
|
||||
def _prettyPrintByName(self, name, seen=None, dent=0):
|
||||
"""Get pretty printed object prototype from the schema name.
|
||||
|
||||
Args:
|
||||
name: string, Name of schema in the discovery document.
|
||||
seen: list of string, Names of schema already seen. Used to handle
|
||||
recursive definitions.
|
||||
|
||||
Returns:
|
||||
string, A string that contains a prototype object with
|
||||
comments that conforms to the given schema.
|
||||
"""
|
||||
if seen is None:
|
||||
seen = []
|
||||
|
||||
if name in seen:
|
||||
# Do not fall into an infinite loop over recursive definitions.
|
||||
return "# Object with schema name: %s" % name
|
||||
seen.append(name)
|
||||
|
||||
if name not in self.pretty:
|
||||
self.pretty[name] = _SchemaToStruct(
|
||||
self.schemas[name], seen, dent=dent
|
||||
).to_str(self._prettyPrintByName)
|
||||
|
||||
seen.pop()
|
||||
|
||||
return self.pretty[name]
|
||||
|
||||
def prettyPrintByName(self, name):
|
||||
"""Get pretty printed object prototype from the schema name.
|
||||
|
||||
Args:
|
||||
name: string, Name of schema in the discovery document.
|
||||
|
||||
Returns:
|
||||
string, A string that contains a prototype object with
|
||||
comments that conforms to the given schema.
|
||||
"""
|
||||
# Return with trailing comma and newline removed.
|
||||
return self._prettyPrintByName(name, seen=[], dent=0)[:-2]
|
||||
|
||||
@util.positional(2)
|
||||
def _prettyPrintSchema(self, schema, seen=None, dent=0):
|
||||
"""Get pretty printed object prototype of schema.
|
||||
|
||||
Args:
|
||||
schema: object, Parsed JSON schema.
|
||||
seen: list of string, Names of schema already seen. Used to handle
|
||||
recursive definitions.
|
||||
|
||||
Returns:
|
||||
string, A string that contains a prototype object with
|
||||
comments that conforms to the given schema.
|
||||
"""
|
||||
if seen is None:
|
||||
seen = []
|
||||
|
||||
return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName)
|
||||
|
||||
def prettyPrintSchema(self, schema):
|
||||
"""Get pretty printed object prototype of schema.
|
||||
|
||||
Args:
|
||||
schema: object, Parsed JSON schema.
|
||||
|
||||
Returns:
|
||||
string, A string that contains a prototype object with
|
||||
comments that conforms to the given schema.
|
||||
"""
|
||||
# Return with trailing comma and newline removed.
|
||||
return self._prettyPrintSchema(schema, dent=0)[:-2]
|
||||
|
||||
def get(self, name, default=None):
|
||||
"""Get deserialized JSON schema from the schema name.
|
||||
|
||||
Args:
|
||||
name: string, Schema name.
|
||||
default: object, return value if name not found.
|
||||
"""
|
||||
return self.schemas.get(name, default)
|
||||
|
||||
|
||||
class _SchemaToStruct(object):
|
||||
"""Convert schema to a prototype object."""
|
||||
|
||||
@util.positional(3)
|
||||
def __init__(self, schema, seen, dent=0):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
schema: object, Parsed JSON schema.
|
||||
seen: list, List of names of schema already seen while parsing. Used to
|
||||
handle recursive definitions.
|
||||
dent: int, Initial indentation depth.
|
||||
"""
|
||||
# The result of this parsing kept as list of strings.
|
||||
self.value = []
|
||||
|
||||
# The final value of the parsing.
|
||||
self.string = None
|
||||
|
||||
# The parsed JSON schema.
|
||||
self.schema = schema
|
||||
|
||||
# Indentation level.
|
||||
self.dent = dent
|
||||
|
||||
# Method that when called returns a prototype object for the schema with
|
||||
# the given name.
|
||||
self.from_cache = None
|
||||
|
||||
# List of names of schema already seen while parsing.
|
||||
self.seen = seen
|
||||
|
||||
def emit(self, text):
|
||||
"""Add text as a line to the output.
|
||||
|
||||
Args:
|
||||
text: string, Text to output.
|
||||
"""
|
||||
self.value.extend([" " * self.dent, text, "\n"])
|
||||
|
||||
def emitBegin(self, text):
|
||||
"""Add text to the output, but with no line terminator.
|
||||
|
||||
Args:
|
||||
text: string, Text to output.
|
||||
"""
|
||||
self.value.extend([" " * self.dent, text])
|
||||
|
||||
def emitEnd(self, text, comment):
|
||||
"""Add text and comment to the output with line terminator.
|
||||
|
||||
Args:
|
||||
text: string, Text to output.
|
||||
comment: string, Python comment.
|
||||
"""
|
||||
if comment:
|
||||
divider = "\n" + " " * (self.dent + 2) + "# "
|
||||
lines = comment.splitlines()
|
||||
lines = [x.rstrip() for x in lines]
|
||||
comment = divider.join(lines)
|
||||
self.value.extend([text, " # ", comment, "\n"])
|
||||
else:
|
||||
self.value.extend([text, "\n"])
|
||||
|
||||
def indent(self):
|
||||
"""Increase indentation level."""
|
||||
self.dent += 1
|
||||
|
||||
def undent(self):
|
||||
"""Decrease indentation level."""
|
||||
self.dent -= 1
|
||||
|
||||
def _to_str_impl(self, schema):
|
||||
"""Prototype object based on the schema, in Python code with comments.
|
||||
|
||||
Args:
|
||||
schema: object, Parsed JSON schema file.
|
||||
|
||||
Returns:
|
||||
Prototype object based on the schema, in Python code with comments.
|
||||
"""
|
||||
stype = schema.get("type")
|
||||
if stype == "object":
|
||||
self.emitEnd("{", schema.get("description", ""))
|
||||
self.indent()
|
||||
if "properties" in schema:
|
||||
properties = schema.get("properties", {})
|
||||
sorted_properties = OrderedDict(sorted(properties.items()))
|
||||
for pname, pschema in sorted_properties.items():
|
||||
self.emitBegin('"%s": ' % pname)
|
||||
self._to_str_impl(pschema)
|
||||
elif "additionalProperties" in schema:
|
||||
self.emitBegin('"a_key": ')
|
||||
self._to_str_impl(schema["additionalProperties"])
|
||||
self.undent()
|
||||
self.emit("},")
|
||||
elif "$ref" in schema:
|
||||
schemaName = schema["$ref"]
|
||||
description = schema.get("description", "")
|
||||
s = self.from_cache(schemaName, seen=self.seen)
|
||||
parts = s.splitlines()
|
||||
self.emitEnd(parts[0], description)
|
||||
for line in parts[1:]:
|
||||
self.emit(line.rstrip())
|
||||
elif stype == "boolean":
|
||||
value = schema.get("default", "True or False")
|
||||
self.emitEnd("%s," % str(value), schema.get("description", ""))
|
||||
elif stype == "string":
|
||||
value = schema.get("default", "A String")
|
||||
self.emitEnd('"%s",' % str(value), schema.get("description", ""))
|
||||
elif stype == "integer":
|
||||
value = schema.get("default", "42")
|
||||
self.emitEnd("%s," % str(value), schema.get("description", ""))
|
||||
elif stype == "number":
|
||||
value = schema.get("default", "3.14")
|
||||
self.emitEnd("%s," % str(value), schema.get("description", ""))
|
||||
elif stype == "null":
|
||||
self.emitEnd("None,", schema.get("description", ""))
|
||||
elif stype == "any":
|
||||
self.emitEnd('"",', schema.get("description", ""))
|
||||
elif stype == "array":
|
||||
self.emitEnd("[", schema.get("description"))
|
||||
self.indent()
|
||||
self.emitBegin("")
|
||||
self._to_str_impl(schema["items"])
|
||||
self.undent()
|
||||
self.emit("],")
|
||||
else:
|
||||
self.emit("Unknown type! %s" % stype)
|
||||
self.emitEnd("", "")
|
||||
|
||||
self.string = "".join(self.value)
|
||||
return self.string
|
||||
|
||||
def to_str(self, from_cache):
|
||||
"""Prototype object based on the schema, in Python code with comments.
|
||||
|
||||
Args:
|
||||
from_cache: callable(name, seen), Callable that retrieves an object
|
||||
prototype for a schema with the given name. Seen is a list of schema
|
||||
names already seen as we recursively descend the schema definition.
|
||||
|
||||
Returns:
|
||||
Prototype object based on the schema, in Python code with comments.
|
||||
The lines of the code will all be properly indented.
|
||||
"""
|
||||
self.from_cache = from_cache
|
||||
return self._to_str_impl(self.schema)
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
# Copyright 2021 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -12,15 +12,4 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Contains Django URL patterns used for OAuth2 flow."""
|
||||
|
||||
from django.conf import urls
|
||||
|
||||
from oauth2client.contrib.django_util import views
|
||||
|
||||
urlpatterns = [
|
||||
urls.url(r'oauth2callback/', views.oauth2_callback, name="callback"),
|
||||
urls.url(r'oauth2authorize/', views.oauth2_authorize, name="authorize")
|
||||
]
|
||||
|
||||
urls = (urlpatterns, "google_oauth", "google_oauth")
|
||||
__version__ = "2.146.0"
|
||||
28
src/gam/iso8601/__init__.py
Normal file
28
src/gam/iso8601/__init__.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2007 - 2015 Michael Twomey
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a
|
||||
# copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included
|
||||
# in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
"""ISO 8601 date time string parsing
|
||||
|
||||
"""
|
||||
|
||||
__all__ = ["parse_date", "ParseError", "UTC"]
|
||||
160
src/gam/iso8601/iso8601.py
Normal file
160
src/gam/iso8601/iso8601.py
Normal file
@@ -0,0 +1,160 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""ISO 8601 date time string parsing
|
||||
|
||||
"""
|
||||
|
||||
from datetime import (datetime, timedelta, tzinfo)
|
||||
import time as _time
|
||||
import re
|
||||
|
||||
ISO8601_REGEX = re.compile(r'^(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})(?P<separator>[ T])(?P<hour>[0-9]{2}):(?P<minute>[0-9]{2}):(?P<second>[0-9]{2})([.,](?P<second_fraction>[0-9]+)){0,1}(?P<timezone>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9]{2}):(?P<tz_minute>[0-9]{2}))$')
|
||||
ISO8601_TZ_REGEX = re.compile(r'^(?P<timezone>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9]{2}):(?P<tz_minute>[0-9]{2}))$')
|
||||
|
||||
class ParseError(Exception):
|
||||
"""Raised when there is a problem parsing a date string"""
|
||||
|
||||
# Yoinked from python docs
|
||||
ZERO = timedelta(0)
|
||||
class Utc(tzinfo):
|
||||
"""UTC Timezone
|
||||
|
||||
"""
|
||||
def utcoffset(self, dt):
|
||||
return ZERO
|
||||
|
||||
def tzname(self, dt):
|
||||
return "UTC"
|
||||
|
||||
def dst(self, dt):
|
||||
return ZERO
|
||||
|
||||
def __repr__(self):
|
||||
return "<iso8601.Utc>"
|
||||
|
||||
UTC = Utc()
|
||||
|
||||
class FixedOffset(tzinfo):
|
||||
"""Fixed offset in hours and minutes from UTC
|
||||
|
||||
"""
|
||||
def __init__(self, offset_hours, offset_minutes, name):
|
||||
self.__offset_hours = offset_hours # Keep for later __getinitargs__
|
||||
self.__offset_minutes = offset_minutes # Keep for later __getinitargs__
|
||||
self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes)
|
||||
self.__name = name
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, FixedOffset):
|
||||
return (other.__offset == self.__offset) and (other.__name == self.__name)
|
||||
if isinstance(other, tzinfo):
|
||||
return other == self
|
||||
return False
|
||||
|
||||
def __getinitargs__(self):
|
||||
return (self.__offset_hours, self.__offset_minutes, self.__name)
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return self.__offset
|
||||
|
||||
def tzname(self, dt):
|
||||
return self.__name
|
||||
|
||||
def dst(self, dt):
|
||||
return ZERO
|
||||
|
||||
def __repr__(self):
|
||||
return "<FixedOffset %r %r>" % (self.__name, self.__offset)
|
||||
|
||||
# A class capturing the platform's idea of local time.
|
||||
|
||||
STDOFFSET = timedelta(seconds = -_time.timezone)
|
||||
if _time.daylight:
|
||||
DSTOFFSET = timedelta(seconds = -_time.altzone)
|
||||
else:
|
||||
DSTOFFSET = STDOFFSET
|
||||
|
||||
DSTDIFF = DSTOFFSET - STDOFFSET
|
||||
|
||||
class LocalTimezone(tzinfo):
|
||||
"""Local time zone
|
||||
|
||||
"""
|
||||
|
||||
def utcoffset(self, dt):
|
||||
if self._isdst(dt):
|
||||
return DSTOFFSET
|
||||
else:
|
||||
return STDOFFSET
|
||||
|
||||
def dst(self, dt):
|
||||
if self._isdst(dt):
|
||||
return DSTDIFF
|
||||
else:
|
||||
return ZERO
|
||||
|
||||
def tzname(self, dt):
|
||||
return _time.tzname[self._isdst(dt)]
|
||||
|
||||
def _isdst(self, dt):
|
||||
tt = (dt.year, dt.month, dt.day,
|
||||
dt.hour, dt.minute, dt.second,
|
||||
dt.weekday(), 0, 0)
|
||||
stamp = _time.mktime(tt)
|
||||
tt = _time.localtime(stamp)
|
||||
return tt.tm_isdst > 0
|
||||
|
||||
Local = LocalTimezone()
|
||||
|
||||
def parse_timezone(matches):
|
||||
"""Parses ISO 8601 time zone specs into tzinfo offsets
|
||||
|
||||
"""
|
||||
|
||||
if matches["timezone"] == "Z":
|
||||
return UTC
|
||||
sign = matches["tz_sign"]
|
||||
hours = int(matches['tz_hour'])
|
||||
minutes = int(matches['tz_minute'])
|
||||
description = "%s%02d:%02d" % (sign, hours, minutes)
|
||||
if sign == "-":
|
||||
hours = -hours
|
||||
minutes = -minutes
|
||||
return FixedOffset(hours, minutes, description)
|
||||
|
||||
def parse_timezone_str(tzstring):
|
||||
m = ISO8601_TZ_REGEX.match(tzstring)
|
||||
if not m:
|
||||
raise ParseError("Unable to parse timezone string %r" % tzstring)
|
||||
groups = m.groupdict()
|
||||
return parse_timezone(groups)
|
||||
|
||||
def parse_date(datestring):
|
||||
"""Parses ISO 8601 dates into datetime objects
|
||||
|
||||
The timezone is parsed from the date string. However it is quite common to
|
||||
have dates without a timezone (not strictly correct). In this case the
|
||||
default timezone specified in default_timezone is used. This is UTC by
|
||||
default.
|
||||
|
||||
:param datestring: The date to parse as a string
|
||||
:returns: A datetime.datetime instance
|
||||
:raises: ParseError when there is a problem parsing the date or
|
||||
constructing the datetime instance.
|
||||
|
||||
"""
|
||||
m = ISO8601_REGEX.match(datestring)
|
||||
if not m:
|
||||
raise ParseError("Unable to parse date string %r" % datestring)
|
||||
groups = m.groupdict()
|
||||
tz = parse_timezone(groups)
|
||||
try:
|
||||
return (datetime(year=int(groups['year']),
|
||||
month=int(groups['month']),
|
||||
day=int(groups['day']),
|
||||
hour=int(groups['hour']),
|
||||
minute=int(groups['minute']),
|
||||
second=int(groups['second']),
|
||||
tzinfo=tz),
|
||||
tz)
|
||||
except Exception as e:
|
||||
raise ParseError(e)
|
||||
141
src/gam/serviceaccountlookup-v1.json
Normal file
141
src/gam/serviceaccountlookup-v1.json
Normal file
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"basePath": "",
|
||||
"baseUrl": "https://www.googleapis.com/service_accounts/v1",
|
||||
"canonicalName": "serviceaccountlookup",
|
||||
"description": "Pseudo-API to lookup public certificates for a service account anonymously",
|
||||
"discoveryVersion": "v1",
|
||||
"documentationLink": "https://example.com/",
|
||||
"fullyEncodeReservedExpansion": true,
|
||||
"icons": {
|
||||
"x16": "http://www.google.com/images/icons/product/search-16.gif",
|
||||
"x32": "http://www.google.com/images/icons/product/search-32.gif"
|
||||
},
|
||||
"id": "serviceaccountlookup:v1",
|
||||
"kind": "discovery#restDescription",
|
||||
"name": "serviceaccountlookup",
|
||||
"ownerDomain": "google.com",
|
||||
"ownerName": "Google",
|
||||
"packagePath": "admin",
|
||||
"parameters": {
|
||||
"$.xgafv": {
|
||||
"description": "V1 error format.",
|
||||
"enum": [
|
||||
"1",
|
||||
"2"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"v1 error format",
|
||||
"v2 error format"
|
||||
],
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"access_token": {
|
||||
"description": "OAuth access token.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"alt": {
|
||||
"default": "json",
|
||||
"description": "Data format for response.",
|
||||
"enum": [
|
||||
"json",
|
||||
"media",
|
||||
"proto"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Responses with Content-Type of application/json",
|
||||
"Media download with context-dependent Content-Type",
|
||||
"Responses with Content-Type of application/x-protobuf"
|
||||
],
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"callback": {
|
||||
"description": "JSONP",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"description": "Selector specifying which fields to include in a partial response.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"oauth_token": {
|
||||
"description": "OAuth 2.0 token for the current user.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"prettyPrint": {
|
||||
"default": "true",
|
||||
"description": "Returns response with indentations and line breaks.",
|
||||
"location": "query",
|
||||
"type": "boolean"
|
||||
},
|
||||
"quotaUser": {
|
||||
"description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters.",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"uploadType": {
|
||||
"description": "Legacy upload protocol for media (e.g. \"media\", \"multipart\").",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
},
|
||||
"upload_protocol": {
|
||||
"description": "Upload protocol for media (e.g. \"raw\", \"multipart\").",
|
||||
"location": "query",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"protocol": "rest",
|
||||
"resources": {
|
||||
"serviceaccounts": {
|
||||
"methods": {
|
||||
"lookup": {
|
||||
"description": "Lookup",
|
||||
"flatPath": "metadata/x509/{account}",
|
||||
"httpMethod": "GET",
|
||||
"id": "serviceaccountslookup.lookup",
|
||||
"parameterOrder": [
|
||||
"account"
|
||||
],
|
||||
"parameters": {
|
||||
"account": {
|
||||
"description": "Email or ID of the service account.",
|
||||
"location": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": "metadata/x509/{account}",
|
||||
"response": {
|
||||
"$ref": "Certificates"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rootUrl": "https://www.googleapis.com/service_accounts/v1",
|
||||
"schemas": {
|
||||
"Certificates": {
|
||||
"description": "JSON template for certificates.",
|
||||
"id": "Certificates",
|
||||
"properties": {
|
||||
"email": { "description": "Email of the delegate.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"servicePath": "",
|
||||
"title": "Service Account Lookup Pseudo-API",
|
||||
"version": "v1",
|
||||
"version_module": true
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
"""Utilities for writing code that runs on Python 2 and 3"""
|
||||
|
||||
# Copyright (c) 2010-2015 Benjamin Peterson
|
||||
# Copyright (c) 2010-2020 Benjamin Peterson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -20,6 +18,8 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
"""Utilities for writing code that runs on Python 2 and 3"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import functools
|
||||
@@ -29,7 +29,7 @@ import sys
|
||||
import types
|
||||
|
||||
__author__ = "Benjamin Peterson <benjamin@python.org>"
|
||||
__version__ = "1.10.0"
|
||||
__version__ = "1.15.0"
|
||||
|
||||
|
||||
# Useful for very coarse version differentiation.
|
||||
@@ -241,6 +241,7 @@ _moved_attributes = [
|
||||
MovedAttribute("map", "itertools", "builtins", "imap", "map"),
|
||||
MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"),
|
||||
MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"),
|
||||
MovedAttribute("getoutput", "commands", "subprocess"),
|
||||
MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
|
||||
MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"),
|
||||
MovedAttribute("reduce", "__builtin__", "functools"),
|
||||
@@ -254,18 +255,21 @@ _moved_attributes = [
|
||||
MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
|
||||
MovedModule("builtins", "__builtin__"),
|
||||
MovedModule("configparser", "ConfigParser"),
|
||||
MovedModule("collections_abc", "collections", "collections.abc" if sys.version_info >= (3, 3) else "collections"),
|
||||
MovedModule("copyreg", "copy_reg"),
|
||||
MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
|
||||
MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"),
|
||||
MovedModule("dbm_ndbm", "dbm", "dbm.ndbm"),
|
||||
MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread" if sys.version_info < (3, 9) else "_thread"),
|
||||
MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
|
||||
MovedModule("http_cookies", "Cookie", "http.cookies"),
|
||||
MovedModule("html_entities", "htmlentitydefs", "html.entities"),
|
||||
MovedModule("html_parser", "HTMLParser", "html.parser"),
|
||||
MovedModule("http_client", "httplib", "http.client"),
|
||||
MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
|
||||
MovedModule("email_mime_image", "email.MIMEImage", "email.mime.image"),
|
||||
MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
|
||||
MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"),
|
||||
MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
|
||||
MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
|
||||
MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
|
||||
MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
|
||||
MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
|
||||
@@ -337,10 +341,12 @@ _urllib_parse_moved_attributes = [
|
||||
MovedAttribute("quote_plus", "urllib", "urllib.parse"),
|
||||
MovedAttribute("unquote", "urllib", "urllib.parse"),
|
||||
MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
|
||||
MovedAttribute("unquote_to_bytes", "urllib", "urllib.parse", "unquote", "unquote_to_bytes"),
|
||||
MovedAttribute("urlencode", "urllib", "urllib.parse"),
|
||||
MovedAttribute("splitquery", "urllib", "urllib.parse"),
|
||||
MovedAttribute("splittag", "urllib", "urllib.parse"),
|
||||
MovedAttribute("splituser", "urllib", "urllib.parse"),
|
||||
MovedAttribute("splitvalue", "urllib", "urllib.parse"),
|
||||
MovedAttribute("uses_fragment", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("uses_netloc", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("uses_params", "urlparse", "urllib.parse"),
|
||||
@@ -416,6 +422,8 @@ _urllib_request_moved_attributes = [
|
||||
MovedAttribute("URLopener", "urllib", "urllib.request"),
|
||||
MovedAttribute("FancyURLopener", "urllib", "urllib.request"),
|
||||
MovedAttribute("proxy_bypass", "urllib", "urllib.request"),
|
||||
MovedAttribute("parse_http_list", "urllib2", "urllib.request"),
|
||||
MovedAttribute("parse_keqv_list", "urllib2", "urllib.request"),
|
||||
]
|
||||
for attr in _urllib_request_moved_attributes:
|
||||
setattr(Module_six_moves_urllib_request, attr.name, attr)
|
||||
@@ -631,13 +639,16 @@ if PY3:
|
||||
import io
|
||||
StringIO = io.StringIO
|
||||
BytesIO = io.BytesIO
|
||||
del io
|
||||
_assertCountEqual = "assertCountEqual"
|
||||
if sys.version_info[1] <= 1:
|
||||
_assertRaisesRegex = "assertRaisesRegexp"
|
||||
_assertRegex = "assertRegexpMatches"
|
||||
_assertNotRegex = "assertNotRegexpMatches"
|
||||
else:
|
||||
_assertRaisesRegex = "assertRaisesRegex"
|
||||
_assertRegex = "assertRegex"
|
||||
_assertNotRegex = "assertNotRegex"
|
||||
else:
|
||||
def b(s):
|
||||
return s
|
||||
@@ -659,6 +670,7 @@ else:
|
||||
_assertCountEqual = "assertItemsEqual"
|
||||
_assertRaisesRegex = "assertRaisesRegexp"
|
||||
_assertRegex = "assertRegexpMatches"
|
||||
_assertNotRegex = "assertNotRegexpMatches"
|
||||
_add_doc(b, """Byte literal""")
|
||||
_add_doc(u, """Text literal""")
|
||||
|
||||
@@ -675,15 +687,23 @@ def assertRegex(self, *args, **kwargs):
|
||||
return getattr(self, _assertRegex)(*args, **kwargs)
|
||||
|
||||
|
||||
def assertNotRegex(self, *args, **kwargs):
|
||||
return getattr(self, _assertNotRegex)(*args, **kwargs)
|
||||
|
||||
|
||||
if PY3:
|
||||
exec_ = getattr(moves.builtins, "exec")
|
||||
|
||||
def reraise(tp, value, tb=None):
|
||||
if value is None:
|
||||
value = tp()
|
||||
if value.__traceback__ is not tb:
|
||||
raise value.with_traceback(tb)
|
||||
raise value
|
||||
try:
|
||||
if value is None:
|
||||
value = tp()
|
||||
if value.__traceback__ is not tb:
|
||||
raise value.with_traceback(tb)
|
||||
raise value
|
||||
finally:
|
||||
value = None
|
||||
tb = None
|
||||
|
||||
else:
|
||||
def exec_(_code_, _globs_=None, _locs_=None):
|
||||
@@ -699,19 +719,19 @@ else:
|
||||
exec("""exec _code_ in _globs_, _locs_""")
|
||||
|
||||
exec_("""def reraise(tp, value, tb=None):
|
||||
raise tp, value, tb
|
||||
try:
|
||||
raise tp, value, tb
|
||||
finally:
|
||||
tb = None
|
||||
""")
|
||||
|
||||
|
||||
if sys.version_info[:2] == (3, 2):
|
||||
if sys.version_info[:2] > (3,):
|
||||
exec_("""def raise_from(value, from_value):
|
||||
if from_value is None:
|
||||
raise value
|
||||
raise value from from_value
|
||||
""")
|
||||
elif sys.version_info[:2] > (3, 2):
|
||||
exec_("""def raise_from(value, from_value):
|
||||
raise value from from_value
|
||||
try:
|
||||
raise value from from_value
|
||||
finally:
|
||||
value = None
|
||||
""")
|
||||
else:
|
||||
def raise_from(value, from_value):
|
||||
@@ -786,13 +806,33 @@ if sys.version_info[:2] < (3, 3):
|
||||
_add_doc(reraise, """Reraise an exception.""")
|
||||
|
||||
if sys.version_info[0:2] < (3, 4):
|
||||
# This does exactly the same what the :func:`py3:functools.update_wrapper`
|
||||
# function does on Python versions after 3.2. It sets the ``__wrapped__``
|
||||
# attribute on ``wrapper`` object and it doesn't raise an error if any of
|
||||
# the attributes mentioned in ``assigned`` and ``updated`` are missing on
|
||||
# ``wrapped`` object.
|
||||
def _update_wrapper(wrapper, wrapped,
|
||||
assigned=functools.WRAPPER_ASSIGNMENTS,
|
||||
updated=functools.WRAPPER_UPDATES):
|
||||
for attr in assigned:
|
||||
try:
|
||||
value = getattr(wrapped, attr)
|
||||
except AttributeError:
|
||||
continue
|
||||
else:
|
||||
setattr(wrapper, attr, value)
|
||||
for attr in updated:
|
||||
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
|
||||
wrapper.__wrapped__ = wrapped
|
||||
return wrapper
|
||||
_update_wrapper.__doc__ = functools.update_wrapper.__doc__
|
||||
|
||||
def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
|
||||
updated=functools.WRAPPER_UPDATES):
|
||||
def wrapper(f):
|
||||
f = functools.wraps(wrapped, assigned, updated)(f)
|
||||
f.__wrapped__ = wrapped
|
||||
return f
|
||||
return wrapper
|
||||
return functools.partial(_update_wrapper, wrapped=wrapped,
|
||||
assigned=assigned, updated=updated)
|
||||
wraps.__doc__ = functools.wraps.__doc__
|
||||
|
||||
else:
|
||||
wraps = functools.wraps
|
||||
|
||||
@@ -802,10 +842,22 @@ def with_metaclass(meta, *bases):
|
||||
# This requires a bit of explanation: the basic idea is to make a dummy
|
||||
# metaclass for one level of class instantiation that replaces itself with
|
||||
# the actual metaclass.
|
||||
class metaclass(meta):
|
||||
class metaclass(type):
|
||||
|
||||
def __new__(cls, name, this_bases, d):
|
||||
return meta(name, bases, d)
|
||||
if sys.version_info[:2] >= (3, 7):
|
||||
# This version introduced PEP 560 that requires a bit
|
||||
# of extra care (we mimic what is done by __build_class__).
|
||||
resolved_bases = types.resolve_bases(bases)
|
||||
if resolved_bases is not bases:
|
||||
d['__orig_bases__'] = bases
|
||||
else:
|
||||
resolved_bases = bases
|
||||
return meta(name, resolved_bases, d)
|
||||
|
||||
@classmethod
|
||||
def __prepare__(cls, name, this_bases):
|
||||
return meta.__prepare__(name, bases)
|
||||
return type.__new__(metaclass, 'temporary_class', (), {})
|
||||
|
||||
|
||||
@@ -821,13 +873,75 @@ def add_metaclass(metaclass):
|
||||
orig_vars.pop(slots_var)
|
||||
orig_vars.pop('__dict__', None)
|
||||
orig_vars.pop('__weakref__', None)
|
||||
if hasattr(cls, '__qualname__'):
|
||||
orig_vars['__qualname__'] = cls.__qualname__
|
||||
return metaclass(cls.__name__, cls.__bases__, orig_vars)
|
||||
return wrapper
|
||||
|
||||
|
||||
def ensure_binary(s, encoding='utf-8', errors='strict'):
|
||||
"""Coerce **s** to six.binary_type.
|
||||
|
||||
For Python 2:
|
||||
- `unicode` -> encoded to `str`
|
||||
- `str` -> `str`
|
||||
|
||||
For Python 3:
|
||||
- `str` -> encoded to `bytes`
|
||||
- `bytes` -> `bytes`
|
||||
"""
|
||||
if isinstance(s, binary_type):
|
||||
return s
|
||||
if isinstance(s, text_type):
|
||||
return s.encode(encoding, errors)
|
||||
raise TypeError("not expecting type '%s'" % type(s))
|
||||
|
||||
|
||||
def ensure_str(s, encoding='utf-8', errors='strict'):
|
||||
"""Coerce *s* to `str`.
|
||||
|
||||
For Python 2:
|
||||
- `unicode` -> encoded to `str`
|
||||
- `str` -> `str`
|
||||
|
||||
For Python 3:
|
||||
- `str` -> `str`
|
||||
- `bytes` -> decoded to `str`
|
||||
"""
|
||||
# Optimization: Fast return for the common case.
|
||||
if type(s) is str:
|
||||
return s
|
||||
if PY2 and isinstance(s, text_type):
|
||||
return s.encode(encoding, errors)
|
||||
elif PY3 and isinstance(s, binary_type):
|
||||
return s.decode(encoding, errors)
|
||||
elif not isinstance(s, (text_type, binary_type)):
|
||||
raise TypeError("not expecting type '%s'" % type(s))
|
||||
return s
|
||||
|
||||
|
||||
def ensure_text(s, encoding='utf-8', errors='strict'):
|
||||
"""Coerce *s* to six.text_type.
|
||||
|
||||
For Python 2:
|
||||
- `unicode` -> `unicode`
|
||||
- `str` -> `unicode`
|
||||
|
||||
For Python 3:
|
||||
- `str` -> `str`
|
||||
- `bytes` -> decoded to `str`
|
||||
"""
|
||||
if isinstance(s, binary_type):
|
||||
return s.decode(encoding, errors)
|
||||
elif isinstance(s, text_type):
|
||||
return s
|
||||
else:
|
||||
raise TypeError("not expecting type '%s'" % type(s))
|
||||
|
||||
|
||||
def python_2_unicode_compatible(klass):
|
||||
"""
|
||||
A decorator that defines __unicode__ and __str__ methods under Python 2.
|
||||
A class decorator that defines __unicode__ and __str__ methods under Python 2.
|
||||
Under Python 3 it does nothing.
|
||||
|
||||
To support Python 2 and 3 with a single code base, define a __str__ method
|
||||
@@ -1,294 +0,0 @@
|
||||
"""Channel notifications support.
|
||||
|
||||
Classes and functions to support channel subscriptions and notifications
|
||||
on those channels.
|
||||
|
||||
Notes:
|
||||
- This code is based on experimental APIs and is subject to change.
|
||||
- Notification does not do deduplication of notification ids, that's up to
|
||||
the receiver.
|
||||
- Storing the Channel between calls is up to the caller.
|
||||
|
||||
|
||||
Example setting up a channel:
|
||||
|
||||
# Create a new channel that gets notifications via webhook.
|
||||
channel = new_webhook_channel("https://example.com/my_web_hook")
|
||||
|
||||
# Store the channel, keyed by 'channel.id'. Store it before calling the
|
||||
# watch method because notifications may start arriving before the watch
|
||||
# method returns.
|
||||
...
|
||||
|
||||
resp = service.objects().watchAll(
|
||||
bucket="some_bucket_id", body=channel.body()).execute()
|
||||
channel.update(resp)
|
||||
|
||||
# Store the channel, keyed by 'channel.id'. Store it after being updated
|
||||
# since the resource_id value will now be correct, and that's needed to
|
||||
# stop a subscription.
|
||||
...
|
||||
|
||||
|
||||
An example Webhook implementation using webapp2. Note that webapp2 puts
|
||||
headers in a case insensitive dictionary, as headers aren't guaranteed to
|
||||
always be upper case.
|
||||
|
||||
id = self.request.headers[X_GOOG_CHANNEL_ID]
|
||||
|
||||
# Retrieve the channel by id.
|
||||
channel = ...
|
||||
|
||||
# Parse notification from the headers, including validating the id.
|
||||
n = notification_from_headers(channel, self.request.headers)
|
||||
|
||||
# Do app specific stuff with the notification here.
|
||||
if n.resource_state == 'sync':
|
||||
# Code to handle sync state.
|
||||
elif n.resource_state == 'exists':
|
||||
# Code to handle the exists state.
|
||||
elif n.resource_state == 'not_exists':
|
||||
# Code to handle the not exists state.
|
||||
|
||||
|
||||
Example of unsubscribing.
|
||||
|
||||
service.channels().stop(channel.body())
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from googleapiclient import errors
|
||||
from oauth2client import util
|
||||
import six
|
||||
|
||||
# Oauth2client < 3 has the positional helper in 'util', >= 3 has it
|
||||
# in '_helpers'.
|
||||
try:
|
||||
from oauth2client import util
|
||||
except ImportError:
|
||||
from oauth2client import _helpers as util
|
||||
|
||||
|
||||
# The unix time epoch starts at midnight 1970.
|
||||
EPOCH = datetime.datetime.utcfromtimestamp(0)
|
||||
|
||||
# Map the names of the parameters in the JSON channel description to
|
||||
# the parameter names we use in the Channel class.
|
||||
CHANNEL_PARAMS = {
|
||||
'address': 'address',
|
||||
'id': 'id',
|
||||
'expiration': 'expiration',
|
||||
'params': 'params',
|
||||
'resourceId': 'resource_id',
|
||||
'resourceUri': 'resource_uri',
|
||||
'type': 'type',
|
||||
'token': 'token',
|
||||
}
|
||||
|
||||
X_GOOG_CHANNEL_ID = 'X-GOOG-CHANNEL-ID'
|
||||
X_GOOG_MESSAGE_NUMBER = 'X-GOOG-MESSAGE-NUMBER'
|
||||
X_GOOG_RESOURCE_STATE = 'X-GOOG-RESOURCE-STATE'
|
||||
X_GOOG_RESOURCE_URI = 'X-GOOG-RESOURCE-URI'
|
||||
X_GOOG_RESOURCE_ID = 'X-GOOG-RESOURCE-ID'
|
||||
|
||||
|
||||
def _upper_header_keys(headers):
|
||||
new_headers = {}
|
||||
for k, v in six.iteritems(headers):
|
||||
new_headers[k.upper()] = v
|
||||
return new_headers
|
||||
|
||||
|
||||
class Notification(object):
|
||||
"""A Notification from a Channel.
|
||||
|
||||
Notifications are not usually constructed directly, but are returned
|
||||
from functions like notification_from_headers().
|
||||
|
||||
Attributes:
|
||||
message_number: int, The unique id number of this notification.
|
||||
state: str, The state of the resource being monitored.
|
||||
uri: str, The address of the resource being monitored.
|
||||
resource_id: str, The unique identifier of the version of the resource at
|
||||
this event.
|
||||
"""
|
||||
@util.positional(5)
|
||||
def __init__(self, message_number, state, resource_uri, resource_id):
|
||||
"""Notification constructor.
|
||||
|
||||
Args:
|
||||
message_number: int, The unique id number of this notification.
|
||||
state: str, The state of the resource being monitored. Can be one
|
||||
of "exists", "not_exists", or "sync".
|
||||
resource_uri: str, The address of the resource being monitored.
|
||||
resource_id: str, The identifier of the watched resource.
|
||||
"""
|
||||
self.message_number = message_number
|
||||
self.state = state
|
||||
self.resource_uri = resource_uri
|
||||
self.resource_id = resource_id
|
||||
|
||||
|
||||
class Channel(object):
|
||||
"""A Channel for notifications.
|
||||
|
||||
Usually not constructed directly, instead it is returned from helper
|
||||
functions like new_webhook_channel().
|
||||
|
||||
Attributes:
|
||||
type: str, The type of delivery mechanism used by this channel. For
|
||||
example, 'web_hook'.
|
||||
id: str, A UUID for the channel.
|
||||
token: str, An arbitrary string associated with the channel that
|
||||
is delivered to the target address with each event delivered
|
||||
over this channel.
|
||||
address: str, The address of the receiving entity where events are
|
||||
delivered. Specific to the channel type.
|
||||
expiration: int, The time, in milliseconds from the epoch, when this
|
||||
channel will expire.
|
||||
params: dict, A dictionary of string to string, with additional parameters
|
||||
controlling delivery channel behavior.
|
||||
resource_id: str, An opaque id that identifies the resource that is
|
||||
being watched. Stable across different API versions.
|
||||
resource_uri: str, The canonicalized ID of the watched resource.
|
||||
"""
|
||||
|
||||
@util.positional(5)
|
||||
def __init__(self, type, id, token, address, expiration=None,
|
||||
params=None, resource_id="", resource_uri=""):
|
||||
"""Create a new Channel.
|
||||
|
||||
In user code, this Channel constructor will not typically be called
|
||||
manually since there are functions for creating channels for each specific
|
||||
type with a more customized set of arguments to pass.
|
||||
|
||||
Args:
|
||||
type: str, The type of delivery mechanism used by this channel. For
|
||||
example, 'web_hook'.
|
||||
id: str, A UUID for the channel.
|
||||
token: str, An arbitrary string associated with the channel that
|
||||
is delivered to the target address with each event delivered
|
||||
over this channel.
|
||||
address: str, The address of the receiving entity where events are
|
||||
delivered. Specific to the channel type.
|
||||
expiration: int, The time, in milliseconds from the epoch, when this
|
||||
channel will expire.
|
||||
params: dict, A dictionary of string to string, with additional parameters
|
||||
controlling delivery channel behavior.
|
||||
resource_id: str, An opaque id that identifies the resource that is
|
||||
being watched. Stable across different API versions.
|
||||
resource_uri: str, The canonicalized ID of the watched resource.
|
||||
"""
|
||||
self.type = type
|
||||
self.id = id
|
||||
self.token = token
|
||||
self.address = address
|
||||
self.expiration = expiration
|
||||
self.params = params
|
||||
self.resource_id = resource_id
|
||||
self.resource_uri = resource_uri
|
||||
|
||||
def body(self):
|
||||
"""Build a body from the Channel.
|
||||
|
||||
Constructs a dictionary that's appropriate for passing into watch()
|
||||
methods as the value of body argument.
|
||||
|
||||
Returns:
|
||||
A dictionary representation of the channel.
|
||||
"""
|
||||
result = {
|
||||
'id': self.id,
|
||||
'token': self.token,
|
||||
'type': self.type,
|
||||
'address': self.address
|
||||
}
|
||||
if self.params:
|
||||
result['params'] = self.params
|
||||
if self.resource_id:
|
||||
result['resourceId'] = self.resource_id
|
||||
if self.resource_uri:
|
||||
result['resourceUri'] = self.resource_uri
|
||||
if self.expiration:
|
||||
result['expiration'] = self.expiration
|
||||
|
||||
return result
|
||||
|
||||
def update(self, resp):
|
||||
"""Update a channel with information from the response of watch().
|
||||
|
||||
When a request is sent to watch() a resource, the response returned
|
||||
from the watch() request is a dictionary with updated channel information,
|
||||
such as the resource_id, which is needed when stopping a subscription.
|
||||
|
||||
Args:
|
||||
resp: dict, The response from a watch() method.
|
||||
"""
|
||||
for json_name, param_name in six.iteritems(CHANNEL_PARAMS):
|
||||
value = resp.get(json_name)
|
||||
if value is not None:
|
||||
setattr(self, param_name, value)
|
||||
|
||||
|
||||
def notification_from_headers(channel, headers):
|
||||
"""Parse a notification from the webhook request headers, validate
|
||||
the notification, and return a Notification object.
|
||||
|
||||
Args:
|
||||
channel: Channel, The channel that the notification is associated with.
|
||||
headers: dict, A dictionary like object that contains the request headers
|
||||
from the webhook HTTP request.
|
||||
|
||||
Returns:
|
||||
A Notification object.
|
||||
|
||||
Raises:
|
||||
errors.InvalidNotificationError if the notification is invalid.
|
||||
ValueError if the X-GOOG-MESSAGE-NUMBER can't be converted to an int.
|
||||
"""
|
||||
headers = _upper_header_keys(headers)
|
||||
channel_id = headers[X_GOOG_CHANNEL_ID]
|
||||
if channel.id != channel_id:
|
||||
raise errors.InvalidNotificationError(
|
||||
'Channel id mismatch: %s != %s' % (channel.id, channel_id))
|
||||
else:
|
||||
message_number = int(headers[X_GOOG_MESSAGE_NUMBER])
|
||||
state = headers[X_GOOG_RESOURCE_STATE]
|
||||
resource_uri = headers[X_GOOG_RESOURCE_URI]
|
||||
resource_id = headers[X_GOOG_RESOURCE_ID]
|
||||
return Notification(message_number, state, resource_uri, resource_id)
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def new_webhook_channel(url, token=None, expiration=None, params=None):
|
||||
"""Create a new webhook Channel.
|
||||
|
||||
Args:
|
||||
url: str, URL to post notifications to.
|
||||
token: str, An arbitrary string associated with the channel that
|
||||
is delivered to the target address with each notification delivered
|
||||
over this channel.
|
||||
expiration: datetime.datetime, A time in the future when the channel
|
||||
should expire. Can also be None if the subscription should use the
|
||||
default expiration. Note that different services may have different
|
||||
limits on how long a subscription lasts. Check the response from the
|
||||
watch() method to see the value the service has set for an expiration
|
||||
time.
|
||||
params: dict, Extra parameters to pass on channel creation. Currently
|
||||
not used for webhook channels.
|
||||
"""
|
||||
expiration_ms = 0
|
||||
if expiration:
|
||||
delta = expiration - EPOCH
|
||||
expiration_ms = delta.microseconds/1000 + (
|
||||
delta.seconds + delta.days*24*3600)*1000
|
||||
if expiration_ms < 0:
|
||||
expiration_ms = 0
|
||||
|
||||
return Channel('web_hook', str(uuid.uuid4()),
|
||||
token, url, expiration=expiration_ms,
|
||||
params=params)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user