##// END OF EJS Templates
#404 API extensions for showing permission for users...
marcink -
r2151:12ceeda3 beta
parent child Browse files
Show More
@@ -1,650 +1,659 b''
1 1 .. _api:
2 2
3 3 ===
4 4 API
5 5 ===
6 6
7 7
8 8 Starting from RhodeCode version 1.2 a simple API was implemented.
9 9 There's a single schema for calling all api methods. API is implemented
10 10 with JSON protocol both ways. An url to send API request in RhodeCode is
11 11 <your_server>/_admin/api
12 12
13 13 API ACCESS FOR WEB VIEWS
14 14 ++++++++++++++++++++++++
15 15
16 16 API access can also be turned on for each web view in RhodeCode that is
17 17 decorated with `@LoginRequired` decorator. To enable API access simple change
18 18 the standard login decorator to `@LoginRequired(api_access=True)`.
19 19 After this change, a rhodecode view can be accessed without login by adding a
20 20 GET parameter `?api_key=<api_key>` to url. By default this is only
21 21 enabled on RSS/ATOM feed views.
22 22
23 23
24 24 API ACCESS
25 25 ++++++++++
26 26
27 27 All clients are required to send JSON-RPC spec JSON data::
28 28
29 29 {
30 30 "id:"<id>",
31 31 "api_key":"<api_key>",
32 32 "method":"<method_name>",
33 33 "args":{"<arg_key>":"<arg_val>"}
34 34 }
35 35
36 36 Example call for autopulling remotes repos using curl::
37 37 curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"id":1,"api_key":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull","args":{"repo":"CPython"}}'
38 38
39 39 Simply provide
40 40 - *id* A value of any type, which is used to match the response with the request that it is replying to.
41 41 - *api_key* for access and permission validation.
42 42 - *method* is name of method to call
43 43 - *args* is an key:value list of arguments to pass to method
44 44
45 45 .. note::
46 46
47 47 api_key can be found in your user account page
48 48
49 49
50 50 RhodeCode API will return always a JSON-RPC response::
51 51
52 52 {
53 53 "id":<id>, # matching id sent by request
54 54 "result": "<result>"|null, # JSON formatted result, null if any errors
55 55 "error": "null"|<error_message> # JSON formatted error (if any)
56 56 }
57 57
58 58 All responses from API will be `HTTP/1.0 200 OK`, if there's an error while
59 59 calling api *error* key from response will contain failure description
60 60 and result will be null.
61 61
62 62 API METHODS
63 63 +++++++++++
64 64
65 65
66 66 pull
67 67 ----
68 68
69 69 Pulls given repo from remote location. Can be used to automatically keep
70 70 remote repos up to date. This command can be executed only using api_key
71 71 belonging to user with admin rights
72 72
73 73 INPUT::
74 74
75 75 id : <id_for_response>
76 76 api_key : "<api_key>"
77 77 method : "pull"
78 78 args : {
79 79 "repo_name" : "<reponame>"
80 80 }
81 81
82 82 OUTPUT::
83 83
84 84 result : "Pulled from <reponame>"
85 85 error : null
86 86
87 87
88 88 get_user
89 89 --------
90 90
91 91 Get's an user by username or user_id, Returns empty result if user is not found.
92 92 This command can be executed only using api_key belonging to user with admin
93 93 rights.
94 94
95 95
96 96 INPUT::
97 97
98 98 id : <id_for_response>
99 99 api_key : "<api_key>"
100 100 method : "get_user"
101 101 args : {
102 102 "userid" : "<username or user_id>"
103 103 }
104 104
105 105 OUTPUT::
106 106
107 107 result: None if user does not exist or
108 108 {
109 109 "id" : "<id>",
110 110 "username" : "<username>",
111 111 "firstname": "<firstname>",
112 112 "lastname" : "<lastname>",
113 113 "email" : "<email>",
114 114 "active" : "<bool>",
115 115 "admin" :Β  "<bool>",
116 "ldap_dn" : "<ldap_dn>"
116 "ldap_dn" : "<ldap_dn>",
117 "last_login": "<last_login>",
118 "permissions": {
119 "global": ["hg.create.repository",
120 "repository.read",
121 "hg.register.manual_activate"],
122 "repositories": {"repo1": "repository.none"},
123 "repositories_groups": {"Group1": "group.read"}
124 },
117 125 }
118 126
119 127 error: null
120 128
121 129
122 130 get_users
123 131 ---------
124 132
125 133 Lists all existing users. This command can be executed only using api_key
126 134 belonging to user with admin rights.
127 135
128 136
129 137 INPUT::
130 138
131 139 id : <id_for_response>
132 140 api_key : "<api_key>"
133 141 method : "get_users"
134 142 args : { }
135 143
136 144 OUTPUT::
137 145
138 146 result: [
139 147 {
140 148 "id" : "<id>",
141 149 "username" : "<username>",
142 150 "firstname": "<firstname>",
143 151 "lastname" : "<lastname>",
144 152 "email" : "<email>",
145 153 "active" : "<bool>",
146 154 "admin" :Β  "<bool>",
147 "ldap_dn" : "<ldap_dn>"
155 "ldap_dn" : "<ldap_dn>",
156 "last_login": "<last_login>",
148 157 },
149 158 …
150 159 ]
151 160 error: null
152 161
153 162
154 163 create_user
155 164 -----------
156 165
157 166 Creates new user. This command can
158 167 be executed only using api_key belonging to user with admin rights.
159 168
160 169
161 170 INPUT::
162 171
163 172 id : <id_for_response>
164 173 api_key : "<api_key>"
165 174 method : "create_user"
166 175 args : {
167 176 "username" : "<username>",
168 177 "password" : "<password>",
169 178 "email" : "<useremail>",
170 179 "firstname" : "<firstname> = None",
171 180 "lastname" : "<lastname> = None",
172 181 "active" : "<bool> = True",
173 182 "admin" : "<bool> = False",
174 183 "ldap_dn" : "<ldap_dn> = None"
175 184 }
176 185
177 186 OUTPUT::
178 187
179 188 result: {
180 189 "id" : "<new_user_id>",
181 190 "msg" : "created new user <username>"
182 191 }
183 192 error: null
184 193
185 194
186 195 update_user
187 196 -----------
188 197
189 198 updates current one if such user exists. This command can
190 199 be executed only using api_key belonging to user with admin rights.
191 200
192 201
193 202 INPUT::
194 203
195 204 id : <id_for_response>
196 205 api_key : "<api_key>"
197 206 method : "update_user"
198 207 args : {
199 208 "userid" : "<user_id or username>",
200 209 "username" : "<username>",
201 210 "password" : "<password>",
202 211 "email" : "<useremail>",
203 212 "firstname" : "<firstname>",
204 213 "lastname" : "<lastname>",
205 214 "active" : "<bool>",
206 215 "admin" : "<bool>",
207 216 "ldap_dn" : "<ldap_dn>"
208 217 }
209 218
210 219 OUTPUT::
211 220
212 221 result: {
213 222 "id" : "<edited_user_id>",
214 223 "msg" : "updated user <username>"
215 224 }
216 225 error: null
217 226
218 227
219 228 get_users_group
220 229 ---------------
221 230
222 231 Gets an existing users group. This command can be executed only using api_key
223 232 belonging to user with admin rights.
224 233
225 234
226 235 INPUT::
227 236
228 237 id : <id_for_response>
229 238 api_key : "<api_key>"
230 239 method : "get_users_group"
231 240 args : {
232 241 "group_name" : "<name>"
233 242 }
234 243
235 244 OUTPUT::
236 245
237 246 result : None if group not exist
238 247 {
239 248 "id" : "<id>",
240 249 "group_name" : "<groupname>",
241 250 "active": "<bool>",
242 251 "members" : [
243 252 { "id" : "<userid>",
244 253 "username" : "<username>",
245 254 "firstname": "<firstname>",
246 255 "lastname" : "<lastname>",
247 256 "email" : "<email>",
248 257 "active" : "<bool>",
249 258 "admin" :Β  "<bool>",
250 259 "ldap" : "<ldap_dn>"
251 260 },
252 261 …
253 262 ]
254 263 }
255 264 error : null
256 265
257 266
258 267 get_users_groups
259 268 ----------------
260 269
261 270 Lists all existing users groups. This command can be executed only using
262 271 api_key belonging to user with admin rights.
263 272
264 273
265 274 INPUT::
266 275
267 276 id : <id_for_response>
268 277 api_key : "<api_key>"
269 278 method : "get_users_groups"
270 279 args : { }
271 280
272 281 OUTPUT::
273 282
274 283 result : [
275 284 {
276 285 "id" : "<id>",
277 286 "group_name" : "<groupname>",
278 287 "active": "<bool>",
279 288 "members" : [
280 289 {
281 290 "id" : "<userid>",
282 291 "username" : "<username>",
283 292 "firstname": "<firstname>",
284 293 "lastname" : "<lastname>",
285 294 "email" : "<email>",
286 295 "active" : "<bool>",
287 296 "admin" :Β  "<bool>",
288 297 "ldap" : "<ldap_dn>"
289 298 },
290 299 …
291 300 ]
292 301 }
293 302 ]
294 303 error : null
295 304
296 305
297 306 create_users_group
298 307 ------------------
299 308
300 309 Creates new users group. This command can be executed only using api_key
301 310 belonging to user with admin rights
302 311
303 312
304 313 INPUT::
305 314
306 315 id : <id_for_response>
307 316 api_key : "<api_key>"
308 317 method : "create_users_group"
309 318 args: {
310 319 "group_name": "<groupname>",
311 320 "active":"<bool> = True"
312 321 }
313 322
314 323 OUTPUT::
315 324
316 325 result: {
317 326 "id": "<newusersgroupid>",
318 327 "msg": "created new users group <groupname>"
319 328 }
320 329 error: null
321 330
322 331
323 332 add_user_to_users_group
324 333 -----------------------
325 334
326 335 Adds a user to a users group. If user exists in that group success will be
327 336 `false`. This command can be executed only using api_key
328 337 belonging to user with admin rights
329 338
330 339
331 340 INPUT::
332 341
333 342 id : <id_for_response>
334 343 api_key : "<api_key>"
335 344 method : "add_user_users_group"
336 345 args: {
337 346 "group_name" : "<groupname>",
338 347 "username" : "<username>"
339 348 }
340 349
341 350 OUTPUT::
342 351
343 352 result: {
344 353 "id": "<newusersgroupmemberid>",
345 354 "success": True|False # depends on if member is in group
346 355 "msg": "added member <username> to users group <groupname> |
347 356 User is already in that group"
348 357 }
349 358 error: null
350 359
351 360
352 361 remove_user_from_users_group
353 362 ----------------------------
354 363
355 364 Removes a user from a users group. If user is not in given group success will
356 365 be `false`. This command can be executed only
357 366 using api_key belonging to user with admin rights
358 367
359 368
360 369 INPUT::
361 370
362 371 id : <id_for_response>
363 372 api_key : "<api_key>"
364 373 method : "remove_user_from_users_group"
365 374 args: {
366 375 "group_name" : "<groupname>",
367 376 "username" : "<username>"
368 377 }
369 378
370 379 OUTPUT::
371 380
372 381 result: {
373 382 "success": True|False, # depends on if member is in group
374 383 "msg": "removed member <username> from users group <groupname> |
375 384 User wasn't in group"
376 385 }
377 386 error: null
378 387
379 388
380 389 get_repo
381 390 --------
382 391
383 392 Gets an existing repository by it's name or repository_id. Members will return
384 393 either users_group or user associated to that repository. This command can
385 394 be executed only using api_key belonging to user with admin rights.
386 395
387 396
388 397 INPUT::
389 398
390 399 id : <id_for_response>
391 400 api_key : "<api_key>"
392 401 method : "get_repo"
393 402 args: {
394 403 "repoid" : "<reponame or repo_id>"
395 404 }
396 405
397 406 OUTPUT::
398 407
399 408 result: None if repository does not exist or
400 409 {
401 410 "id" : "<id>",
402 411 "repo_name" : "<reponame>"
403 412 "type" : "<type>",
404 413 "description" : "<description>",
405 414 "members" : [
406 415 {
407 416 "type": "user",
408 417 "id" : "<userid>",
409 418 "username" : "<username>",
410 419 "firstname": "<firstname>",
411 420 "lastname" : "<lastname>",
412 421 "email" : "<email>",
413 422 "active" : "<bool>",
414 423 "admin" :Β  "<bool>",
415 424 "ldap" : "<ldap_dn>",
416 425 "permission" : "repository.(read|write|admin)"
417 426 },
418 427 …
419 428 {
420 429 "type": "users_group",
421 430 "id" : "<usersgroupid>",
422 431 "name" : "<usersgroupname>",
423 432 "active": "<bool>",
424 433 "permission" : "repository.(read|write|admin)"
425 434 },
426 435 …
427 436 ]
428 437 }
429 438 error: null
430 439
431 440
432 441 get_repos
433 442 ---------
434 443
435 444 Lists all existing repositories. This command can be executed only using api_key
436 445 belonging to user with admin rights
437 446
438 447
439 448 INPUT::
440 449
441 450 id : <id_for_response>
442 451 api_key : "<api_key>"
443 452 method : "get_repos"
444 453 args: { }
445 454
446 455 OUTPUT::
447 456
448 457 result: [
449 458 {
450 459 "id" : "<id>",
451 460 "repo_name" : "<reponame>"
452 461 "type" : "<type>",
453 462 "description" : "<description>"
454 463 },
455 464 …
456 465 ]
457 466 error: null
458 467
459 468
460 469 get_repo_nodes
461 470 --------------
462 471
463 472 returns a list of nodes and it's children in a flat list for a given path
464 473 at given revision. It's possible to specify ret_type to show only `files` or
465 474 `dirs`. This command can be executed only using api_key belonging to user
466 475 with admin rights
467 476
468 477
469 478 INPUT::
470 479
471 480 id : <id_for_response>
472 481 api_key : "<api_key>"
473 482 method : "get_repo_nodes"
474 483 args: {
475 484 "repo_name" : "<reponame>",
476 485 "revision" : "<revision>",
477 486 "root_path" : "<root_path>",
478 487 "ret_type" : "<ret_type>" = 'all'
479 488 }
480 489
481 490 OUTPUT::
482 491
483 492 result: [
484 493 {
485 494 "name" : "<name>"
486 495 "type" : "<type>",
487 496 },
488 497 …
489 498 ]
490 499 error: null
491 500
492 501
493 502 create_repo
494 503 -----------
495 504
496 505 Creates a repository. This command can be executed only using api_key
497 506 belonging to user with admin rights.
498 507 If repository name contains "/", all needed repository groups will be created.
499 508 For example "foo/bar/baz" will create groups "foo", "bar" (with "foo" as parent),
500 509 and create "baz" repository with "bar" as group.
501 510
502 511
503 512 INPUT::
504 513
505 514 id : <id_for_response>
506 515 api_key : "<api_key>"
507 516 method : "create_repo"
508 517 args: {
509 518 "repo_name" : "<reponame>",
510 519 "owner_name" : "<ownername>",
511 520 "description" : "<description> = ''",
512 521 "repo_type" : "<type> = 'hg'",
513 522 "private" : "<bool> = False",
514 523 "clone_uri" : "<clone_uri> = None",
515 524 }
516 525
517 526 OUTPUT::
518 527
519 528 result: {
520 529 "id": "<newrepoid>",
521 530 "msg": "Created new repository <reponame>",
522 531 }
523 532 error: null
524 533
525 534
526 535 delete_repo
527 536 -----------
528 537
529 538 Deletes a repository. This command can be executed only using api_key
530 539 belonging to user with admin rights.
531 540
532 541
533 542 INPUT::
534 543
535 544 id : <id_for_response>
536 545 api_key : "<api_key>"
537 546 method : "delete_repo"
538 547 args: {
539 548 "repo_name" : "<reponame>",
540 549 }
541 550
542 551 OUTPUT::
543 552
544 553 result: {
545 554 "msg": "Deleted repository <reponame>",
546 555 }
547 556 error: null
548 557
549 558
550 559 grant_user_permission
551 560 ---------------------
552 561
553 562 Grant permission for user on given repository, or update existing one
554 563 if found. This command can be executed only using api_key belonging to user
555 564 with admin rights.
556 565
557 566
558 567 INPUT::
559 568
560 569 id : <id_for_response>
561 570 api_key : "<api_key>"
562 571 method : "grant_user_permission"
563 572 args: {
564 573 "repo_name" : "<reponame>",
565 574 "username" : "<username>",
566 575 "perm" : "(repository.(none|read|write|admin))",
567 576 }
568 577
569 578 OUTPUT::
570 579
571 580 result: {
572 581 "msg" : "Granted perm: <perm> for user: <username> in repo: <reponame>"
573 582 }
574 583 error: null
575 584
576 585
577 586 revoke_user_permission
578 587 ----------------------
579 588
580 589 Revoke permission for user on given repository. This command can be executed
581 590 only using api_key belonging to user with admin rights.
582 591
583 592
584 593 INPUT::
585 594
586 595 id : <id_for_response>
587 596 api_key : "<api_key>"
588 597 method : "revoke_user_permission"
589 598 args: {
590 599 "repo_name" : "<reponame>",
591 600 "username" : "<username>",
592 601 }
593 602
594 603 OUTPUT::
595 604
596 605 result: {
597 606 "msg" : "Revoked perm for user: <suername> in repo: <reponame>"
598 607 }
599 608 error: null
600 609
601 610
602 611 grant_users_group_permission
603 612 ----------------------------
604 613
605 614 Grant permission for users group on given repository, or update
606 615 existing one if found. This command can be executed only using
607 616 api_key belonging to user with admin rights.
608 617
609 618
610 619 INPUT::
611 620
612 621 id : <id_for_response>
613 622 api_key : "<api_key>"
614 623 method : "grant_users_group_permission"
615 624 args: {
616 625 "repo_name" : "<reponame>",
617 626 "group_name" : "<usersgroupname>",
618 627 "perm" : "(repository.(none|read|write|admin))",
619 628 }
620 629
621 630 OUTPUT::
622 631
623 632 result: {
624 633 "msg" : "Granted perm: <perm> for group: <usersgroupname> in repo: <reponame>"
625 634 }
626 635 error: null
627 636
628 637
629 638 revoke_users_group_permission
630 639 -----------------------------
631 640
632 641 Revoke permission for users group on given repository.This command can be
633 642 executed only using api_key belonging to user with admin rights.
634 643
635 644 INPUT::
636 645
637 646 id : <id_for_response>
638 647 api_key : "<api_key>"
639 648 method : "revoke_users_group_permission"
640 649 args: {
641 650 "repo_name" : "<reponame>",
642 651 "users_group" : "<usersgroupname>",
643 652 }
644 653
645 654 OUTPUT::
646 655
647 656 result: {
648 657 "msg" : "Revoked perm for group: <usersgroupname> in repo: <reponame>"
649 658 }
650 659 error: null No newline at end of file
@@ -1,600 +1,602 b''
1 1 .. _changelog:
2 2
3 3 =========
4 4 Changelog
5 5 =========
6 6
7 7
8 8 1.3.4 (**2012-XX-XX**)
9 9 ----------------------
10 10
11 11 :status: in-progress
12 12 :branch: beta
13 13
14 14 news
15 15 ++++
16 16
17 17 - Whoosh logging is now controlled by the .ini files logging setup
18 18 - added clone-url into edit form on /settings page
19 19 - added help text into repo add/edit forms
20 20 - created rcextensions module with additional mappings (ref #322) and
21 21 post push/pull/create repo hooks callbacks
22 22 - implemented #377 Users view for his own permissions on account page
23 23 - #399 added inheritance of permissions for users group on repos groups
24 24 - #401 repository group is automatically pre-selected when adding repos
25 25 inside a repository group
26 26 - added alternative HTTP 403 response when client failed to authenticate. Helps
27 27 solving issues with Mercurial and LDAP
28 28 - #402 removed group prefix from repository name when listing repositories
29 29 inside a group
30 30 - added gravatars into permission view and permissions autocomplete
31 31 - #347 when running multiple RhodeCode instances, properly invalidates cache
32 32 for all registered servers
33 33
34 34 fixes
35 35 +++++
36 36
37 37 - fixed #390 cache invalidation problems on repos inside group
38 38 - fixed #385 clone by ID url was loosing proxy prefix in URL
39 39 - fixed some unicode problems with waitress
40 40 - fixed issue with escaping < and > in changeset commits
41 41 - fixed error occurring during recursive group creation in API
42 42 create_repo function
43 43 - fixed #393 py2.5 fixes for routes url generator
44 44 - fixed #397 Private repository groups shows up before login
45 45 - fixed #396 fixed problems with revoking users in nested groups
46 - fixed mysql unicode issues + specified InnoDB as default engine with
47 utf8 charset
46 48
47 49 1.3.3 (**2012-03-02**)
48 50 ----------------------
49 51
50 52 news
51 53 ++++
52 54
53 55
54 56 fixes
55 57 +++++
56 58
57 59 - fixed some python2.5 compatibility issues
58 60 - fixed issues with removed repos was accidentally added as groups, after
59 61 full rescan of paths
60 62 - fixes #376 Cannot edit user (using container auth)
61 63 - fixes #378 Invalid image urls on changeset screen with proxy-prefix
62 64 configuration
63 65 - fixed initial sorting of repos inside repo group
64 66 - fixes issue when user tried to resubmit same permission into user/user_groups
65 67 - bumped beaker version that fixes #375 leap error bug
66 68 - fixed raw_changeset for git. It was generated with hg patch headers
67 69 - fixed vcs issue with last_changeset for filenodes
68 70 - fixed missing commit after hook delete
69 71 - fixed #372 issues with git operation detection that caused a security issue
70 72 for git repos
71 73
72 74 1.3.2 (**2012-02-28**)
73 75 ----------------------
74 76
75 77 news
76 78 ++++
77 79
78 80
79 81 fixes
80 82 +++++
81 83
82 84 - fixed git protocol issues with repos-groups
83 85 - fixed git remote repos validator that prevented from cloning remote git repos
84 86 - fixes #370 ending slashes fixes for repo and groups
85 87 - fixes #368 improved git-protocol detection to handle other clients
86 88 - fixes #366 When Setting Repository Group To Blank Repo Group Wont Be
87 89 Moved To Root
88 90 - fixes #371 fixed issues with beaker/sqlalchemy and non-ascii cache keys
89 91 - fixed #373 missing cascade drop on user_group_to_perm table
90 92
91 93 1.3.1 (**2012-02-27**)
92 94 ----------------------
93 95
94 96 news
95 97 ++++
96 98
97 99
98 100 fixes
99 101 +++++
100 102
101 103 - redirection loop occurs when remember-me wasn't checked during login
102 104 - fixes issues with git blob history generation
103 105 - don't fetch branch for git in file history dropdown. Causes unneeded slowness
104 106
105 107 1.3.0 (**2012-02-26**)
106 108 ----------------------
107 109
108 110 news
109 111 ++++
110 112
111 113 - code review, inspired by github code-comments
112 114 - #215 rst and markdown README files support
113 115 - #252 Container-based and proxy pass-through authentication support
114 116 - #44 branch browser. Filtering of changelog by branches
115 117 - mercurial bookmarks support
116 118 - new hover top menu, optimized to add maximum size for important views
117 119 - configurable clone url template with possibility to specify protocol like
118 120 ssh:// or http:// and also manually alter other parts of clone_url.
119 121 - enabled largefiles extension by default
120 122 - optimized summary file pages and saved a lot of unused space in them
121 123 - #239 option to manually mark repository as fork
122 124 - #320 mapping of commit authors to RhodeCode users
123 125 - #304 hashes are displayed using monospace font
124 126 - diff configuration, toggle white lines and context lines
125 127 - #307 configurable diffs, whitespace toggle, increasing context lines
126 128 - sorting on branches, tags and bookmarks using YUI datatable
127 129 - improved file filter on files page
128 130 - implements #330 api method for listing nodes ar particular revision
129 131 - #73 added linking issues in commit messages to chosen issue tracker url
130 132 based on user defined regular expression
131 133 - added linking of changesets in commit messages
132 134 - new compact changelog with expandable commit messages
133 135 - firstname and lastname are optional in user creation
134 136 - #348 added post-create repository hook
135 137 - #212 global encoding settings is now configurable from .ini files
136 138 - #227 added repository groups permissions
137 139 - markdown gets codehilite extensions
138 140 - new API methods, delete_repositories, grante/revoke permissions for groups
139 141 and repos
140 142
141 143
142 144 fixes
143 145 +++++
144 146
145 147 - rewrote dbsession management for atomic operations, and better error handling
146 148 - fixed sorting of repo tables
147 149 - #326 escape of special html entities in diffs
148 150 - normalized user_name => username in api attributes
149 151 - fixes #298 ldap created users with mixed case emails created conflicts
150 152 on saving a form
151 153 - fixes issue when owner of a repo couldn't revoke permissions for users
152 154 and groups
153 155 - fixes #271 rare JSON serialization problem with statistics
154 156 - fixes #337 missing validation check for conflicting names of a group with a
155 157 repositories group
156 158 - #340 fixed session problem for mysql and celery tasks
157 159 - fixed #331 RhodeCode mangles repository names if the a repository group
158 160 contains the "full path" to the repositories
159 161 - #355 RhodeCode doesn't store encrypted LDAP passwords
160 162
161 163 1.2.5 (**2012-01-28**)
162 164 ----------------------
163 165
164 166 news
165 167 ++++
166 168
167 169 fixes
168 170 +++++
169 171
170 172 - #340 Celery complains about MySQL server gone away, added session cleanup
171 173 for celery tasks
172 174 - #341 "scanning for repositories in None" log message during Rescan was missing
173 175 a parameter
174 176 - fixed creating archives with subrepos. Some hooks were triggered during that
175 177 operation leading to crash.
176 178 - fixed missing email in account page.
177 179 - Reverted Mercurial to 2.0.1 for windows due to bug in Mercurial that makes
178 180 forking on windows impossible
179 181
180 182 1.2.4 (**2012-01-19**)
181 183 ----------------------
182 184
183 185 news
184 186 ++++
185 187
186 188 - RhodeCode is bundled with mercurial series 2.0.X by default, with
187 189 full support to largefiles extension. Enabled by default in new installations
188 190 - #329 Ability to Add/Remove Groups to/from a Repository via AP
189 191 - added requires.txt file with requirements
190 192
191 193 fixes
192 194 +++++
193 195
194 196 - fixes db session issues with celery when emailing admins
195 197 - #331 RhodeCode mangles repository names if the a repository group
196 198 contains the "full path" to the repositories
197 199 - #298 Conflicting e-mail addresses for LDAP and RhodeCode users
198 200 - DB session cleanup after hg protocol operations, fixes issues with
199 201 `mysql has gone away` errors
200 202 - #333 doc fixes for get_repo api function
201 203 - #271 rare JSON serialization problem with statistics enabled
202 204 - #337 Fixes issues with validation of repository name conflicting with
203 205 a group name. A proper message is now displayed.
204 206 - #292 made ldap_dn in user edit readonly, to get rid of confusion that field
205 207 doesn't work
206 208 - #316 fixes issues with web description in hgrc files
207 209
208 210 1.2.3 (**2011-11-02**)
209 211 ----------------------
210 212
211 213 news
212 214 ++++
213 215
214 216 - added option to manage repos group for non admin users
215 217 - added following API methods for get_users, create_user, get_users_groups,
216 218 get_users_group, create_users_group, add_user_to_users_groups, get_repos,
217 219 get_repo, create_repo, add_user_to_repo
218 220 - implements #237 added password confirmation for my account
219 221 and admin edit user.
220 222 - implements #291 email notification for global events are now sent to all
221 223 administrator users, and global config email.
222 224
223 225 fixes
224 226 +++++
225 227
226 228 - added option for passing auth method for smtp mailer
227 229 - #276 issue with adding a single user with id>10 to usergroups
228 230 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
229 231 - #288 fixes managing of repos in a group for non admin user
230 232
231 233 1.2.2 (**2011-10-17**)
232 234 ----------------------
233 235
234 236 news
235 237 ++++
236 238
237 239 - #226 repo groups are available by path instead of numerical id
238 240
239 241 fixes
240 242 +++++
241 243
242 244 - #259 Groups with the same name but with different parent group
243 245 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
244 246 - #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
245 247 - #265 ldap save fails sometimes on converting attributes to booleans,
246 248 added getter and setter into model that will prevent from this on db model level
247 249 - fixed problems with timestamps issues #251 and #213
248 250 - fixes #266 RhodeCode allows to create repo with the same name and in
249 251 the same parent as group
250 252 - fixes #245 Rescan of the repositories on Windows
251 253 - fixes #248 cannot edit repos inside a group on windows
252 254 - fixes #219 forking problems on windows
253 255
254 256 1.2.1 (**2011-10-08**)
255 257 ----------------------
256 258
257 259 news
258 260 ++++
259 261
260 262
261 263 fixes
262 264 +++++
263 265
264 266 - fixed problems with basic auth and push problems
265 267 - gui fixes
266 268 - fixed logger
267 269
268 270 1.2.0 (**2011-10-07**)
269 271 ----------------------
270 272
271 273 news
272 274 ++++
273 275
274 276 - implemented #47 repository groups
275 277 - implemented #89 Can setup google analytics code from settings menu
276 278 - implemented #91 added nicer looking archive urls with more download options
277 279 like tags, branches
278 280 - implemented #44 into file browsing, and added follow branch option
279 281 - implemented #84 downloads can be enabled/disabled for each repository
280 282 - anonymous repository can be cloned without having to pass default:default
281 283 into clone url
282 284 - fixed #90 whoosh indexer can index chooses repositories passed in command
283 285 line
284 286 - extended journal with day aggregates and paging
285 287 - implemented #107 source code lines highlight ranges
286 288 - implemented #93 customizable changelog on combined revision ranges -
287 289 equivalent of githubs compare view
288 290 - implemented #108 extended and more powerful LDAP configuration
289 291 - implemented #56 users groups
290 292 - major code rewrites optimized codes for speed and memory usage
291 293 - raw and diff downloads are now in git format
292 294 - setup command checks for write access to given path
293 295 - fixed many issues with international characters and unicode. It uses utf8
294 296 decode with replace to provide less errors even with non utf8 encoded strings
295 297 - #125 added API KEY access to feeds
296 298 - #109 Repository can be created from external Mercurial link (aka. remote
297 299 repository, and manually updated (via pull) from admin panel
298 300 - beta git support - push/pull server + basic view for git repos
299 301 - added followers page and forks page
300 302 - server side file creation (with binary file upload interface)
301 303 and edition with commits powered by codemirror
302 304 - #111 file browser file finder, quick lookup files on whole file tree
303 305 - added quick login sliding menu into main page
304 306 - changelog uses lazy loading of affected files details, in some scenarios
305 307 this can improve speed of changelog page dramatically especially for
306 308 larger repositories.
307 309 - implements #214 added support for downloading subrepos in download menu.
308 310 - Added basic API for direct operations on rhodecode via JSON
309 311 - Implemented advanced hook management
310 312
311 313 fixes
312 314 +++++
313 315
314 316 - fixed file browser bug, when switching into given form revision the url was
315 317 not changing
316 318 - fixed propagation to error controller on simplehg and simplegit middlewares
317 319 - fixed error when trying to make a download on empty repository
318 320 - fixed problem with '[' chars in commit messages in journal
319 321 - fixed #99 Unicode errors, on file node paths with non utf-8 characters
320 322 - journal fork fixes
321 323 - removed issue with space inside renamed repository after deletion
322 324 - fixed strange issue on formencode imports
323 325 - fixed #126 Deleting repository on Windows, rename used incompatible chars.
324 326 - #150 fixes for errors on repositories mapped in db but corrupted in
325 327 filesystem
326 328 - fixed problem with ascendant characters in realm #181
327 329 - fixed problem with sqlite file based database connection pool
328 330 - whoosh indexer and code stats share the same dynamic extensions map
329 331 - fixes #188 - relationship delete of repo_to_perm entry on user removal
330 332 - fixes issue #189 Trending source files shows "show more" when no more exist
331 333 - fixes issue #197 Relative paths for pidlocks
332 334 - fixes issue #198 password will require only 3 chars now for login form
333 335 - fixes issue #199 wrong redirection for non admin users after creating a repository
334 336 - fixes issues #202, bad db constraint made impossible to attach same group
335 337 more than one time. Affects only mysql/postgres
336 338 - fixes #218 os.kill patch for windows was missing sig param
337 339 - improved rendering of dag (they are not trimmed anymore when number of
338 340 heads exceeds 5)
339 341
340 342 1.1.8 (**2011-04-12**)
341 343 ----------------------
342 344
343 345 news
344 346 ++++
345 347
346 348 - improved windows support
347 349
348 350 fixes
349 351 +++++
350 352
351 353 - fixed #140 freeze of python dateutil library, since new version is python2.x
352 354 incompatible
353 355 - setup-app will check for write permission in given path
354 356 - cleaned up license info issue #149
355 357 - fixes for issues #137,#116 and problems with unicode and accented characters.
356 358 - fixes crashes on gravatar, when passed in email as unicode
357 359 - fixed tooltip flickering problems
358 360 - fixed came_from redirection on windows
359 361 - fixed logging modules, and sql formatters
360 362 - windows fixes for os.kill issue #133
361 363 - fixes path splitting for windows issues #148
362 364 - fixed issue #143 wrong import on migration to 1.1.X
363 365 - fixed problems with displaying binary files, thanks to Thomas Waldmann
364 366 - removed name from archive files since it's breaking ui for long repo names
365 367 - fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
366 368 - fixed compatibility for 1024px displays, and larger dpi settings, thanks to
367 369 Thomas Waldmann
368 370 - fixed issue #166 summary pager was skipping 10 revisions on second page
369 371
370 372
371 373 1.1.7 (**2011-03-23**)
372 374 ----------------------
373 375
374 376 news
375 377 ++++
376 378
377 379 fixes
378 380 +++++
379 381
380 382 - fixed (again) #136 installation support for FreeBSD
381 383
382 384
383 385 1.1.6 (**2011-03-21**)
384 386 ----------------------
385 387
386 388 news
387 389 ++++
388 390
389 391 fixes
390 392 +++++
391 393
392 394 - fixed #136 installation support for FreeBSD
393 395 - RhodeCode will check for python version during installation
394 396
395 397 1.1.5 (**2011-03-17**)
396 398 ----------------------
397 399
398 400 news
399 401 ++++
400 402
401 403 - basic windows support, by exchanging pybcrypt into sha256 for windows only
402 404 highly inspired by idea of mantis406
403 405
404 406 fixes
405 407 +++++
406 408
407 409 - fixed sorting by author in main page
408 410 - fixed crashes with diffs on binary files
409 411 - fixed #131 problem with boolean values for LDAP
410 412 - fixed #122 mysql problems thanks to striker69
411 413 - fixed problem with errors on calling raw/raw_files/annotate functions
412 414 with unknown revisions
413 415 - fixed returned rawfiles attachment names with international character
414 416 - cleaned out docs, big thanks to Jason Harris
415 417
416 418 1.1.4 (**2011-02-19**)
417 419 ----------------------
418 420
419 421 news
420 422 ++++
421 423
422 424 fixes
423 425 +++++
424 426
425 427 - fixed formencode import problem on settings page, that caused server crash
426 428 when that page was accessed as first after server start
427 429 - journal fixes
428 430 - fixed option to access repository just by entering http://server/<repo_name>
429 431
430 432 1.1.3 (**2011-02-16**)
431 433 ----------------------
432 434
433 435 news
434 436 ++++
435 437
436 438 - implemented #102 allowing the '.' character in username
437 439 - added option to access repository just by entering http://server/<repo_name>
438 440 - celery task ignores result for better performance
439 441
440 442 fixes
441 443 +++++
442 444
443 445 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
444 446 apollo13 and Johan Walles
445 447 - small fixes in journal
446 448 - fixed problems with getting setting for celery from .ini files
447 449 - registration, password reset and login boxes share the same title as main
448 450 application now
449 451 - fixed #113: to high permissions to fork repository
450 452 - fixed problem with '[' chars in commit messages in journal
451 453 - removed issue with space inside renamed repository after deletion
452 454 - db transaction fixes when filesystem repository creation failed
453 455 - fixed #106 relation issues on databases different than sqlite
454 456 - fixed static files paths links to use of url() method
455 457
456 458 1.1.2 (**2011-01-12**)
457 459 ----------------------
458 460
459 461 news
460 462 ++++
461 463
462 464
463 465 fixes
464 466 +++++
465 467
466 468 - fixes #98 protection against float division of percentage stats
467 469 - fixed graph bug
468 470 - forced webhelpers version since it was making troubles during installation
469 471
470 472 1.1.1 (**2011-01-06**)
471 473 ----------------------
472 474
473 475 news
474 476 ++++
475 477
476 478 - added force https option into ini files for easier https usage (no need to
477 479 set server headers with this options)
478 480 - small css updates
479 481
480 482 fixes
481 483 +++++
482 484
483 485 - fixed #96 redirect loop on files view on repositories without changesets
484 486 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
485 487 and server crashed with errors
486 488 - fixed large tooltips problems on main page
487 489 - fixed #92 whoosh indexer is more error proof
488 490
489 491 1.1.0 (**2010-12-18**)
490 492 ----------------------
491 493
492 494 news
493 495 ++++
494 496
495 497 - rewrite of internals for vcs >=0.1.10
496 498 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
497 499 with older clients
498 500 - anonymous access, authentication via ldap
499 501 - performance upgrade for cached repos list - each repository has its own
500 502 cache that's invalidated when needed.
501 503 - performance upgrades on repositories with large amount of commits (20K+)
502 504 - main page quick filter for filtering repositories
503 505 - user dashboards with ability to follow chosen repositories actions
504 506 - sends email to admin on new user registration
505 507 - added cache/statistics reset options into repository settings
506 508 - more detailed action logger (based on hooks) with pushed changesets lists
507 509 and options to disable those hooks from admin panel
508 510 - introduced new enhanced changelog for merges that shows more accurate results
509 511 - new improved and faster code stats (based on pygments lexers mapping tables,
510 512 showing up to 10 trending sources for each repository. Additionally stats
511 513 can be disabled in repository settings.
512 514 - gui optimizations, fixed application width to 1024px
513 515 - added cut off (for large files/changesets) limit into config files
514 516 - whoosh, celeryd, upgrade moved to paster command
515 517 - other than sqlite database backends can be used
516 518
517 519 fixes
518 520 +++++
519 521
520 522 - fixes #61 forked repo was showing only after cache expired
521 523 - fixes #76 no confirmation on user deletes
522 524 - fixes #66 Name field misspelled
523 525 - fixes #72 block user removal when he owns repositories
524 526 - fixes #69 added password confirmation fields
525 527 - fixes #87 RhodeCode crashes occasionally on updating repository owner
526 528 - fixes #82 broken annotations on files with more than 1 blank line at the end
527 529 - a lot of fixes and tweaks for file browser
528 530 - fixed detached session issues
529 531 - fixed when user had no repos he would see all repos listed in my account
530 532 - fixed ui() instance bug when global hgrc settings was loaded for server
531 533 instance and all hgrc options were merged with our db ui() object
532 534 - numerous small bugfixes
533 535
534 536 (special thanks for TkSoh for detailed feedback)
535 537
536 538
537 539 1.0.2 (**2010-11-12**)
538 540 ----------------------
539 541
540 542 news
541 543 ++++
542 544
543 545 - tested under python2.7
544 546 - bumped sqlalchemy and celery versions
545 547
546 548 fixes
547 549 +++++
548 550
549 551 - fixed #59 missing graph.js
550 552 - fixed repo_size crash when repository had broken symlinks
551 553 - fixed python2.5 crashes.
552 554
553 555
554 556 1.0.1 (**2010-11-10**)
555 557 ----------------------
556 558
557 559 news
558 560 ++++
559 561
560 562 - small css updated
561 563
562 564 fixes
563 565 +++++
564 566
565 567 - fixed #53 python2.5 incompatible enumerate calls
566 568 - fixed #52 disable mercurial extension for web
567 569 - fixed #51 deleting repositories don't delete it's dependent objects
568 570
569 571
570 572 1.0.0 (**2010-11-02**)
571 573 ----------------------
572 574
573 575 - security bugfix simplehg wasn't checking for permissions on commands
574 576 other than pull or push.
575 577 - fixed doubled messages after push or pull in admin journal
576 578 - templating and css corrections, fixed repo switcher on chrome, updated titles
577 579 - admin menu accessible from options menu on repository view
578 580 - permissions cached queries
579 581
580 582 1.0.0rc4 (**2010-10-12**)
581 583 --------------------------
582 584
583 585 - fixed python2.5 missing simplejson imports (thanks to Jens BΓ€ckman)
584 586 - removed cache_manager settings from sqlalchemy meta
585 587 - added sqlalchemy cache settings to ini files
586 588 - validated password length and added second try of failure on paster setup-app
587 589 - fixed setup database destroy prompt even when there was no db
588 590
589 591
590 592 1.0.0rc3 (**2010-10-11**)
591 593 -------------------------
592 594
593 595 - fixed i18n during installation.
594 596
595 597 1.0.0rc2 (**2010-10-11**)
596 598 -------------------------
597 599
598 600 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
599 601 occure. After vcs is fixed it'll be put back again.
600 602 - templating/css rewrites, optimized css. No newline at end of file
@@ -1,262 +1,262 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.api
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 JSON RPC controller
7 7
8 8 :created_on: Aug 20, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software; you can redistribute it and/or
14 14 # modify it under the terms of the GNU General Public License
15 15 # as published by the Free Software Foundation; version 2
16 16 # of the License or (at your opinion) any later version of the license.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program; if not, write to the Free Software
25 25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 26 # MA 02110-1301, USA.
27 27
28 28 import inspect
29 29 import logging
30 30 import types
31 31 import urllib
32 32 import traceback
33 33
34 34 from rhodecode.lib.compat import izip_longest, json
35 35
36 36 from paste.response import replace_header
37 37
38 38 from pylons.controllers import WSGIController
39 39
40 40
41 41 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
42 42 HTTPBadRequest, HTTPError
43 43
44 44 from rhodecode.model.db import User
45 45 from rhodecode.lib.auth import AuthUser
46 46
47 47 log = logging.getLogger('JSONRPC')
48 48
49 49
50 50 class JSONRPCError(BaseException):
51 51
52 52 def __init__(self, message):
53 53 self.message = message
54 54 super(JSONRPCError, self).__init__()
55 55
56 56 def __str__(self):
57 57 return str(self.message)
58 58
59 59
60 60 def jsonrpc_error(message, code=None):
61 61 """
62 62 Generate a Response object with a JSON-RPC error body
63 63 """
64 64 from pylons.controllers.util import Response
65 65 resp = Response(body=json.dumps(dict(id=None, result=None, error=message)),
66 66 status=code,
67 67 content_type='application/json')
68 68 return resp
69 69
70 70
71 71 class JSONRPCController(WSGIController):
72 72 """
73 73 A WSGI-speaking JSON-RPC controller class
74 74
75 75 See the specification:
76 76 <http://json-rpc.org/wiki/specification>`.
77 77
78 78 Valid controller return values should be json-serializable objects.
79 79
80 80 Sub-classes should catch their exceptions and raise JSONRPCError
81 81 if they want to pass meaningful errors to the client.
82 82
83 83 """
84 84
85 85 def _get_method_args(self):
86 86 """
87 87 Return `self._rpc_args` to dispatched controller method
88 88 chosen by __call__
89 89 """
90 90 return self._rpc_args
91 91
92 92 def __call__(self, environ, start_response):
93 93 """
94 94 Parse the request body as JSON, look up the method on the
95 95 controller and if it exists, dispatch to it.
96 96 """
97 97 if 'CONTENT_LENGTH' not in environ:
98 98 log.debug("No Content-Length")
99 99 return jsonrpc_error(message="No Content-Length in request")
100 100 else:
101 101 length = environ['CONTENT_LENGTH'] or 0
102 102 length = int(environ['CONTENT_LENGTH'])
103 103 log.debug('Content-Length: %s' % length)
104 104
105 105 if length == 0:
106 106 log.debug("Content-Length is 0")
107 107 return jsonrpc_error(message="Content-Length is 0")
108 108
109 109 raw_body = environ['wsgi.input'].read(length)
110 110
111 111 try:
112 112 json_body = json.loads(urllib.unquote_plus(raw_body))
113 113 except ValueError, e:
114 114 # catch JSON errors Here
115 115 return jsonrpc_error(message="JSON parse error ERR:%s RAW:%r" \
116 116 % (e, urllib.unquote_plus(raw_body)))
117 117
118 118 # check AUTH based on API KEY
119 119 try:
120 120 self._req_api_key = json_body['api_key']
121 121 self._req_id = json_body['id']
122 122 self._req_method = json_body['method']
123 123 self._request_params = json_body['args']
124 124 log.debug(
125 125 'method: %s, params: %s' % (self._req_method,
126 126 self._request_params)
127 127 )
128 128 except KeyError, e:
129 129 return jsonrpc_error(message='Incorrect JSON query missing %s' % e)
130 130
131 131 # check if we can find this session using api_key
132 132 try:
133 133 u = User.get_by_api_key(self._req_api_key)
134 134 if u is None:
135 135 return jsonrpc_error(message='Invalid API KEY')
136 136 auth_u = AuthUser(u.user_id, self._req_api_key)
137 137 except Exception, e:
138 138 return jsonrpc_error(message='Invalid API KEY')
139 139
140 140 self._error = None
141 141 try:
142 142 self._func = self._find_method()
143 143 except AttributeError, e:
144 144 return jsonrpc_error(message=str(e))
145 145
146 146 # now that we have a method, add self._req_params to
147 147 # self.kargs and dispatch control to WGIController
148 148 argspec = inspect.getargspec(self._func)
149 149 arglist = argspec[0][1:]
150 150 defaults = map(type, argspec[3] or [])
151 151 default_empty = types.NotImplementedType
152 152
153 153 # kw arguments required by this method
154 154 func_kwargs = dict(izip_longest(reversed(arglist), reversed(defaults),
155 155 fillvalue=default_empty))
156 156
157 157 # this is little trick to inject logged in user for
158 158 # perms decorators to work they expect the controller class to have
159 159 # rhodecode_user attribute set
160 160 self.rhodecode_user = auth_u
161 161
162 162 # This attribute will need to be first param of a method that uses
163 163 # api_key, which is translated to instance of user at that name
164 164 USER_SESSION_ATTR = 'apiuser'
165 165
166 166 if USER_SESSION_ATTR not in arglist:
167 167 return jsonrpc_error(message='This method [%s] does not support '
168 168 'authentication (missing %s param)' %
169 169 (self._func.__name__, USER_SESSION_ATTR))
170 170
171 171 # get our arglist and check if we provided them as args
172 172 for arg, default in func_kwargs.iteritems():
173 173 if arg == USER_SESSION_ATTR:
174 174 # USER_SESSION_ATTR is something translated from api key and
175 175 # this is checked before so we don't need validate it
176 176 continue
177 177
178 178 # skip the required param check if it's default value is
179 179 # NotImplementedType (default_empty)
180 180 if (default == default_empty and arg not in self._request_params):
181 181 return jsonrpc_error(
182 182 message=(
183 183 'Missing non optional `%s` arg in JSON DATA' % arg
184 184 )
185 185 )
186 186
187 187 self._rpc_args = {USER_SESSION_ATTR: u}
188 188 self._rpc_args.update(self._request_params)
189 189
190 190 self._rpc_args['action'] = self._req_method
191 191 self._rpc_args['environ'] = environ
192 192 self._rpc_args['start_response'] = start_response
193 193
194 194 status = []
195 195 headers = []
196 196 exc_info = []
197 197
198 198 def change_content(new_status, new_headers, new_exc_info=None):
199 199 status.append(new_status)
200 200 headers.extend(new_headers)
201 201 exc_info.append(new_exc_info)
202 202
203 203 output = WSGIController.__call__(self, environ, change_content)
204 204 output = list(output)
205 205 headers.append(('Content-Length', str(len(output[0]))))
206 206 replace_header(headers, 'Content-Type', 'application/json')
207 207 start_response(status[0], headers, exc_info[0])
208 208
209 209 return output
210 210
211 211 def _dispatch_call(self):
212 212 """
213 213 Implement dispatch interface specified by WSGIController
214 214 """
215 215 try:
216 216 raw_response = self._inspect_call(self._func)
217 217 if isinstance(raw_response, HTTPError):
218 218 self._error = str(raw_response)
219 219 except JSONRPCError, e:
220 220 self._error = str(e)
221 221 except Exception, e:
222 222 log.error('Encountered unhandled exception: %s' \
223 223 % traceback.format_exc())
224 224 json_exc = JSONRPCError('Internal server error')
225 225 self._error = str(json_exc)
226 226
227 227 if self._error is not None:
228 228 raw_response = None
229 229
230 230 response = dict(id=self._req_id, result=raw_response,
231 231 error=self._error)
232 232
233 233 try:
234 234 return json.dumps(response)
235 235 except TypeError, e:
236 log.debug('Error encoding response: %s' % e)
236 log.error('API FAILED. Error encoding response: %s' % e)
237 237 return json.dumps(
238 238 dict(
239 self._req_id,
239 id=self._req_id,
240 240 result=None,
241 241 error="Error encoding response"
242 242 )
243 243 )
244 244
245 245 def _find_method(self):
246 246 """
247 247 Return method named by `self._req_method` in controller if able
248 248 """
249 249 log.debug('Trying to find JSON-RPC method: %s' % self._req_method)
250 250 if self._req_method.startswith('_'):
251 251 raise AttributeError("Method not allowed")
252 252
253 253 try:
254 254 func = getattr(self, self._req_method, None)
255 255 except UnicodeEncodeError:
256 256 raise AttributeError("Problem decoding unicode in requested "
257 257 "method name.")
258 258
259 259 if isinstance(func, types.MethodType):
260 260 return func
261 261 else:
262 262 raise AttributeError("No such method: %s" % self._req_method)
@@ -1,656 +1,657 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.api
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 API controller for RhodeCode
7 7
8 8 :created_on: Aug 20, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software; you can redistribute it and/or
14 14 # modify it under the terms of the GNU General Public License
15 15 # as published by the Free Software Foundation; version 2
16 16 # of the License or (at your opinion) any later version of the license.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program; if not, write to the Free Software
25 25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 26 # MA 02110-1301, USA.
27 27
28 28 import traceback
29 29 import logging
30 30
31 31 from rhodecode.controllers.api import JSONRPCController, JSONRPCError
32 32 from rhodecode.lib.auth import HasPermissionAllDecorator, \
33 HasPermissionAnyDecorator, PasswordGenerator
33 HasPermissionAnyDecorator, PasswordGenerator, AuthUser
34 34
35 35 from rhodecode.model.meta import Session
36 36 from rhodecode.model.scm import ScmModel
37 from rhodecode.model.db import User, UsersGroup, RepoGroup, Repository
37 from rhodecode.model.db import User, UsersGroup, Repository
38 38 from rhodecode.model.repo import RepoModel
39 39 from rhodecode.model.user import UserModel
40 40 from rhodecode.model.users_group import UsersGroupModel
41 from rhodecode.model.repos_group import ReposGroupModel
42 41 from rhodecode.lib.utils import map_groups
43 42
44
45 43 log = logging.getLogger(__name__)
46 44
47 45
48 46 class ApiController(JSONRPCController):
49 47 """
50 48 API Controller
51 49
52 50
53 51 Each method needs to have USER as argument this is then based on given
54 52 API_KEY propagated as instance of user object
55 53
56 54 Preferably this should be first argument also
57 55
58 56
59 57 Each function should also **raise** JSONRPCError for any
60 58 errors that happens
61 59
62 60 """
63 61
64 62 @HasPermissionAllDecorator('hg.admin')
65 63 def pull(self, apiuser, repo_name):
66 64 """
67 65 Dispatch pull action on given repo
68 66
69 67
70 68 :param user:
71 69 :param repo_name:
72 70 """
73 71
74 72 if Repository.is_valid(repo_name) is False:
75 73 raise JSONRPCError('Unknown repo "%s"' % repo_name)
76 74
77 75 try:
78 76 ScmModel().pull_changes(repo_name, self.rhodecode_user.username)
79 77 return 'Pulled from %s' % repo_name
80 78 except Exception:
81 79 raise JSONRPCError('Unable to pull changes from "%s"' % repo_name)
82 80
83 81 @HasPermissionAllDecorator('hg.admin')
84 82 def get_user(self, apiuser, userid):
85 83 """"
86 84 Get a user by username
87 85
88 86 :param apiuser:
89 87 :param username:
90 88 """
91 89
92 90 user = UserModel().get_user(userid)
93 91 if user is None:
94 92 return user
95 93
96 94 return dict(
97 95 id=user.user_id,
98 96 username=user.username,
99 97 firstname=user.name,
100 98 lastname=user.lastname,
101 99 email=user.email,
102 100 active=user.active,
103 101 admin=user.admin,
104 ldap_dn=user.ldap_dn
102 ldap_dn=user.ldap_dn,
103 last_login=user.last_login,
104 permissions=AuthUser(user_id=user.user_id).permissions
105 105 )
106 106
107 107 @HasPermissionAllDecorator('hg.admin')
108 108 def get_users(self, apiuser):
109 109 """"
110 110 Get all users
111 111
112 112 :param apiuser:
113 113 """
114 114
115 115 result = []
116 116 for user in User.getAll():
117 117 result.append(
118 118 dict(
119 119 id=user.user_id,
120 120 username=user.username,
121 121 firstname=user.name,
122 122 lastname=user.lastname,
123 123 email=user.email,
124 124 active=user.active,
125 125 admin=user.admin,
126 ldap_dn=user.ldap_dn
126 ldap_dn=user.ldap_dn,
127 last_login=user.last_login,
127 128 )
128 129 )
129 130 return result
130 131
131 132 @HasPermissionAllDecorator('hg.admin')
132 133 def create_user(self, apiuser, username, email, password, firstname=None,
133 134 lastname=None, active=True, admin=False, ldap_dn=None):
134 135 """
135 136 Create new user
136 137
137 138 :param apiuser:
138 139 :param username:
139 140 :param password:
140 141 :param email:
141 142 :param name:
142 143 :param lastname:
143 144 :param active:
144 145 :param admin:
145 146 :param ldap_dn:
146 147 """
147 148 if User.get_by_username(username):
148 149 raise JSONRPCError("user %s already exist" % username)
149 150
150 151 if User.get_by_email(email, case_insensitive=True):
151 152 raise JSONRPCError("email %s already exist" % email)
152 153
153 154 if ldap_dn:
154 155 # generate temporary password if ldap_dn
155 156 password = PasswordGenerator().gen_password(length=8)
156 157
157 158 try:
158 159 usr = UserModel().create_or_update(
159 160 username, password, email, firstname,
160 161 lastname, active, admin, ldap_dn
161 162 )
162 163 Session.commit()
163 164 return dict(
164 165 id=usr.user_id,
165 166 msg='created new user %s' % username
166 167 )
167 168 except Exception:
168 169 log.error(traceback.format_exc())
169 170 raise JSONRPCError('failed to create user %s' % username)
170 171
171 172 @HasPermissionAllDecorator('hg.admin')
172 173 def update_user(self, apiuser, userid, username, password, email,
173 174 firstname, lastname, active, admin, ldap_dn):
174 175 """
175 176 Updates given user
176 177
177 178 :param apiuser:
178 179 :param username:
179 180 :param password:
180 181 :param email:
181 182 :param name:
182 183 :param lastname:
183 184 :param active:
184 185 :param admin:
185 186 :param ldap_dn:
186 187 """
187 188 if not UserModel().get_user(userid):
188 189 raise JSONRPCError("user %s does not exist" % username)
189 190
190 191 try:
191 192 usr = UserModel().create_or_update(
192 193 username, password, email, firstname,
193 194 lastname, active, admin, ldap_dn
194 195 )
195 196 Session.commit()
196 197 return dict(
197 198 id=usr.user_id,
198 199 msg='updated user %s' % username
199 200 )
200 201 except Exception:
201 202 log.error(traceback.format_exc())
202 203 raise JSONRPCError('failed to update user %s' % username)
203 204
204 205 @HasPermissionAllDecorator('hg.admin')
205 206 def get_users_group(self, apiuser, group_name):
206 207 """"
207 208 Get users group by name
208 209
209 210 :param apiuser:
210 211 :param group_name:
211 212 """
212 213
213 214 users_group = UsersGroup.get_by_group_name(group_name)
214 215 if not users_group:
215 216 return None
216 217
217 218 members = []
218 219 for user in users_group.members:
219 220 user = user.user
220 221 members.append(dict(id=user.user_id,
221 222 username=user.username,
222 223 firstname=user.name,
223 224 lastname=user.lastname,
224 225 email=user.email,
225 226 active=user.active,
226 227 admin=user.admin,
227 228 ldap=user.ldap_dn))
228 229
229 230 return dict(id=users_group.users_group_id,
230 231 group_name=users_group.users_group_name,
231 232 active=users_group.users_group_active,
232 233 members=members)
233 234
234 235 @HasPermissionAllDecorator('hg.admin')
235 236 def get_users_groups(self, apiuser):
236 237 """"
237 238 Get all users groups
238 239
239 240 :param apiuser:
240 241 """
241 242
242 243 result = []
243 244 for users_group in UsersGroup.getAll():
244 245 members = []
245 246 for user in users_group.members:
246 247 user = user.user
247 248 members.append(dict(id=user.user_id,
248 249 username=user.username,
249 250 firstname=user.name,
250 251 lastname=user.lastname,
251 252 email=user.email,
252 253 active=user.active,
253 254 admin=user.admin,
254 255 ldap=user.ldap_dn))
255 256
256 257 result.append(dict(id=users_group.users_group_id,
257 258 group_name=users_group.users_group_name,
258 259 active=users_group.users_group_active,
259 260 members=members))
260 261 return result
261 262
262 263 @HasPermissionAllDecorator('hg.admin')
263 264 def create_users_group(self, apiuser, group_name, active=True):
264 265 """
265 266 Creates an new usergroup
266 267
267 268 :param group_name:
268 269 :param active:
269 270 """
270 271
271 272 if self.get_users_group(apiuser, group_name):
272 273 raise JSONRPCError("users group %s already exist" % group_name)
273 274
274 275 try:
275 276 ug = UsersGroupModel().create(name=group_name, active=active)
276 277 Session.commit()
277 278 return dict(id=ug.users_group_id,
278 279 msg='created new users group %s' % group_name)
279 280 except Exception:
280 281 log.error(traceback.format_exc())
281 282 raise JSONRPCError('failed to create group %s' % group_name)
282 283
283 284 @HasPermissionAllDecorator('hg.admin')
284 285 def add_user_to_users_group(self, apiuser, group_name, username):
285 286 """"
286 287 Add a user to a users group
287 288
288 289 :param apiuser:
289 290 :param group_name:
290 291 :param username:
291 292 """
292 293
293 294 try:
294 295 users_group = UsersGroup.get_by_group_name(group_name)
295 296 if not users_group:
296 297 raise JSONRPCError('unknown users group %s' % group_name)
297 298
298 299 user = User.get_by_username(username)
299 300 if user is None:
300 301 raise JSONRPCError('unknown user %s' % username)
301 302
302 303 ugm = UsersGroupModel().add_user_to_group(users_group, user)
303 304 success = True if ugm != True else False
304 305 msg = 'added member %s to users group %s' % (username, group_name)
305 306 msg = msg if success else 'User is already in that group'
306 307 Session.commit()
307 308
308 309 return dict(
309 310 id=ugm.users_group_member_id if ugm != True else None,
310 311 success=success,
311 312 msg=msg
312 313 )
313 314 except Exception:
314 315 log.error(traceback.format_exc())
315 316 raise JSONRPCError('failed to add users group member')
316 317
317 318 @HasPermissionAllDecorator('hg.admin')
318 319 def remove_user_from_users_group(self, apiuser, group_name, username):
319 320 """
320 321 Remove user from a group
321 322
322 323 :param apiuser
323 324 :param group_name
324 325 :param username
325 326 """
326 327
327 328 try:
328 329 users_group = UsersGroup.get_by_group_name(group_name)
329 330 if not users_group:
330 331 raise JSONRPCError('unknown users group %s' % group_name)
331 332
332 333 user = User.get_by_username(username)
333 334 if user is None:
334 335 raise JSONRPCError('unknown user %s' % username)
335 336
336 337 success = UsersGroupModel().remove_user_from_group(users_group, user)
337 338 msg = 'removed member %s from users group %s' % (username, group_name)
338 339 msg = msg if success else "User wasn't in group"
339 340 Session.commit()
340 341 return dict(success=success, msg=msg)
341 342 except Exception:
342 343 log.error(traceback.format_exc())
343 344 raise JSONRPCError('failed to remove user from group')
344 345
345 346 @HasPermissionAnyDecorator('hg.admin')
346 347 def get_repo(self, apiuser, repoid):
347 348 """"
348 349 Get repository by name
349 350
350 351 :param apiuser:
351 352 :param repo_name:
352 353 """
353 354
354 355 repo = RepoModel().get_repo(repoid)
355 356 if repo is None:
356 357 raise JSONRPCError('unknown repository %s' % repo)
357 358
358 359 members = []
359 360 for user in repo.repo_to_perm:
360 361 perm = user.permission.permission_name
361 362 user = user.user
362 363 members.append(
363 364 dict(
364 365 type="user",
365 366 id=user.user_id,
366 367 username=user.username,
367 368 firstname=user.name,
368 369 lastname=user.lastname,
369 370 email=user.email,
370 371 active=user.active,
371 372 admin=user.admin,
372 373 ldap=user.ldap_dn,
373 374 permission=perm
374 375 )
375 376 )
376 377 for users_group in repo.users_group_to_perm:
377 378 perm = users_group.permission.permission_name
378 379 users_group = users_group.users_group
379 380 members.append(
380 381 dict(
381 382 type="users_group",
382 383 id=users_group.users_group_id,
383 384 name=users_group.users_group_name,
384 385 active=users_group.users_group_active,
385 386 permission=perm
386 387 )
387 388 )
388 389
389 390 return dict(
390 391 id=repo.repo_id,
391 392 repo_name=repo.repo_name,
392 393 type=repo.repo_type,
393 394 description=repo.description,
394 395 members=members
395 396 )
396 397
397 398 @HasPermissionAnyDecorator('hg.admin')
398 399 def get_repos(self, apiuser):
399 400 """"
400 401 Get all repositories
401 402
402 403 :param apiuser:
403 404 """
404 405
405 406 result = []
406 407 for repository in Repository.getAll():
407 408 result.append(
408 409 dict(
409 410 id=repository.repo_id,
410 411 repo_name=repository.repo_name,
411 412 type=repository.repo_type,
412 413 description=repository.description
413 414 )
414 415 )
415 416 return result
416 417
417 418 @HasPermissionAnyDecorator('hg.admin')
418 419 def get_repo_nodes(self, apiuser, repo_name, revision, root_path,
419 420 ret_type='all'):
420 421 """
421 422 returns a list of nodes and it's children
422 423 for a given path at given revision. It's possible to specify ret_type
423 424 to show only files or dirs
424 425
425 426 :param apiuser:
426 427 :param repo_name: name of repository
427 428 :param revision: revision for which listing should be done
428 429 :param root_path: path from which start displaying
429 430 :param ret_type: return type 'all|files|dirs' nodes
430 431 """
431 432 try:
432 433 _d, _f = ScmModel().get_nodes(repo_name, revision, root_path,
433 434 flat=False)
434 435 _map = {
435 436 'all': _d + _f,
436 437 'files': _f,
437 438 'dirs': _d,
438 439 }
439 440 return _map[ret_type]
440 441 except KeyError:
441 442 raise JSONRPCError('ret_type must be one of %s' % _map.keys())
442 443 except Exception, e:
443 444 raise JSONRPCError(e)
444 445
445 446 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
446 447 def create_repo(self, apiuser, repo_name, owner_name, description='',
447 448 repo_type='hg', private=False, clone_uri=None):
448 449 """
449 450 Create repository, if clone_url is given it makes a remote clone
450 451
451 452 :param apiuser:
452 453 :param repo_name:
453 454 :param owner_name:
454 455 :param description:
455 456 :param repo_type:
456 457 :param private:
457 458 :param clone_uri:
458 459 """
459 460
460 461 try:
461 462 owner = User.get_by_username(owner_name)
462 463 if owner is None:
463 464 raise JSONRPCError('unknown user %s' % owner_name)
464 465
465 466 if Repository.get_by_repo_name(repo_name):
466 467 raise JSONRPCError("repo %s already exist" % repo_name)
467 468
468 469 groups = repo_name.split(Repository.url_sep())
469 470 real_name = groups[-1]
470 471 # create structure of groups
471 472 group = map_groups(repo_name)
472 473
473 474 repo = RepoModel().create(
474 475 dict(
475 476 repo_name=real_name,
476 477 repo_name_full=repo_name,
477 478 description=description,
478 479 private=private,
479 480 repo_type=repo_type,
480 481 repo_group=group.group_id if group else None,
481 482 clone_uri=clone_uri
482 483 ),
483 484 owner
484 485 )
485 486 Session.commit()
486 487
487 488 return dict(
488 489 id=repo.repo_id,
489 490 msg="Created new repository %s" % repo.repo_name
490 491 )
491 492
492 493 except Exception:
493 494 log.error(traceback.format_exc())
494 495 raise JSONRPCError('failed to create repository %s' % repo_name)
495 496
496 497 @HasPermissionAnyDecorator('hg.admin')
497 498 def delete_repo(self, apiuser, repo_name):
498 499 """
499 500 Deletes a given repository
500 501
501 502 :param repo_name:
502 503 """
503 504 if not Repository.get_by_repo_name(repo_name):
504 505 raise JSONRPCError("repo %s does not exist" % repo_name)
505 506 try:
506 507 RepoModel().delete(repo_name)
507 508 Session.commit()
508 509 return dict(
509 510 msg='Deleted repository %s' % repo_name
510 511 )
511 512 except Exception:
512 513 log.error(traceback.format_exc())
513 514 raise JSONRPCError('failed to delete repository %s' % repo_name)
514 515
515 516 @HasPermissionAnyDecorator('hg.admin')
516 517 def grant_user_permission(self, apiuser, repo_name, username, perm):
517 518 """
518 519 Grant permission for user on given repository, or update existing one
519 520 if found
520 521
521 522 :param repo_name:
522 523 :param username:
523 524 :param perm:
524 525 """
525 526
526 527 try:
527 528 repo = Repository.get_by_repo_name(repo_name)
528 529 if repo is None:
529 530 raise JSONRPCError('unknown repository %s' % repo)
530 531
531 532 user = User.get_by_username(username)
532 533 if user is None:
533 534 raise JSONRPCError('unknown user %s' % username)
534 535
535 536 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
536 537
537 538 Session.commit()
538 539 return dict(
539 540 msg='Granted perm: %s for user: %s in repo: %s' % (
540 541 perm, username, repo_name
541 542 )
542 543 )
543 544 except Exception:
544 545 log.error(traceback.format_exc())
545 546 raise JSONRPCError(
546 547 'failed to edit permission %(repo)s for %(user)s' % dict(
547 548 user=username, repo=repo_name
548 549 )
549 550 )
550 551
551 552 @HasPermissionAnyDecorator('hg.admin')
552 553 def revoke_user_permission(self, apiuser, repo_name, username):
553 554 """
554 555 Revoke permission for user on given repository
555 556
556 557 :param repo_name:
557 558 :param username:
558 559 """
559 560
560 561 try:
561 562 repo = Repository.get_by_repo_name(repo_name)
562 563 if repo is None:
563 564 raise JSONRPCError('unknown repository %s' % repo)
564 565
565 566 user = User.get_by_username(username)
566 567 if user is None:
567 568 raise JSONRPCError('unknown user %s' % username)
568 569
569 570 RepoModel().revoke_user_permission(repo=repo_name, user=username)
570 571
571 572 Session.commit()
572 573 return dict(
573 574 msg='Revoked perm for user: %s in repo: %s' % (
574 575 username, repo_name
575 576 )
576 577 )
577 578 except Exception:
578 579 log.error(traceback.format_exc())
579 580 raise JSONRPCError(
580 581 'failed to edit permission %(repo)s for %(user)s' % dict(
581 582 user=username, repo=repo_name
582 583 )
583 584 )
584 585
585 586 @HasPermissionAnyDecorator('hg.admin')
586 587 def grant_users_group_permission(self, apiuser, repo_name, group_name, perm):
587 588 """
588 589 Grant permission for users group on given repository, or update
589 590 existing one if found
590 591
591 592 :param repo_name:
592 593 :param group_name:
593 594 :param perm:
594 595 """
595 596
596 597 try:
597 598 repo = Repository.get_by_repo_name(repo_name)
598 599 if repo is None:
599 600 raise JSONRPCError('unknown repository %s' % repo)
600 601
601 602 user_group = UsersGroup.get_by_group_name(group_name)
602 603 if user_group is None:
603 604 raise JSONRPCError('unknown users group %s' % user_group)
604 605
605 606 RepoModel().grant_users_group_permission(repo=repo_name,
606 607 group_name=group_name,
607 608 perm=perm)
608 609
609 610 Session.commit()
610 611 return dict(
611 612 msg='Granted perm: %s for group: %s in repo: %s' % (
612 613 perm, group_name, repo_name
613 614 )
614 615 )
615 616 except Exception:
616 617 log.error(traceback.format_exc())
617 618 raise JSONRPCError(
618 619 'failed to edit permission %(repo)s for %(usersgr)s' % dict(
619 620 usersgr=group_name, repo=repo_name
620 621 )
621 622 )
622 623
623 624 @HasPermissionAnyDecorator('hg.admin')
624 625 def revoke_users_group_permission(self, apiuser, repo_name, group_name):
625 626 """
626 627 Revoke permission for users group on given repository
627 628
628 629 :param repo_name:
629 630 :param group_name:
630 631 """
631 632
632 633 try:
633 634 repo = Repository.get_by_repo_name(repo_name)
634 635 if repo is None:
635 636 raise JSONRPCError('unknown repository %s' % repo)
636 637
637 638 user_group = UsersGroup.get_by_group_name(group_name)
638 639 if user_group is None:
639 640 raise JSONRPCError('unknown users group %s' % user_group)
640 641
641 642 RepoModel().revoke_users_group_permission(repo=repo_name,
642 643 group_name=group_name)
643 644
644 645 Session.commit()
645 646 return dict(
646 647 msg='Revoked perm for group: %s in repo: %s' % (
647 648 group_name, repo_name
648 649 )
649 650 )
650 651 except Exception:
651 652 log.error(traceback.format_exc())
652 653 raise JSONRPCError(
653 654 'failed to edit permission %(repo)s for %(usersgr)s' % dict(
654 655 usersgr=group_name, repo=repo_name
655 656 )
656 657 )
@@ -1,399 +1,438 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.compat
4 4 ~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Python backward compatibility functions and common libs
7 7
8 8
9 9 :created_on: Oct 7, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2010-2010 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 import datetime
29 import functools
28 30 from rhodecode import __platform__, PLATFORM_WIN
29 31
30 32 #==============================================================================
31 33 # json
32 34 #==============================================================================
35
36
37 def __obj_dump(obj):
38 DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
39 DATE_FORMAT = "%Y-%m-%d"
40 if isinstance(obj, complex):
41 return [obj.real, obj.imag]
42 elif isinstance(obj, datetime.datetime):
43 return obj.strftime(DATETIME_FORMAT)
44 elif isinstance(obj, datetime.date):
45 return obj.strftime(DATE_FORMAT)
46 elif isinstance(obj, set):
47 return list(obj)
48 elif isinstance(obj, OrderedDict):
49 return obj.as_dict()
50 else:
51 raise NotImplementedError
52
33 53 try:
34 54 import json
55
56 # extended JSON encoder for json
57 class ExtendedEncoder(json.JSONEncoder):
58 def default(self, obj):
59 try:
60 return __obj_dump(obj)
61 except NotImplementedError:
62 pass
63 return json.JSONEncoder.default(self, obj)
64 # monkey-patch JSON encoder to use extended version
65 json.dumps = functools.partial(json.dumps, cls=ExtendedEncoder)
35 66 except ImportError:
36 67 import simplejson as json
37 68
69 def extended_encode(obj):
70 try:
71 return __obj_dump(obj)
72 except NotImplementedError:
73 pass
74 raise TypeError("%r is not JSON serializable" % (obj,))
75 json.dumps = functools.partial(json.dumps, default=extended_encode)
76
38 77
39 78 #==============================================================================
40 79 # izip_longest
41 80 #==============================================================================
42 81 try:
43 82 from itertools import izip_longest
44 83 except ImportError:
45 84 import itertools
46 85
47 def izip_longest(*args, **kwds): # noqa
86 def izip_longest(*args, **kwds):
48 87 fillvalue = kwds.get("fillvalue")
49 88
50 89 def sentinel(counter=([fillvalue] * (len(args) - 1)).pop):
51 yield counter() # yields the fillvalue, or raises IndexError
90 yield counter() # yields the fillvalue, or raises IndexError
52 91
53 92 fillers = itertools.repeat(fillvalue)
54 93 iters = [itertools.chain(it, sentinel(), fillers)
55 94 for it in args]
56 95 try:
57 96 for tup in itertools.izip(*iters):
58 97 yield tup
59 98 except IndexError:
60 99 pass
61 100
62 101
63 102 #==============================================================================
64 103 # OrderedDict
65 104 #==============================================================================
66 105
67 106 # Python Software Foundation License
68 107
69 108 # XXX: it feels like using the class with "is" and "is not" instead of "==" and
70 109 # "!=" should be faster.
71 110 class _Nil(object):
72 111
73 112 def __repr__(self):
74 113 return "nil"
75 114
76 115 def __eq__(self, other):
77 116 if (isinstance(other, _Nil)):
78 117 return True
79 118 else:
80 119 return NotImplemented
81 120
82 121 def __ne__(self, other):
83 122 if (isinstance(other, _Nil)):
84 123 return False
85 124 else:
86 125 return NotImplemented
87 126
88 127 _nil = _Nil()
89 128
90 129
91 130 class _odict(object):
92 131 """Ordered dict data structure, with O(1) complexity for dict operations
93 132 that modify one element.
94 133
95 134 Overwriting values doesn't change their original sequential order.
96 135 """
97 136
98 137 def _dict_impl(self):
99 138 return None
100 139
101 140 def __init__(self, data=(), **kwds):
102 141 """This doesn't accept keyword initialization as normal dicts to avoid
103 142 a trap - inside a function or method the keyword args are accessible
104 143 only as a dict, without a defined order, so their original order is
105 144 lost.
106 145 """
107 146 if kwds:
108 147 raise TypeError("__init__() of ordered dict takes no keyword "
109 148 "arguments to avoid an ordering trap.")
110 149 self._dict_impl().__init__(self)
111 150 # If you give a normal dict, then the order of elements is undefined
112 151 if hasattr(data, "iteritems"):
113 152 for key, val in data.iteritems():
114 153 self[key] = val
115 154 else:
116 155 for key, val in data:
117 156 self[key] = val
118 157
119 158 # Double-linked list header
120 159 def _get_lh(self):
121 160 dict_impl = self._dict_impl()
122 161 if not hasattr(self, '_lh'):
123 162 dict_impl.__setattr__(self, '_lh', _nil)
124 163 return dict_impl.__getattribute__(self, '_lh')
125 164
126 165 def _set_lh(self, val):
127 166 self._dict_impl().__setattr__(self, '_lh', val)
128 167
129 168 lh = property(_get_lh, _set_lh)
130 169
131 170 # Double-linked list tail
132 171 def _get_lt(self):
133 172 dict_impl = self._dict_impl()
134 173 if not hasattr(self, '_lt'):
135 174 dict_impl.__setattr__(self, '_lt', _nil)
136 175 return dict_impl.__getattribute__(self, '_lt')
137 176
138 177 def _set_lt(self, val):
139 178 self._dict_impl().__setattr__(self, '_lt', val)
140 179
141 180 lt = property(_get_lt, _set_lt)
142 181
143 182 def __getitem__(self, key):
144 183 return self._dict_impl().__getitem__(self, key)[1]
145 184
146 185 def __setitem__(self, key, val):
147 186 dict_impl = self._dict_impl()
148 187 try:
149 188 dict_impl.__getitem__(self, key)[1] = val
150 189 except KeyError:
151 190 new = [dict_impl.__getattribute__(self, 'lt'), val, _nil]
152 191 dict_impl.__setitem__(self, key, new)
153 192 if dict_impl.__getattribute__(self, 'lt') == _nil:
154 193 dict_impl.__setattr__(self, 'lh', key)
155 194 else:
156 195 dict_impl.__getitem__(
157 196 self, dict_impl.__getattribute__(self, 'lt'))[2] = key
158 197 dict_impl.__setattr__(self, 'lt', key)
159 198
160 199 def __delitem__(self, key):
161 200 dict_impl = self._dict_impl()
162 201 pred, _, succ = self._dict_impl().__getitem__(self, key)
163 202 if pred == _nil:
164 203 dict_impl.__setattr__(self, 'lh', succ)
165 204 else:
166 205 dict_impl.__getitem__(self, pred)[2] = succ
167 206 if succ == _nil:
168 207 dict_impl.__setattr__(self, 'lt', pred)
169 208 else:
170 209 dict_impl.__getitem__(self, succ)[0] = pred
171 210 dict_impl.__delitem__(self, key)
172 211
173 212 def __contains__(self, key):
174 213 return key in self.keys()
175 214
176 215 def __len__(self):
177 216 return len(self.keys())
178 217
179 218 def __str__(self):
180 219 pairs = ("%r: %r" % (k, v) for k, v in self.iteritems())
181 220 return "{%s}" % ", ".join(pairs)
182 221
183 222 def __repr__(self):
184 223 if self:
185 224 pairs = ("(%r, %r)" % (k, v) for k, v in self.iteritems())
186 225 return "odict([%s])" % ", ".join(pairs)
187 226 else:
188 227 return "odict()"
189 228
190 229 def get(self, k, x=None):
191 230 if k in self:
192 231 return self._dict_impl().__getitem__(self, k)[1]
193 232 else:
194 233 return x
195 234
196 235 def __iter__(self):
197 236 dict_impl = self._dict_impl()
198 237 curr_key = dict_impl.__getattribute__(self, 'lh')
199 238 while curr_key != _nil:
200 239 yield curr_key
201 240 curr_key = dict_impl.__getitem__(self, curr_key)[2]
202 241
203 242 iterkeys = __iter__
204 243
205 244 def keys(self):
206 245 return list(self.iterkeys())
207 246
208 247 def itervalues(self):
209 248 dict_impl = self._dict_impl()
210 249 curr_key = dict_impl.__getattribute__(self, 'lh')
211 250 while curr_key != _nil:
212 251 _, val, curr_key = dict_impl.__getitem__(self, curr_key)
213 252 yield val
214 253
215 254 def values(self):
216 255 return list(self.itervalues())
217 256
218 257 def iteritems(self):
219 258 dict_impl = self._dict_impl()
220 259 curr_key = dict_impl.__getattribute__(self, 'lh')
221 260 while curr_key != _nil:
222 261 _, val, next_key = dict_impl.__getitem__(self, curr_key)
223 262 yield curr_key, val
224 263 curr_key = next_key
225 264
226 265 def items(self):
227 266 return list(self.iteritems())
228 267
229 268 def sort(self, cmp=None, key=None, reverse=False):
230 269 items = [(k, v) for k, v in self.items()]
231 270 if cmp is not None:
232 271 items = sorted(items, cmp=cmp)
233 272 elif key is not None:
234 273 items = sorted(items, key=key)
235 274 else:
236 275 items = sorted(items, key=lambda x: x[1])
237 276 if reverse:
238 277 items.reverse()
239 278 self.clear()
240 279 self.__init__(items)
241 280
242 281 def clear(self):
243 282 dict_impl = self._dict_impl()
244 283 dict_impl.clear(self)
245 284 dict_impl.__setattr__(self, 'lh', _nil)
246 285 dict_impl.__setattr__(self, 'lt', _nil)
247 286
248 287 def copy(self):
249 288 return self.__class__(self)
250 289
251 290 def update(self, data=(), **kwds):
252 291 if kwds:
253 292 raise TypeError("update() of ordered dict takes no keyword "
254 293 "arguments to avoid an ordering trap.")
255 294 if hasattr(data, "iteritems"):
256 295 data = data.iteritems()
257 296 for key, val in data:
258 297 self[key] = val
259 298
260 299 def setdefault(self, k, x=None):
261 300 try:
262 301 return self[k]
263 302 except KeyError:
264 303 self[k] = x
265 304 return x
266 305
267 306 def pop(self, k, x=_nil):
268 307 try:
269 308 val = self[k]
270 309 del self[k]
271 310 return val
272 311 except KeyError:
273 312 if x == _nil:
274 313 raise
275 314 return x
276 315
277 316 def popitem(self):
278 317 try:
279 318 dict_impl = self._dict_impl()
280 319 key = dict_impl.__getattribute__(self, 'lt')
281 320 return key, self.pop(key)
282 321 except KeyError:
283 322 raise KeyError("'popitem(): ordered dictionary is empty'")
284 323
285 324 def riterkeys(self):
286 325 """To iterate on keys in reversed order.
287 326 """
288 327 dict_impl = self._dict_impl()
289 328 curr_key = dict_impl.__getattribute__(self, 'lt')
290 329 while curr_key != _nil:
291 330 yield curr_key
292 331 curr_key = dict_impl.__getitem__(self, curr_key)[0]
293 332
294 333 __reversed__ = riterkeys
295 334
296 335 def rkeys(self):
297 336 """List of the keys in reversed order.
298 337 """
299 338 return list(self.riterkeys())
300 339
301 340 def ritervalues(self):
302 341 """To iterate on values in reversed order.
303 342 """
304 343 dict_impl = self._dict_impl()
305 344 curr_key = dict_impl.__getattribute__(self, 'lt')
306 345 while curr_key != _nil:
307 346 curr_key, val, _ = dict_impl.__getitem__(self, curr_key)
308 347 yield val
309 348
310 349 def rvalues(self):
311 350 """List of the values in reversed order.
312 351 """
313 352 return list(self.ritervalues())
314 353
315 354 def riteritems(self):
316 355 """To iterate on (key, value) in reversed order.
317 356 """
318 357 dict_impl = self._dict_impl()
319 358 curr_key = dict_impl.__getattribute__(self, 'lt')
320 359 while curr_key != _nil:
321 360 pred_key, val, _ = dict_impl.__getitem__(self, curr_key)
322 361 yield curr_key, val
323 362 curr_key = pred_key
324 363
325 364 def ritems(self):
326 365 """List of the (key, value) in reversed order.
327 366 """
328 367 return list(self.riteritems())
329 368
330 369 def firstkey(self):
331 370 if self:
332 371 return self._dict_impl().__getattribute__(self, 'lh')
333 372 else:
334 373 raise KeyError("'firstkey(): ordered dictionary is empty'")
335 374
336 375 def lastkey(self):
337 376 if self:
338 377 return self._dict_impl().__getattribute__(self, 'lt')
339 378 else:
340 379 raise KeyError("'lastkey(): ordered dictionary is empty'")
341 380
342 381 def as_dict(self):
343 382 return self._dict_impl()(self.items())
344 383
345 384 def _repr(self):
346 385 """_repr(): low level repr of the whole data contained in the odict.
347 386 Useful for debugging.
348 387 """
349 388 dict_impl = self._dict_impl()
350 389 form = "odict low level repr lh,lt,data: %r, %r, %s"
351 390 return form % (dict_impl.__getattribute__(self, 'lh'),
352 391 dict_impl.__getattribute__(self, 'lt'),
353 392 dict_impl.__repr__(self))
354 393
355 394
356 395 class OrderedDict(_odict, dict):
357 396
358 397 def _dict_impl(self):
359 398 return dict
360 399
361 400
362 401 #==============================================================================
363 402 # OrderedSet
364 403 #==============================================================================
365 404 from sqlalchemy.util import OrderedSet
366 405
367 406
368 407 #==============================================================================
369 408 # kill FUNCTIONS
370 409 #==============================================================================
371 410 if __platform__ in PLATFORM_WIN:
372 411 import ctypes
373 412
374 413 def kill(pid, sig):
375 414 """kill function for Win32"""
376 415 kernel32 = ctypes.windll.kernel32
377 416 handle = kernel32.OpenProcess(1, 0, pid)
378 417 return (0 != kernel32.TerminateProcess(handle, 0))
379 418
380 419 else:
381 420 kill = os.kill
382 421
383 422
384 423 #==============================================================================
385 424 # itertools.product
386 425 #==============================================================================
387 426
388 427 try:
389 428 from itertools import product
390 429 except ImportError:
391 430 def product(*args, **kwds):
392 431 # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy
393 432 # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111
394 433 pools = map(tuple, args) * kwds.get('repeat', 1)
395 434 result = [[]]
396 435 for pool in pools:
397 436 result = [x + [y] for x in result for y in pool]
398 437 for prod in result:
399 438 yield tuple(prod)
General Comments 0
You need to be logged in to leave comments. Login now