├── COPYING.SPL.txt ├── Makefile ├── README.txt ├── TODO.rst ├── debian ├── changelog ├── compat ├── control ├── copyright ├── docs ├── pycompat └── rules ├── setup.cfg ├── setup.py └── simplegeo ├── __init__.py └── shared ├── __init__.py ├── _version.py └── test ├── __init__.py ├── test_client.py └── test_record.py /COPYING.SPL.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted to any person obtaining a copy of this work to 2 | deal in this work without restriction (including the rights to use, modify, 3 | distribute, sublicense, and/or sell copies). 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BASE ?= $(PWD) 2 | BIN = $(BASE)/bin 3 | PYVERS := 2.6 4 | PYTHON = $(BIN)/python 5 | PLATFORM = $(shell uname -s) 6 | EZ_INSTALL := $(BIN)/easy_install 7 | VIRTUALENV ?= VIRTUALENV_USE_SETUPTOOLS=1 \ 8 | $(shell test -x `which virtualenv` && which virtualenv || \ 9 | test -x `which virtualenv-$(PYVERS)` && \ 10 | which virtualenv-$(PYVERS)) 11 | VIRTUALENV += --no-site-packages 12 | SETUP = $(PYTHON) setup.py 13 | GIT_HEAD = $(BASE)/.git/$(shell cut -d\ -f2-999 .git/HEAD) 14 | BUILD_NUMBER ?= 0000INVALID 15 | 16 | .PHONY: dev env clean xclean extraclean bundles \ 17 | debian/changelog 18 | 19 | sdist: 20 | $(SETUP) sdist 21 | 22 | env: $(PYTHON) 23 | 24 | $(PYTHON) $(BIN)/easy_install: 25 | $(VIRTUALENV) . 26 | 27 | dev: env 28 | $(SETUP) develop 29 | 30 | @echo " ===============================================================" 31 | @echo " To activate your shiny new environment, please run:" 32 | @echo 33 | @which figlet > /dev/null && figlet -c " . bin/activate" || \ 34 | echo " . bin/activate" 35 | 36 | @echo " ===============================================================" 37 | 38 | lint flakes: 39 | $(SETUP) flakes 40 | 41 | coverage: .coverage 42 | @$(COVERAGE) html -d $@ $(COVERED) 43 | 44 | coverage.xml: .coverage 45 | @$(COVERAGE) xml $(COVERED) 46 | 47 | .coverage: $(SOURCES) $(TESTS) 48 | -@$(COVERAGE) run --branch setup.py test -s simplegeo.shared.test 49 | 50 | bin/coverage: bin/easy_install 51 | @$(EZ_INSTALL) coverage 52 | 53 | prereqs: prereqs-$(PLATFORM) 54 | prereqs-Linux: 55 | sudo apt-get install python2.6 python2.6-dev python-virtualenv \ 56 | libxml2-dev libxslt-dev libmemcached-dev \ 57 | mercurial darcs build-essential g++ wget 58 | 59 | prereqs-Darwin: 60 | sudo port install python_select \ 61 | python26 py26-virtualenv thrift libxml2 libxslt \ 62 | libmemcached mercurial darcs wget 63 | sudo python_select python26 64 | 65 | clean: 66 | find . -type f -name \*.pyc -exec rm {} \; 67 | rm -rf *.egg *.egg-info htmlcov build* src* dist coverage \ 68 | build-bundle* _trial_temp 69 | 70 | xclean: extraclean 71 | extraclean: clean 72 | rm -rf .Python bin include lib 73 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Please see our on-line docs for detailed instructions, examples, support forums, etc.: 2 | 3 | http://simplegeo.com/docs/clients-code-libraries/python 4 | 5 | 6 | See also these other two libraries: 7 | 8 | 9 | https://github.com/simplegeo/python-simplegeo-context 10 | 11 | http://pypi.python.org/pypi/simplegeo-context 12 | 13 | 14 | https://github.com/simplegeo/python-simplegeo-places 15 | 16 | http://pypi.python.org/pypi/simplegeo-places 17 | -------------------------------------------------------------------------------- /TODO.rst: -------------------------------------------------------------------------------- 1 | * name the Debian package after the Python module instead of after the Python distribution (that sounds worse to me, but that's what Piotr Ożarowski says is the Debian standard) therefore the Debian packages should be named python-simplegeo.shared instead of python-simplegeo-shared. 2 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | python-simplegeo-shared (2.5.127) maverick; urgency=low 2 | 3 | * UNRELEASED 4 | 5 | -- SimpleGeo Nerds Tue, 13 Dec 2011 17:39:08 +0000 6 | 7 | python-simplegeo-shared (2.5.126) maverick; urgency=low 8 | 9 | * UNRELEASED 10 | 11 | -- SimpleGeo Nerds Fri, 09 Dec 2011 01:11:39 +0000 12 | 13 | python-simplegeo-shared (2.5.85) lucid; urgency=low 14 | 15 | * UNRELEASED 16 | 17 | -- SimpleGeo Nerds Mon, 02 May 2011 23:49:03 +0000 18 | 19 | python-simplegeo-shared (2.5.84) lucid; urgency=low 20 | 21 | * UNRELEASED 22 | 23 | -- SimpleGeo Nerds Fri, 15 Apr 2011 21:21:39 +0000 24 | 25 | python-simplegeo-shared (2.5.83) lucid; urgency=low 26 | 27 | * UNRELEASED 28 | 29 | -- SimpleGeo Nerds Tue, 05 Apr 2011 23:12:11 +0000 30 | 31 | python-simplegeo-shared (2.5.82) lucid; urgency=low 32 | 33 | * UNRELEASED 34 | 35 | -- SimpleGeo Nerds Thu, 17 Feb 2011 21:54:02 +0000 36 | 37 | python-simplegeo-shared (2.5.81) lucid; urgency=low 38 | 39 | [ Zooko Ofsimplegeo ] 40 | * add test that shows Feature class rejects coords which aren't 41 | numbers when it is constructed So this is another thing that could 42 | have caused the misbehavior Wade saw -- a client sending GeoJSON 43 | with strings for coords -- but didn't. 44 | 45 | [ SimpleGeo Nerds ] 46 | 47 | -- SimpleGeo Nerds Thu, 10 Feb 2011 22:59:00 +0000 48 | 49 | python-simplegeo-shared (2.5.80) lucid; urgency=low 50 | 51 | [ Zooko Ofsimplegeo ] 52 | * add test that the geojson produced has coords as numbers, not as 53 | strings Wade reported that our python client is submitting coords as 54 | strings. This test shows that the problem isn't the Feature class 55 | (which uses pyutil.jsonutil) serializing the coords incorrectly. 56 | 57 | [ SimpleGeo Nerds ] 58 | 59 | -- SimpleGeo Nerds Thu, 10 Feb 2011 22:47:00 +0000 60 | 61 | python-simplegeo-shared (2.5.79) lucid; urgency=low 62 | 63 | [ Ben Standefer ] 64 | * Add annotations endpoint. 65 | * Add annotations endpoint. 66 | * Add validation and tests for annotations. 67 | * Fix function names in annotations tests. 68 | 69 | [ SimpleGeo Nerds ] 70 | 71 | -- SimpleGeo Nerds Tue, 08 Feb 2011 17:31:57 +0000 72 | 73 | python-simplegeo-shared (2.5.78) lucid; urgency=low 74 | 75 | * UNRELEASED 76 | 77 | -- SimpleGeo Nerds Fri, 04 Feb 2011 17:52:10 +0000 78 | 79 | python-simplegeo-shared (2.5.77) lucid; urgency=low 80 | 81 | [ Zooko Ofsimplegeo ] 82 | * update version to 2.5 83 | * make the error message on input validation easier to understand 84 | (thanks to Kim for reporting this) loosen to_unicode() to handle 85 | utf-8 encoded bytes instead of requiring ascii 86 | 87 | [ SimpleGeo Nerds ] 88 | 89 | -- SimpleGeo Nerds Fri, 04 Feb 2011 17:40:56 +0000 90 | 91 | python-simplegeo-shared (2.4.72) lucid; urgency=low 92 | 93 | [ Zooko Ofsimplegeo ] 94 | * change version number from 2.3 to 2.4 95 | 96 | [ SimpleGeo Nerds ] 97 | 98 | -- SimpleGeo Nerds Mon, 24 Jan 2011 22:16:34 +0000 99 | 100 | python-simplegeo-shared (2.3.71) lucid; urgency=low 101 | 102 | * UNRELEASED 103 | 104 | -- SimpleGeo Nerds Mon, 24 Jan 2011 22:10:31 +0000 105 | 106 | python-simplegeo-shared (2.3.70) lucid; urgency=low 107 | 108 | [ Zooko Ofsimplegeo ] 109 | * raise the requirement on the version of python-oauth2 to v1.5 That 110 | version has better handling unicode which we rely on. 111 | 112 | [ SimpleGeo Nerds ] 113 | 114 | -- SimpleGeo Nerds Mon, 24 Jan 2011 22:09:34 +0000 115 | 116 | python-simplegeo-shared (2.3.69) lucid; urgency=low 117 | 118 | * UNRELEASED 119 | 120 | -- SimpleGeo Nerds Mon, 24 Jan 2011 22:03:44 +0000 121 | 122 | python-simplegeo-shared (2.3.68) lucid; urgency=low 123 | 124 | * UNRELEASED 125 | 126 | -- SimpleGeo Nerds Mon, 24 Jan 2011 20:28:37 +0000 127 | 128 | python-simplegeo-shared (2.3.67) lucid; urgency=low 129 | 130 | * UNRELEASED 131 | 132 | -- SimpleGeo Nerds Mon, 24 Jan 2011 19:50:34 +0000 133 | 134 | python-simplegeo-shared (2.3.66) lucid; urgency=low 135 | 136 | * UNRELEASED 137 | 138 | -- SimpleGeo Nerds Mon, 24 Jan 2011 18:36:39 +0000 139 | 140 | python-simplegeo-shared (2.3.65) lucid; urgency=low 141 | 142 | * UNRELEASED 143 | 144 | -- SimpleGeo Nerds Mon, 24 Jan 2011 17:42:00 +0000 145 | 146 | python-simplegeo-shared (2.3.64) lucid; urgency=low 147 | 148 | * UNRELEASED 149 | 150 | -- SimpleGeo Nerds Mon, 24 Jan 2011 17:34:45 +0000 151 | 152 | python-simplegeo-shared (2.3.63) lucid; urgency=low 153 | 154 | * UNRELEASED 155 | 156 | -- SimpleGeo Nerds Mon, 24 Jan 2011 17:22:21 +0000 157 | 158 | python-simplegeo-shared (2.3.62) lucid; urgency=low 159 | 160 | * UNRELEASED 161 | 162 | -- SimpleGeo Nerds Fri, 21 Jan 2011 19:43:31 +0000 163 | 164 | python-simplegeo-shared (2.3.60) lucid; urgency=low 165 | 166 | * UNRELEASED 167 | 168 | -- SimpleGeo Nerds Fri, 21 Jan 2011 19:32:22 +0000 169 | 170 | python-simplegeo-shared (2.3.59) lucid; urgency=low 171 | 172 | * UNRELEASED 173 | 174 | -- SimpleGeo Nerds Tue, 18 Jan 2011 17:36:06 +0000 175 | 176 | python-simplegeo-shared (2.3.58) lucid; urgency=low 177 | 178 | [ Zooko Ofsimplegeo ] 179 | * add "private" field to properties, default to False, add doc 180 | * raise instructive exception if the data argument is neither a 181 | unicode argument nor a str containing an ascii-encoded string 182 | * * loosen validation on longitudes to allow longitudes up to 360 183 | degrees away from the Prime Meridian (optionally -- you can request 184 | stricter validation) * do lat/lon validation on Feature.from_dict() 185 | * validation failures raise TypeError, not AssertionError, and 186 | validation proceeds even if PYTHONOPTIMIZE * add unit test of 187 | to_unicode() * raise version number to 2.1 188 | * fix test cases after merge 189 | 190 | [ SimpleGeo Nerds ] 191 | 192 | -- SimpleGeo Nerds Tue, 18 Jan 2011 17:33:15 +0000 193 | 194 | python-simplegeo-shared (2.2.57) lucid; urgency=low 195 | 196 | * UNRELEASED 197 | 198 | -- SimpleGeo Nerds Tue, 18 Jan 2011 16:43:58 +0000 199 | 200 | python-simplegeo-shared (2.2.56) lucid; urgency=low 201 | 202 | * UNRELEASED 203 | 204 | -- SimpleGeo Nerds Tue, 18 Jan 2011 16:41:00 +0000 205 | 206 | python-simplegeo-shared (2.2.55) lucid; urgency=low 207 | 208 | [ Zooko Ofsimplegeo ] 209 | * raise instructive exception if the data argument is neither a 210 | unicode argument nor a str containing an ascii-encoded string 211 | 212 | [ SimpleGeo Nerds ] 213 | 214 | -- SimpleGeo Nerds Mon, 10 Jan 2011 23:11:34 +0000 215 | 216 | python-simplegeo-shared (2.2.54) lucid; urgency=low 217 | 218 | [ Zooko Ofsimplegeo ] 219 | * test the repr of DecodeError separately from the tests of the client 220 | 221 | [ SimpleGeo Nerds ] 222 | 223 | -- SimpleGeo Nerds Thu, 06 Jan 2011 18:01:04 +0000 224 | 225 | python-simplegeo-shared (2.2.53) lucid; urgency=low 226 | 227 | * UNRELEASED 228 | 229 | -- SimpleGeo Nerds Tue, 04 Jan 2011 23:03:05 +0000 230 | 231 | python-simplegeo-shared (2.2.52) lucid; urgency=low 232 | 233 | * UNRELEASED 234 | 235 | -- SimpleGeo Nerds Tue, 04 Jan 2011 19:12:49 +0000 236 | 237 | python-simplegeo-shared (2.2.51) lucid; urgency=low 238 | 239 | * UNRELEASED 240 | 241 | -- SimpleGeo Nerds Tue, 04 Jan 2011 18:50:17 +0000 242 | 243 | python-simplegeo-shared (2.2.50) lucid; urgency=low 244 | 245 | * UNRELEASED 246 | 247 | -- SimpleGeo Nerds Thu, 23 Dec 2010 17:28:52 +0000 248 | 249 | python-simplegeo-shared (2.2.49) lucid; urgency=low 250 | 251 | [ Zooko Ofsimplegeo ] 252 | * add docstring explaining the "private" key for the properties dict, 253 | and a default setting of private=False We should double-check with 254 | Joe that this is the correct default setting. 255 | * update test to reflect the presence of the "private" key 256 | 257 | [ SimpleGeo Nerds ] 258 | 259 | -- SimpleGeo Nerds Wed, 22 Dec 2010 18:11:09 +0000 260 | 261 | python-simplegeo-shared (2.2.47) lucid; urgency=low 262 | 263 | [ Zooko Ofsimplegeo ] 264 | * relax version requirement on ipaddr to 2.0.0 since that is the 265 | version on PyPI 266 | 267 | [ SimpleGeo Nerds ] 268 | 269 | -- SimpleGeo Nerds Wed, 22 Dec 2010 14:36:53 +0000 270 | 271 | python-simplegeo-shared (2.2.45) lucid; urgency=low 272 | 273 | [ Zooko ] 274 | * avoid tickling an obscure bug in ipaddr in our unit test 275 | http://code.google.com/p/ipaddr-py/issues/detail?id=71 276 | 277 | [ SimpleGeo Nerds ] 278 | 279 | -- SimpleGeo Nerds Wed, 22 Dec 2010 05:46:53 +0000 280 | 281 | python-simplegeo-shared (2.2.44) lucid; urgency=low 282 | 283 | [ Zooko ] 284 | * add a TODO about strange Debian naming convention 285 | 286 | [ SimpleGeo Nerds ] 287 | 288 | -- SimpleGeo Nerds Wed, 22 Dec 2010 02:05:53 +0000 289 | 290 | python-simplegeo-shared (2.2.43) lucid; urgency=low 291 | 292 | [ Zooko Ofsimplegeo ] 293 | * add unit test of is_valid_ip(), raise requirement on pyutil to >= 294 | 1.8.1 to workaround bad metadata in its packaging in pyutil v1.7.9 295 | and v1.8.0, bump version number to 2.2.36 296 | * parse new separately-written manual and automated version strings 297 | 298 | [ SimpleGeo Nerds ] 299 | 300 | -- SimpleGeo Nerds Wed, 22 Dec 2010 00:04:32 +0000 301 | 302 | python-simplegeo-shared (2.1.37) lucid; urgency=low 303 | 304 | [ Zooko Ofsimplegeo ] 305 | * M-x whitespace-cleanup 306 | * add is_valid_ip(), add dependency on ipaddr, bump version to 2.1.36 307 | 308 | [ SimpleGeo Nerds ] 309 | 310 | -- SimpleGeo Nerds Tue, 21 Dec 2010 21:24:53 +0000 311 | 312 | python-simplegeo-shared (2.0.36) lucid; urgency=low 313 | 314 | * UNRELEASED 315 | 316 | -- SimpleGeo Nerds Mon, 20 Dec 2010 18:17:58 +0000 317 | 318 | python-simplegeo-shared (2.0.34) lucid; urgency=low 319 | 320 | [ Zooko Ofsimplegeo ] 321 | * we require the "extra" functionality of pyutil which is jsonutil 322 | (Other users of pyutil, which don't use jsonutil, don't need pyutil 323 | to depend on simplejson.) 324 | 325 | [ SimpleGeo Nerds ] 326 | 327 | -- SimpleGeo Nerds Mon, 20 Dec 2010 17:30:45 +0000 328 | 329 | python-simplegeo-shared (2.0.33) lucid; urgency=low 330 | 331 | [ ansate ] 332 | * set headers to None before request, and test that this works 333 | 334 | [ SimpleGeo Nerds ] 335 | 336 | -- SimpleGeo Nerds Wed, 15 Dec 2010 23:36:22 +0000 337 | 338 | python-simplegeo-shared (2.0.32) lucid; urgency=low 339 | 340 | [ Zooko Ofsimplegeo ] 341 | * pyutil's _assert() acts a lot like assert but still gets checked 342 | when -O (and also gives more information about things when the 343 | expression evaluates to False) 344 | * this bound doesn't need to be a float when the rest are int 345 | 346 | [ SimpleGeo Nerds ] 347 | 348 | -- SimpleGeo Nerds Wed, 15 Dec 2010 23:33:23 +0000 349 | 350 | python-simplegeo-shared (2.0.29) lucid; urgency=low 351 | 352 | [ Zooko Ofsimplegeo ] 353 | * make error message more clear if a Feature is constructed with the 354 | wrong type of arguments -- having "coordinates" shaped wrong is the 355 | more common mistake (thanks Andrew Mager) 356 | 357 | [ SimpleGeo Nerds ] 358 | 359 | -- SimpleGeo Nerds Wed, 15 Dec 2010 22:49:22 +0000 360 | 361 | python-simplegeo-shared (2.0.28) lucid; urgency=low 362 | 363 | [ Zooko Ofsimplegeo ] 364 | * a couple of tiny cleanups in the setup 365 | * more detailed error message if coordinates fail to validate as being 366 | a set of lat, lon pairs 367 | 368 | [ SimpleGeo Nerds ] 369 | 370 | -- SimpleGeo Nerds Tue, 14 Dec 2010 23:05:02 +0000 371 | 372 | python-simplegeo-shared (2.0.27) lucid; urgency=low 373 | 374 | * UNRELEASED 375 | 376 | -- SimpleGeo Nerds Tue, 14 Dec 2010 20:30:53 +0000 377 | 378 | python-simplegeo-shared (2.0.26) lucid; urgency=low 379 | 380 | * UNRELEASED 381 | 382 | -- SimpleGeo Nerds Tue, 14 Dec 2010 19:52:11 +0000 383 | 384 | python-simplegeo-shared (2.0.22) lucid; urgency=low 385 | 386 | * UNRELEASED 387 | 388 | -- SimpleGeo Nerds Mon, 13 Dec 2010 22:37:30 +0000 389 | 390 | python-simplegeo-shared (2.0.21) lucid; urgency=low 391 | 392 | [ Zooko Ofsimplegeo ] 393 | * README.txt -- link to sister libraries 394 | * build with pyflakes and better coverage 395 | * input validation on lats and lons being within bounds 396 | * handle arbitary nested GeoJSON objects (including the feared 397 | MultiPolygon) and bring all Python objects into SimpleGeo schematic 398 | of lat,lon and convert to/from GeoJSON lon,lat as needed 399 | * build dep pyflakes 400 | * fix mistake in debian/rules 401 | 402 | [ SimpleGeo Nerds ] 403 | 404 | -- SimpleGeo Nerds Wed, 08 Dec 2010 07:27:30 +0000 405 | 406 | python-simplegeo-shared (1.13.16) lucid; urgency=low 407 | 408 | [ Zooko Ofsimplegeo ] 409 | * add get_most_recent_headers() 410 | 411 | [ SimpleGeo Nerds ] 412 | 413 | -- SimpleGeo Nerds Wed, 08 Dec 2010 03:46:54 +0000 414 | 415 | python-simplegeo-shared (1.13.15) lucid; urgency=low 416 | 417 | [ Zooko Ofsimplegeo ] 418 | * remove endpoints 419 | 420 | [ SimpleGeo Nerds ] 421 | 422 | -- SimpleGeo Nerds Wed, 08 Dec 2010 03:42:55 +0000 423 | 424 | python-simplegeo-shared (1.13.14) lucid; urgency=low 425 | 426 | [ ansate ] 427 | * incremented version since we made major changes that child libs will 428 | need to use 429 | 430 | [ SimpleGeo Nerds ] 431 | 432 | -- SimpleGeo Nerds Wed, 08 Dec 2010 03:28:19 +0000 433 | 434 | python-simplegeo-shared (1.12.12) lucid; urgency=low 435 | 436 | [ ansate ] 437 | * Use Feature instead of Record Move the record tests into shared from 438 | places Update Feature class to handle features that are Polygon 439 | geometries as well as Points Cleanup some unused code 440 | 441 | [ SimpleGeo Nerds ] 442 | 443 | -- SimpleGeo Nerds Wed, 08 Dec 2010 03:19:54 +0000 444 | 445 | python-simplegeo-shared (1.12.11) lucid; urgency=low 446 | 447 | * UNRELEASED 448 | 449 | -- SimpleGeo Nerds Wed, 08 Dec 2010 00:46:54 +0000 450 | 451 | python-simplegeo-shared (1.12.10) lucid; urgency=low 452 | 453 | [ Zooko Ofsimplegeo ] 454 | * copy in the general-purpose parts from places to shared 455 | 456 | [ SimpleGeo Nerds ] 457 | 458 | -- SimpleGeo Nerds Tue, 07 Dec 2010 22:12:46 +0000 459 | 460 | python-simplegeo-shared (1.11.8) lucid; urgency=low 461 | 462 | [ Simple Geebus ] 463 | * 464 | 465 | [ Zooko Ofsimplegeo ] 466 | * setup: set test suite so "python setup.py test" works 467 | 468 | [ SimpleGeo Nerds ] 469 | 470 | -- SimpleGeo Nerds Tue, 07 Dec 2010 01:09:54 +0000 471 | 472 | python-simplegeo-shared (1.11.7) lucid; urgency=low 473 | 474 | [ Zooko ] 475 | * add TODO.rst 476 | 477 | [ SimpleGeo Nerds ] 478 | 479 | -- SimpleGeo Nerds Sat, 04 Dec 2010 00:31:26 +0000 480 | 481 | python-simplegeo-shared (1.11.6) lucid; urgency=low 482 | 483 | [ Ian Eure ] 484 | * Use pysupport & pyutil. 485 | 486 | [ SimpleGeo Nerds ] 487 | 488 | -- SimpleGeo Nerds Fri, 03 Dec 2010 23:48:27 +0000 489 | 490 | python-simplegeo-shared (1.11.5) lucid; urgency=low 491 | 492 | [ Zooko Ofsimplegeo ] 493 | * version bump 494 | 495 | [ SimpleGeo Nerds ] 496 | 497 | -- SimpleGeo Nerds Fri, 03 Dec 2010 21:54:37 +0000 498 | 499 | python-simplegeo-shared (1.9.30) lucid; urgency=low 500 | 501 | [ Zooko ] 502 | * tweak docs in setup.py 503 | 504 | [ SimpleGeo Nerds ] 505 | 506 | -- SimpleGeo Nerds Fri, 03 Dec 2010 19:15:18 +0000 507 | 508 | python-simplegeo-shared (1.9.29) lucid; urgency=low 509 | 510 | [ Zooko ] 511 | * copy in debian/rules from gnop project (minus filthy hack) 512 | * fix flakes 513 | 514 | [ SimpleGeo Nerds ] 515 | 516 | -- SimpleGeo Nerds Fri, 03 Dec 2010 18:57:46 +0000 517 | 518 | python-simplegeo-shared (1.9.28) lucid; urgency=low 519 | 520 | [ Zooko ] 521 | * copy in Makefile from gnop project and modify it a bit 522 | 523 | [ SimpleGeo Nerds ] 524 | 525 | -- SimpleGeo Nerds Fri, 03 Dec 2010 18:42:37 +0000 526 | 527 | python-simplegeo-shared (1.9.27) lucid; urgency=low 528 | 529 | [ Zooko ] 530 | * see what happens if we don't override the cdbs install target 531 | 532 | [ SimpleGeo Nerds ] 533 | 534 | -- SimpleGeo Nerds Fri, 03 Dec 2010 18:33:07 +0000 535 | 536 | python-simplegeo-shared (1.9.25) lucid; urgency=low 537 | 538 | [ Simple Geebus ] 539 | * 540 | 541 | [ Zooko ] 542 | * copy in the entire contents of gnop and delete the entire contents 543 | of python-simplegeo-shared in order to see if the build works when 544 | it is identical to the working gnop build, then I'll incrementa lly 545 | change it back to simplegeo-shared and see when the build breaks Oh 546 | except, gnop isn't open source. So I'll just delete the actual 547 | bodies of the methods real quick... 548 | * just changing something to watch it change 549 | * random thing to make sure hudson notices pending has changed 550 | * put python-simplegeo-shared back now that I figured out what was 551 | wrong with the hudson config 552 | 553 | [ SimpleGeo Nerds ] 554 | 555 | -- SimpleGeo Nerds Fri, 03 Dec 2010 18:15:53 +0000 556 | 557 | python-simplegeo-shared (1.9.6) lucid; urgency=low 558 | 559 | [ Zooko Ofsimplegeo ] 560 | * don't express more precision than we actually have in version number 561 | (the 3rd element is controlled by hudson not by source code) 562 | * don't express more precision than we actually have in version number 563 | (the 3rd element is controlled by hudson not by source code) 564 | 565 | [ SimpleGeo Nerds ] 566 | 567 | -- SimpleGeo Nerds Wed, 01 Dec 2010 23:24:18 +0000 568 | 569 | python-simplegeo-shared (1.9.4) lucid; urgency=low 570 | 571 | [ Zooko Ofsimplegeo ] 572 | * add docstring to make it clearer that this is (probably) the 573 | server's fault 574 | * loosen test wrt the exact error message... 575 | 576 | [ SimpleGeo Nerds ] 577 | 578 | -- SimpleGeo Nerds Wed, 01 Dec 2010 21:43:18 +0000 579 | 580 | python-simplegeo-shared (1.9.2) lucid; urgency=low 581 | 582 | [ Zooko Ofsimplegeo ] 583 | * version 1.9.0 584 | 585 | [ SimpleGeo Nerds ] 586 | 587 | -- SimpleGeo Nerds Wed, 01 Dec 2010 19:40:18 +0000 588 | 589 | python-simplegeo-shared (1.2.11) lucid; urgency=low 590 | 591 | * UNRELEASED 592 | 593 | -- SimpleGeo Nerds Tue, 30 Nov 2010 21:38:39 +0000 594 | 595 | python-simplegeo-shared (1.2.10) lucid; urgency=low 596 | 597 | [ ansate ] 598 | * added some more tests, fixed the old ones. removed a bunch of code 599 | 600 | [ SimpleGeo Nerds ] 601 | 602 | -- SimpleGeo Nerds Tue, 30 Nov 2010 21:37:42 +0000 603 | 604 | python-simplegeo-shared (0.9.1) unstable; urgency=low 605 | 606 | * first working version 607 | 608 | -- Zooko Wilcox-O'Hearn Sat, 22 Nov 2010 10:01:10 -0700 609 | 610 | python-simplegeo-shared (0.9.0) unstable; urgency=low 611 | 612 | * initial version 613 | 614 | -- Zooko Wilcox-O'Hearn Sat, 12 Nov 2010 10:01:10 -0700 615 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 7 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: python-simplegeo-shared 2 | Section: python 3 | Priority: extra 4 | Maintainer: SimpleGeo Nerds 5 | Build-Depends: debhelper (>= 7), cdbs (>= 0.4.59), python (>= 2.6), python-support (>= 1.0.4), python-setuptools (>= 0.6.10), python-coverage (>= 2.85-1), python-simplejson (>= 2.1.0), python-pyutil (>= 1.8.1), python-oauth2 (>= 1.5), python-httplib2 (>= 0.6.0), pyflakes (>= 0.4), python-ipaddr (>= 2.0.0) 6 | Standards-Version: 3.8.4 7 | Homepage: http://github.com/simplegeo/python-simplegeo-shared/ 8 | 9 | Package: python-simplegeo-shared 10 | Architecture: all 11 | Depends: ${misc:Depends}, ${python:Depends}, python-simplejson (>= 2.1.0), python-pyutil (>= 1.7.9), python-oauth2 (>= 1.5), python-httplib2 (>= 0.6.0), python-ipaddr (>= 2.0.0) 12 | Description: Python library for the SimpleGeo API. 13 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | (c) 2009, 2010 SimpleGeo, Inc. All rights reserved. -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | README.txt 2 | -------------------------------------------------------------------------------- /debian/pycompat: -------------------------------------------------------------------------------- 1 | 2.6 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | ARTIFACTS ?= . 4 | DEB_PYTHON_SYSTEM=pysupport 5 | 6 | include /usr/share/cdbs/1/rules/debhelper.mk 7 | include /usr/share/cdbs/1/class/python-distutils.mk 8 | 9 | build/python-simplegeo-shared:: 10 | pyflakes simplegeo/shared 11 | python-coverage run --branch --include=simplegeo/* setup.py test 12 | python-coverage html -d $(ARTIFACTS)/htmlcov 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | 3 | # test = nosetests 4 | # test = trial --reporter=bwverbose-coverage 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | import os, re 4 | 5 | PKG='simplegeo-shared' 6 | VERSIONFILE = os.path.join('simplegeo', 'shared', '_version.py') 7 | verstr = "unknown" 8 | try: 9 | verstrline = open(VERSIONFILE, "rt").read() 10 | except EnvironmentError: 11 | pass # Okay, there is no version file. 12 | else: 13 | MVSRE = r"^manual_verstr *= *['\"]([^'\"]*)['\"]" 14 | mo = re.search(MVSRE, verstrline, re.M) 15 | if mo: 16 | mverstr = mo.group(1) 17 | else: 18 | print "unable to find version in %s" % (VERSIONFILE,) 19 | raise RuntimeError("if %s.py exists, it must be well-formed" % (VERSIONFILE,)) 20 | AVSRE = r"^auto_build_num *= *['\"]([^'\"]*)['\"]" 21 | mo = re.search(AVSRE, verstrline, re.M) 22 | if mo: 23 | averstr = mo.group(1) 24 | else: 25 | averstr = '' 26 | verstr = '.'.join([mverstr, averstr]) 27 | 28 | setup_requires = [] 29 | tests_require = ['mock'] 30 | 31 | # nosetests is an optional way to get code-coverage results. Uncomment 32 | # the following and run "python setup.py nosetests --with-coverage. 33 | # --cover-erase --cover-tests --cover-inclusive --cover-html" 34 | # tests_require.extend(['coverage', 'nose']) 35 | 36 | # trialcoverage is another optional way to get code-coverage 37 | # results. Uncomment the following and run "python setup.py trial 38 | # --reporter=bwverbose-coverage -s simplegeo.shared.test". 39 | # setup_requires.append('setuptools_trial') 40 | # tests_require.extend(['setuptools_trial', 'trialcoverage']) 41 | 42 | # As of 2010-11-22 neither of the above options appear to work to 43 | # generate code coverage results, but the following does: 44 | # rm -rf ./.coverage* htmlcov ; coverage run --branch --include=simplegeo/* setup.py test ; coverage html 45 | 46 | setup(name=PKG, 47 | version=verstr, 48 | description="Library for interfacing with SimpleGeo's API", 49 | author="Zooko Wilcox-O'Hearn", 50 | author_email="zooko@simplegeo.com", 51 | url="http://github.com/simplegeo/python-simplegeo-shared", 52 | packages = find_packages(), 53 | license = "MIT License", 54 | install_requires=['httplib2>=0.6.0', 'oauth2>=1.5', 'pyutil[jsonutil] >= 1.8.1', 'ipaddr >= 2.0.0'], 55 | keywords="simplegeo", 56 | zip_safe=False, # actually it is zip safe, but zipping packages doesn't help with anything and can cause some problems (http://bugs.python.org/setuptools/issue33 ) 57 | namespace_packages = ['simplegeo'], 58 | test_suite='simplegeo.shared.test', 59 | setup_requires=setup_requires, 60 | tests_require=tests_require) 61 | -------------------------------------------------------------------------------- /simplegeo/__init__.py: -------------------------------------------------------------------------------- 1 | __import__('pkg_resources').declare_namespace(__name__) 2 | -------------------------------------------------------------------------------- /simplegeo/shared/__init__.py: -------------------------------------------------------------------------------- 1 | from _version import __version__ 2 | 3 | API_VERSION = '1.0' 4 | 5 | import copy, re 6 | from decimal import Decimal as D 7 | 8 | from httplib2 import Http 9 | import oauth2 as oauth 10 | 11 | import ipaddr 12 | 13 | from urlparse import urljoin 14 | 15 | from pyutil import jsonutil as json 16 | from pyutil.assertutil import precondition, _assert 17 | 18 | # example: http://api.simplegeo.com/1.0/feature/abcdefghijklmnopqrstuvwyz.json 19 | 20 | def json_decode(jsonstr): 21 | try: 22 | return json.loads(jsonstr) 23 | except (ValueError, TypeError), le: 24 | raise DecodeError(jsonstr, le) 25 | 26 | def swap(tupleab): 27 | return (tupleab[1], tupleab[0]) 28 | 29 | def deep_swap(struc): 30 | if is_numeric(struc[0]): 31 | _assert (len(struc) == 2, (type(struc), repr(struc))) 32 | _assert (is_numeric(struc[1]), (type(struc), repr(struc))) 33 | return swap(struc) 34 | return [deep_swap(sub) for sub in struc] 35 | 36 | def deep_validate_lat_lon(struc, strict_lon_validation=False): 37 | """ 38 | For the meaning of strict_lon_validation, please see the function 39 | is_valid_lon(). 40 | """ 41 | if not isinstance(struc, (list, tuple, set)): 42 | raise TypeError('argument is required to be a sequence (of sequences of...) numbers, not: %s :: %s' % (struc, type(struc))) 43 | if is_numeric(struc[0]): 44 | if not len(struc) == 2: 45 | raise TypeError("The leaf element of this structure is required to be a tuple of length 2 (to hold a lat and lon).") 46 | 47 | _assert_valid_lat(struc[0]) 48 | _assert_valid_lon(struc[1], strict=strict_lon_validation) 49 | else: 50 | for sub in struc: 51 | deep_validate_lat_lon(sub, strict_lon_validation=strict_lon_validation) 52 | return True 53 | 54 | SIMPLEGEOHANDLE_RSTR=r"""SG_[A-Za-z0-9]{22}(?:_-?[0-9]{1,3}(?:\.[0-9]+)?_-?[0-9]{1,3}(?:\.[0-9]+)?)?(?:@[0-9]+)?$""" 55 | SIMPLEGEOHANDLE_R= re.compile(SIMPLEGEOHANDLE_RSTR) 56 | def is_simplegeohandle(s): 57 | return isinstance(s, basestring) and SIMPLEGEOHANDLE_R.match(s) 58 | 59 | FEATURES_URL_R=re.compile("http://(.*)/features/([A-Za-z_,-]*).json$") 60 | 61 | def is_numeric(x): 62 | return isinstance(x, (int, long, float, D)) 63 | 64 | def is_valid_lat(x): 65 | return is_numeric(x) and (x <= 90) and (x >= -90) 66 | 67 | def _assert_valid_lat(x): 68 | if not is_valid_lat(x): 69 | raise TypeError("not a valid lat: %s" % (x,)) 70 | 71 | def _assert_valid_lon(x, strict=False): 72 | if not is_valid_lon(x, strict=strict): 73 | raise TypeError("not a valid lon (strict=%s): %s" % (strict, x,)) 74 | 75 | def is_valid_lon(x, strict=False): 76 | """ 77 | Longitude is technically defined as extending from -180 to 78 | 180. However in practice people sometimes prefer to use longitudes 79 | which have "wrapped around" past 180. For example, if you are 80 | drawing a polygon around modern-day Russia almost all of it is in 81 | the Eastern Hemisphere, which means its longitudes are almost all 82 | positive numbers, but the easternmost part of it (Big Diomede 83 | Island) lies a few degrees east of the International Date Line, 84 | and it is sometimes more convenient to describe it as having 85 | longitude 190.9 instead of having longitude -169.1. 86 | 87 | If strict=True then is_valid_lon() requires a number to be in 88 | [-180..180] to be considered a valid longitude. If strict=False 89 | (the default) then it requires the number to be in [-360..360]. 90 | """ 91 | if strict: 92 | return is_numeric(x) and (x <= 180) and (x >= -180) 93 | else: 94 | return is_numeric(x) and (x <= 360) and (x >= -360) 95 | 96 | def to_unicode(s): 97 | """ Convert to unicode, raise exception with instructive error 98 | message if s is not unicode, ascii, or utf-8. """ 99 | if not isinstance(s, unicode): 100 | if not isinstance(s, str): 101 | raise TypeError('You are required to pass either unicode or string here, not: %r (%s)' % (type(s), s)) 102 | try: 103 | s = s.decode('utf-8') 104 | except UnicodeDecodeError, le: 105 | raise TypeError('You are required to pass either a unicode object or a utf-8 string here. You passed a Python string object which contained non-utf-8: %r. The UnicodeDecodeError that resulted from attempting to interpret it as utf-8 was: %s' % (s, le,)) 106 | return s 107 | 108 | class Feature: 109 | def __init__(self, coordinates, geomtype='Point', simplegeohandle=None, properties=None, strict_lon_validation=False): 110 | """ 111 | The simplegeohandle and the record_id are both optional -- you 112 | can have one or the other or both or neither. 113 | 114 | A simplegeohandle is globally unique and is assigned by the 115 | Places service. It is returned from the Places service in the 116 | response to a request to add a place to the Places database 117 | (the add_feature method). 118 | 119 | The simplegeohandle is passed in as an argument to the 120 | constructor, named "simplegeohandle", and is stored in the 121 | "id" attribute of the Feature instance. 122 | 123 | A record_id is scoped to your particular user account and is 124 | chosen by you. The only use for the record_id is in case you 125 | call add_feature and you have already previously added that 126 | feature to the database -- if there is already a feature from 127 | your user account with the same record_id then the Places 128 | service will return that feature to you, along with that 129 | feature's simplegeohandle, instead of making a second, duplicate 130 | feature. 131 | 132 | A record_id is passed in as a value in the properties dict 133 | named "record_id". 134 | 135 | geomtype is a GeoJSON geometry type such as "Point", 136 | "Polygon", or "Multipolygon". coordinates is a GeoJSON 137 | coordinates *except* that each lat/lon pair is written in 138 | order lat, lon instead of the GeoJSON order of lon, at. 139 | 140 | When a Feature is being submitted to the SimpleGeo Places 141 | database, if there is a key 'private' in the properties dict 142 | which is set to True, then the Feature is intended to be 143 | visible only to your user account. If there is no 'private' 144 | key or if there is a 'private' key which is set to False, then 145 | the Feature is intended to be merged into the publicly visible 146 | Places Database. 147 | 148 | Note that even if it is intended to be merged into the public 149 | Places Database the actual process of merging it into the 150 | public shared database may take some time, and the newly added 151 | Feature will be visible to your account right away even if it 152 | isn't (yet) visible to the public. 153 | 154 | For the meaning of strict_lon_validation, please see the 155 | function is_valid_lon(). 156 | """ 157 | try: 158 | deep_validate_lat_lon(coordinates, strict_lon_validation=strict_lon_validation) 159 | except TypeError, le: 160 | raise TypeError("The first argument, 'coordinates' is required to be a 2-element sequence of lon, lat for a point (or a more complicated set of coordinates for polygons or multipolygons), but it was %s :: %r. The error that was raised from validating this was: %s" % (type(coordinates), coordinates, le)) 161 | 162 | if not (simplegeohandle is None or is_simplegeohandle(simplegeohandle)): 163 | raise TypeError("The third argument, 'simplegeohandle' is required to be None or to match this regex: %s, but it was %s :: %r" % (SIMPLEGEOHANDLE_RSTR, type(simplegeohandle), simplegeohandle)) 164 | 165 | record_id = properties and properties.get('record_id') or None 166 | if not (record_id is None or isinstance(record_id, basestring)): 167 | raise TypeError("record_id is required to be None or a string, but it was: %r :: %s." % (type(record_id), record_id)) 168 | self.strict_lon_validation = strict_lon_validation 169 | precondition(coordinates) 170 | self.id = simplegeohandle 171 | self.coordinates = coordinates 172 | self.geomtype = geomtype 173 | self.properties = {'private': False} 174 | if properties: 175 | self.properties.update(properties) 176 | 177 | @classmethod 178 | def from_dict(cls, data, strict_lon_validation=False): 179 | """ 180 | data is a GeoJSON standard data structure, including that the 181 | coordinates are in GeoJSON order (lon, lat) instead of 182 | SimpleGeo order (lat, lon) 183 | """ 184 | assert isinstance(data, dict), (type(data), repr(data)) 185 | coordinates = deep_swap(data['geometry']['coordinates']) 186 | try: 187 | deep_validate_lat_lon(coordinates, strict_lon_validation=strict_lon_validation) 188 | except TypeError, le: 189 | raise TypeError("The 'coordinates' value is required to be a 2-element sequence of lon, lat for a point (or a more complicated set of coordinates for polygons or multipolygons), but it was %s :: %r. The error that was raised from validating this was: %s" % (type(coordinates), coordinates, le)) 190 | feature = cls( 191 | simplegeohandle = data.get('id'), 192 | coordinates = coordinates, 193 | geomtype = data['geometry']['type'], 194 | properties = data.get('properties') 195 | ) 196 | 197 | return feature 198 | 199 | def to_dict(self): 200 | """ 201 | Returns a GeoJSON object, including having its coordinates in 202 | GeoJSON standad order (lon, lat) instead of SimpleGeo standard 203 | order (lat, lon). 204 | """ 205 | return { 206 | 'type': 'Feature', 207 | 'id': self.id, 208 | 'geometry': { 209 | 'type': self.geomtype, 210 | 'coordinates': deep_swap(self.coordinates) 211 | }, 212 | 'properties': copy.deepcopy(self.properties), 213 | } 214 | 215 | @classmethod 216 | def from_json(cls, jsonstr): 217 | return cls.from_dict(json_decode(jsonstr)) 218 | 219 | def to_json(self): 220 | return json.dumps(self.to_dict()) 221 | 222 | 223 | class Client(object): 224 | realm = "http://api.simplegeo.com" 225 | endpoints = { 226 | 'feature': 'features/%(simplegeohandle)s.json', 227 | 'annotations': 'features/%(simplegeohandle)s/annotations.json', 228 | } 229 | 230 | def __init__(self, key, secret, api_version=API_VERSION, host="api.simplegeo.com", port=80): 231 | self.host = host 232 | self.port = port 233 | self.consumer = oauth.Consumer(key, secret) 234 | self.key = key 235 | self.secret = secret 236 | self.api_version = api_version 237 | self.signature = oauth.SignatureMethod_HMAC_SHA1() 238 | self.uri = "http://%s:%s" % (host, port) 239 | self.http = Http() 240 | self.headers = None 241 | 242 | def get_most_recent_http_headers(self): 243 | """ Intended for debugging -- return the most recent HTTP 244 | headers which were received from the server. """ 245 | return self.headers 246 | 247 | def _endpoint(self, name, **kwargs): 248 | """Not used directly. Finds and formats the endpoints as needed for any type of request.""" 249 | try: 250 | endpoint = self.endpoints[name] 251 | except KeyError: 252 | raise Exception('No endpoint named "%s"' % name) 253 | try: 254 | endpoint = endpoint % kwargs 255 | except KeyError, e: 256 | raise TypeError('Missing required argument "%s"' % (e.args[0],)) 257 | return urljoin(urljoin(self.uri, self.api_version + '/'), endpoint) 258 | 259 | def get_feature(self, simplegeohandle): 260 | """Return the GeoJSON representation of a feature.""" 261 | if not is_simplegeohandle(simplegeohandle): 262 | raise TypeError("simplegeohandle is required to match the regex %s, but it was %s :: %r" % (SIMPLEGEOHANDLE_RSTR, type(simplegeohandle), simplegeohandle)) 263 | endpoint = self._endpoint('feature', simplegeohandle=simplegeohandle) 264 | return Feature.from_json(self._request(endpoint, 'GET')[1]) 265 | 266 | def get_annotations(self, simplegeohandle): 267 | if not is_simplegeohandle(simplegeohandle): 268 | raise TypeError("simplegeohandle is required to match the regex %s, but it was %s :: %r" % (SIMPLEGEOHANDLE_RSTR, type(simplegeohandle), simplegeohandle)) 269 | endpoint = self._endpoint('annotations', simplegeohandle=simplegeohandle) 270 | return json.loads(self._request(endpoint, 'GET')[1]) 271 | 272 | def annotate(self, simplegeohandle, annotations, private): 273 | if not isinstance(annotations, dict): 274 | raise TypeError('annotations must be of type dict') 275 | if not len(annotations.keys()): 276 | raise ValueError('annotations dict is empty') 277 | for annotation_type in annotations.keys(): 278 | if not len(annotations[annotation_type].keys()): 279 | raise ValueError('annotation type "%s" is empty' % annotation_type) 280 | if not isinstance(private, bool): 281 | raise TypeError('private must be of type bool') 282 | 283 | data = {'annotations': annotations, 284 | 'private': private} 285 | 286 | endpoint = self._endpoint('annotations', simplegeohandle=simplegeohandle) 287 | return json.loads(self._request(endpoint, 288 | 'POST', 289 | data=json.dumps(data))[1]) 290 | 291 | def _request(self, endpoint, method, data=None): 292 | """ 293 | Not used directly by code external to this lib. Performs the 294 | actual request against the API, including passing the 295 | credentials with oauth. Returns a tuple of (headers as dict, 296 | body as string). 297 | """ 298 | if data is not None: 299 | data = to_unicode(data) 300 | params = {} 301 | body = data 302 | request = oauth.Request.from_consumer_and_token(self.consumer, 303 | http_method=method, http_url=endpoint, parameters=params) 304 | 305 | request.sign_request(self.signature, self.consumer, None) 306 | headers = request.to_header(self.realm) 307 | headers['User-Agent'] = 'SimpleGeo Places Client v%s' % __version__ 308 | 309 | self.headers, content = self.http.request(endpoint, method, body=body, headers=headers) 310 | 311 | if self.headers['status'][0] not in ('2', '3'): 312 | raise APIError(int(self.headers['status']), content, self.headers) 313 | 314 | return self.headers, content 315 | 316 | 317 | class APIError(Exception): 318 | """Base exception for all API errors.""" 319 | 320 | def __init__(self, code, msg, headers, description=''): 321 | self.code = code 322 | self.msg = msg 323 | self.headers = headers 324 | self.description = description 325 | 326 | def __str__(self): 327 | return self.__repr__() 328 | 329 | def __repr__(self): 330 | return "%s (#%s) %s" % (self.msg, self.code, self.description) 331 | 332 | class DecodeError(APIError): 333 | """There was a problem decoding the API's response, which was 334 | supposed to be encoded in JSON, but which apparently wasn't.""" 335 | 336 | def __init__(self, body, le): 337 | super(DecodeError, self).__init__(None, "Could not decode JSON from server.", None, repr(le)) 338 | self.body = body 339 | 340 | def __repr__(self): 341 | return "%s content: %s" % (self.description, self.body) 342 | 343 | def is_valid_ip(ip): 344 | try: 345 | ipaddr.IPAddress(ip) 346 | except ValueError: 347 | return False 348 | else: 349 | return True 350 | -------------------------------------------------------------------------------- /simplegeo/shared/_version.py: -------------------------------------------------------------------------------- 1 | # This is the version of this source code. 2 | 3 | manual_verstr = "2.5" 4 | 5 | 6 | 7 | auto_build_num = "127" 8 | 9 | 10 | 11 | verstr = manual_verstr + "." + auto_build_num 12 | try: 13 | from pyutil.version_class import Version as pyutil_Version 14 | __version__ = pyutil_Version(verstr) 15 | except (ImportError, ValueError): 16 | # Maybe there is no pyutil installed. 17 | from distutils.version import LooseVersion as distutils_Version 18 | __version__ = distutils_Version(verstr) 19 | -------------------------------------------------------------------------------- /simplegeo/shared/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simplegeo/python-simplegeo-shared/bb4b32f81958145e81a26d369a1476ea9ec2904c/simplegeo/shared/test/__init__.py -------------------------------------------------------------------------------- /simplegeo/shared/test/test_client.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pyutil import jsonutil as json 3 | from simplegeo.shared import Client, APIError, DecodeError, Feature, is_valid_lat, is_valid_lon, is_valid_ip, to_unicode 4 | 5 | from decimal import Decimal as D 6 | 7 | import mock 8 | 9 | MY_OAUTH_KEY = 'MY_OAUTH_KEY' 10 | MY_OAUTH_SECRET = 'MY_SECRET_KEY' 11 | TESTING_LAYER = 'TESTING_LAYER' 12 | 13 | API_VERSION = '1.0' 14 | API_HOST = 'api.simplegeo.com' 15 | API_PORT = 80 16 | 17 | class ReallyEqualMixin: 18 | def failUnlessReallyEqual(self, a, b, msg='', *args, **kwargs): 19 | self.failUnlessEqual(a, b, msg, *args, **kwargs) 20 | self.failUnlessEqual(type(a), type(b), msg="a :: %r, b :: %r, %r" % (a, b, msg), *args, **kwargs) 21 | 22 | class ToUnicodeTest(unittest.TestCase, ReallyEqualMixin): 23 | def test_to_unicode(self): 24 | self.failUnlessReallyEqual(to_unicode('x'), u'x') 25 | self.failUnlessReallyEqual(to_unicode(u'x'), u'x') 26 | self.failUnlessReallyEqual(to_unicode('\xe2\x9d\xa4'), u'\u2764') 27 | 28 | class LatLonValidationTest(unittest.TestCase): 29 | 30 | def test_is_valid_lon(self): 31 | self.failUnless(is_valid_lon(180, strict=True)) 32 | self.failUnless(is_valid_lon(180.0, strict=True)) 33 | self.failUnless(is_valid_lon(D('180.0'), strict=True)) 34 | self.failUnless(is_valid_lon(-180, strict=True)) 35 | self.failUnless(is_valid_lon(-180.0, strict=True)) 36 | self.failUnless(is_valid_lon(D('-180.0'), strict=True)) 37 | 38 | self.failIf(is_valid_lon(180.0002, strict=True)) 39 | self.failIf(is_valid_lon(D('180.0002'), strict=True)) 40 | self.failIf(is_valid_lon(-180.0002, strict=True)) 41 | self.failIf(is_valid_lon(D('-180.0002'), strict=True)) 42 | 43 | self.failUnless(is_valid_lon(180.0002, strict=False)) 44 | self.failUnless(is_valid_lon(D('180.0002'), strict=False)) 45 | self.failUnless(is_valid_lon(-180.0002, strict=False)) 46 | self.failUnless(is_valid_lon(D('-180.0002'), strict=False)) 47 | 48 | self.failIf(is_valid_lon(360.0002, strict=False)) 49 | self.failIf(is_valid_lon(D('360.0002'), strict=False)) 50 | self.failIf(is_valid_lon(-360.0002, strict=False)) 51 | self.failIf(is_valid_lon(D('-360.0002'), strict=False)) 52 | 53 | def test_is_valid_lat(self): 54 | self.failUnless(is_valid_lat(90)) 55 | self.failUnless(is_valid_lat(90.0)) 56 | self.failUnless(is_valid_lat(D('90.0'))) 57 | self.failUnless(is_valid_lat(-90)) 58 | self.failUnless(is_valid_lat(-90.0)) 59 | self.failUnless(is_valid_lat(D('-90.0'))) 60 | 61 | self.failIf(is_valid_lat(90.0002)) 62 | self.failIf(is_valid_lat(D('90.0002'))) 63 | self.failIf(is_valid_lat(-90.0002)) 64 | self.failIf(is_valid_lat(D('-90.0002'))) 65 | 66 | class DecodeErrorTest(unittest.TestCase): 67 | def test_repr(self): 68 | body = 'this is not json' 69 | try: 70 | json.loads('this is not json') 71 | except ValueError, le: 72 | e = DecodeError(body, le) 73 | else: 74 | self.fail("We were supposed to get an exception from json.loads().") 75 | 76 | self.failUnless("Could not decode JSON" in e.msg, repr(e.msg)) 77 | self.failUnless('JSONDecodeError' in repr(e), repr(e)) 78 | 79 | class ClientTest(unittest.TestCase): 80 | def setUp(self): 81 | self.client = Client(MY_OAUTH_KEY, MY_OAUTH_SECRET, API_VERSION, API_HOST, API_PORT) 82 | self.query_lat = D('37.8016') 83 | self.query_lon = D('-122.4783') 84 | 85 | def test_is_valid_ip(self): 86 | self.failUnless(is_valid_ip('192.0.32.10')) 87 | self.failIf(is_valid_ip('i am not an ip address at all')) 88 | 89 | def test_wrong_endpoint(self): 90 | self.assertRaises(Exception, self.client._endpoint, 'wrongwrong') 91 | 92 | def test_missing_argument(self): 93 | self.assertRaises(Exception, self.client._endpoint, 'feature') 94 | 95 | def test_get_feature_useful_validation_error_message(self): 96 | c = Client('whatever', 'whatever') 97 | try: 98 | c.get_feature('wrong thing') 99 | except TypeError, e: 100 | self.failUnless(str(e).startswith('simplegeohandle is required to match '), str(e)) 101 | else: 102 | self.fail('Should have raised exception.') 103 | 104 | def test_get_most_recent_http_headers(self): 105 | h = self.client.get_most_recent_http_headers() 106 | self.failUnlessEqual(h, None) 107 | 108 | mockhttp = mock.Mock() 109 | mockhttp.request.return_value = ({'status': '200', 'content-type': 'application/json', 'thingie': "just to see if you're listening"}, EXAMPLE_POINT_BODY) 110 | self.client.http = mockhttp 111 | 112 | self.client.get_feature("SG_4bgzicKFmP89tQFGLGZYy0_34.714646_-86.584970") 113 | h = self.client.get_most_recent_http_headers() 114 | self.failUnlessEqual(h, {'status': '200', 'content-type': 'application/json', 'thingie': "just to see if you're listening"}) 115 | 116 | def test_get_point_feature(self): 117 | mockhttp = mock.Mock() 118 | mockhttp.request.return_value = ({'status': '200', 'content-type': 'application/json', 'thingie': "just to see if you're listening"}, EXAMPLE_POINT_BODY) 119 | self.client.http = mockhttp 120 | 121 | res = self.client.get_feature("SG_4bgzicKFmP89tQFGLGZYy0_34.714646_-86.584970") 122 | self.assertEqual(mockhttp.method_calls[0][0], 'request') 123 | self.assertEqual(mockhttp.method_calls[0][1][0], 'http://api.simplegeo.com:80/%s/features/%s.json' % (API_VERSION, "SG_4bgzicKFmP89tQFGLGZYy0_34.714646_-86.584970")) 124 | self.assertEqual(mockhttp.method_calls[0][1][1], 'GET') 125 | # the code under test is required to have json-decoded this before handing it back 126 | self.failUnless(isinstance(res, Feature), (repr(res), type(res))) 127 | 128 | def test_get_polygon_feature(self): 129 | mockhttp = mock.Mock() 130 | mockhttp.request.return_value = ({'status': '200', 'content-type': 'application/json', }, EXAMPLE_BODY) 131 | self.client.http = mockhttp 132 | 133 | res = self.client.get_feature("SG_4bgzicKFmP89tQFGLGZYy0_34.714646_-86.584970") 134 | self.assertEqual(mockhttp.method_calls[0][0], 'request') 135 | self.assertEqual(mockhttp.method_calls[0][1][0], 'http://api.simplegeo.com:80/%s/features/%s.json' % (API_VERSION, "SG_4bgzicKFmP89tQFGLGZYy0_34.714646_-86.584970")) 136 | 137 | self.assertEqual(mockhttp.method_calls[0][1][1], 'GET') 138 | # the code under test is required to have json-decoded this before handing it back 139 | self.failUnless(isinstance(res, Feature), (repr(res), type(res))) 140 | 141 | def test_type_check_request(self): 142 | self.failUnlessRaises(TypeError, self.client._request, 'whatever', 'POST', {'bogus': "non string"}) 143 | 144 | def test_type_check_unicode_data(self): 145 | self.failUnlessRaises(TypeError, self.client._request, 'whatever', 'POST', 'str with nonascii char \x92 in it') 146 | 147 | def test_get_feature_bad_json(self): 148 | mockhttp = mock.Mock() 149 | mockhttp.request.return_value = ({'status': '200', 'content-type': 'application/json', }, EXAMPLE_BODY + 'some crap') 150 | self.client.http = mockhttp 151 | 152 | try: 153 | self.client.get_feature("SG_4bgzicKFmP89tQFGLGZYy0_34.714646_-86.584970") 154 | except DecodeError, e: 155 | self.failUnlessEqual(e.code, None, repr(e.code)) 156 | 157 | self.assertEqual(mockhttp.method_calls[0][0], 'request') 158 | self.assertEqual(mockhttp.method_calls[0][1][0], 'http://api.simplegeo.com:80/%s/features/%s.json' % (API_VERSION, "SG_4bgzicKFmP89tQFGLGZYy0_34.714646_-86.584970")) 159 | self.assertEqual(mockhttp.method_calls[0][1][1], 'GET') 160 | 161 | def test_dont_json_decode_results(self): 162 | """ _request() is required to return the exact string that the HTTP 163 | server sent to it -- no transforming it, such as by json-decoding. """ 164 | 165 | mockhttp = mock.Mock() 166 | mockhttp.request.return_value = ({'status': '200', 'content-type': 'application/json', }, '{ "Hello": "I am a string. \xe2\x9d\xa4" }'.decode('utf-8')) 167 | self.client.http = mockhttp 168 | res = self.client._request("http://thing", 'POST')[1] 169 | self.failUnlessEqual(res, '{ "Hello": "I am a string. \xe2\x9d\xa4" }'.decode('utf-8')) 170 | 171 | def test_dont_Recordify_results(self): 172 | """ _request() is required to return the exact string that the HTTP 173 | server sent to it -- no transforming it, such as by json-decoding and 174 | then constructing a Record. """ 175 | 176 | EXAMPLE_RECORD_JSONSTR=json.dumps({ 'geometry' : { 'type' : 'Point', 'coordinates' : [D('10.0'), D('11.0')] }, 'id' : 'my_id', 'type' : 'Feature', 'properties' : { 'key' : 'value' , 'type' : 'object' } }) 177 | 178 | mockhttp = mock.Mock() 179 | mockhttp.request.return_value = ({'status': '200', 'content-type': 'application/json', }, EXAMPLE_RECORD_JSONSTR) 180 | self.client.http = mockhttp 181 | res = self.client._request("http://thing", 'POST')[1] 182 | self.failUnlessEqual(res, EXAMPLE_RECORD_JSONSTR) 183 | 184 | def test_get_feature_error(self): 185 | mockhttp = mock.Mock() 186 | mockhttp.request.return_value = ({'status': '500', 'content-type': 'application/json', }, '{"message": "help my web server is confuzzled"}') 187 | self.client.http = mockhttp 188 | 189 | try: 190 | self.client.get_feature("SG_4bgzicKFmP89tQFGLGZYy0_34.714646_-86.584970") 191 | except APIError, e: 192 | self.failUnlessEqual(e.code, 500, repr(e.code)) 193 | self.failUnlessEqual(e.msg, '{"message": "help my web server is confuzzled"}', (type(e.msg), repr(e.msg))) 194 | 195 | self.assertEqual(mockhttp.method_calls[0][0], 'request') 196 | self.assertEqual(mockhttp.method_calls[0][1][0], 'http://api.simplegeo.com:80/%s/features/%s.json' % (API_VERSION, "SG_4bgzicKFmP89tQFGLGZYy0_34.714646_-86.584970")) 197 | self.assertEqual(mockhttp.method_calls[0][1][1], 'GET') 198 | 199 | def test_APIError(self): 200 | e = APIError(500, 'whee', {'status': "500"}) 201 | self.failUnlessEqual(e.code, 500) 202 | self.failUnlessEqual(e.msg, 'whee') 203 | repr(e) 204 | str(e) 205 | 206 | EXAMPLE_POINT_BODY=""" 207 | {"geometry":{"type":"Point","coordinates":[-105.048054,40.005274]},"type":"Feature","id":"SG_6sRJczWZHdzNj4qSeRzpzz_40.005274_-105.048054@1291669259","properties":{"province":"CO","city":"Erie","name":"CMD Colorado Inc","tags":["sandwich"],"country":"US","phone":"+1 303 664 9448","address":"305 Baron Ct","owner":"simplegeo","classifiers":[{"category":"Restaurants","type":"Food & Drink","subcategory":""}],"postcode":"80516"}} 208 | """ 209 | 210 | EXAMPLE_BODY=""" 211 | {"geometry":{"type":"Polygon","coordinates":[[[-86.3672637,33.4041157],[-86.3676356,33.4039745],[-86.3681259,33.40365],[-86.3685992,33.4034242],[-86.3690556,33.4031137],[-86.3695121,33.4027609],[-86.3700361,33.4024363],[-86.3705601,33.4021258],[-86.3710166,33.4018012],[-86.3715575,33.4014061],[-86.3720647,33.4008557],[-86.3724366,33.4005311],[-86.3730621,33.3998395],[-86.3733156,33.3992891],[-86.3735523,33.3987811],[-86.3737383,33.3983153],[-86.3739073,33.3978355],[-86.374144,33.3971016],[-86.3741609,33.3968758],[-86.3733494,33.3976943],[-86.3729606,33.3980189],[-86.3725211,33.3984141],[-86.3718111,33.3990069],[-86.3713378,33.399402],[-86.370949,33.3997266],[-86.3705094,33.3999948],[-86.3701206,33.4003899],[-86.3697487,33.4007287],[-86.369157,33.4012791],[-86.3687682,33.401646],[-86.3684132,33.4019847],[-86.368092,33.4023798],[-86.3676694,33.4028738],[-86.3674835,33.4033113],[-86.3672975,33.4037487],[-86.3672637,33.4041157],[-86.3672637,33.4041157]]]},"type":"Feature","properties":{"category":"Island","license":"http://creativecommons.org/licenses/by-sa/2.0/","handle":"SG_4b10i9vCyPnKAYiYBLKZN7_33.400800_-86.370802","subcategory":"","name":"Elliott Island","attribution":"(c) OpenStreetMap (http://openstreetmap.org/) and contributors CC-BY-SA (http://creativecommons.org/licenses/by-sa/2.0/)","type":"Physical Feature","abbr":""},"id":"SG_4b10i9vCyPnKAYiYBLKZN7"} 212 | """ 213 | 214 | class TestAnnotations(unittest.TestCase): 215 | 216 | def setUp(self): 217 | self.client = Client(MY_OAUTH_KEY, MY_OAUTH_SECRET, API_VERSION, API_HOST, API_PORT) 218 | self.handle = 'SG_4H2GqJDZrc0ZAjKGR8qM4D' 219 | 220 | def test_get_annotations(self): 221 | mockhttp = mock.Mock() 222 | headers = {'status': '200', 'content-type': 'application/json'} 223 | mockhttp.request.return_value = (headers, json.dumps(EXAMPLE_ANNOTATIONS_RESPONSE)) 224 | self.client.http = mockhttp 225 | 226 | res = self.client.get_annotations(self.handle) 227 | 228 | self.assertEqual(mockhttp.method_calls[0][0], 'request') 229 | self.assertEqual(mockhttp.method_calls[0][1][0], 'http://api.simplegeo.com:80/%s/features/%s/annotations.json' % (API_VERSION, self.handle)) 230 | self.assertEqual(mockhttp.method_calls[0][1][1], 'GET') 231 | 232 | # Make sure client returns a dict. 233 | self.failUnless(isinstance(res, dict)) 234 | 235 | def test_get_annotations_bad_handle(self): 236 | try: 237 | self.client.get_annotations('bad_handle') 238 | except TypeError, e: 239 | self.failUnless(str(e).startswith('simplegeohandle is required to match the regex')) 240 | else: 241 | self.fail('Should have raised exception.') 242 | 243 | def test_annotate(self): 244 | mockhttp = mock.Mock() 245 | headers = {'status': '200', 'content-type': 'application/json'} 246 | mockhttp.request.return_value = (headers, json.dumps(EXAMPLE_ANNOTATE_RESPONSE)) 247 | self.client.http = mockhttp 248 | 249 | res = self.client.annotate(self.handle, EXAMPLE_ANNOTATIONS, True) 250 | 251 | self.assertEqual(mockhttp.method_calls[0][0], 'request') 252 | self.assertEqual(mockhttp.method_calls[0][1][0], 'http://api.simplegeo.com:80/%s/features/%s/annotations.json' % (API_VERSION, self.handle)) 253 | self.assertEqual(mockhttp.method_calls[0][1][1], 'POST') 254 | 255 | # Make sure client returns a dict. 256 | self.failUnless(isinstance(res, dict)) 257 | 258 | def test_annotate_bad_annotations_type(self): 259 | annotations = 'not_a_dict' 260 | try: 261 | self.client.annotate(self.handle, annotations, True) 262 | except TypeError, e: 263 | self.failUnless(str(e) == 'annotations must be of type dict') 264 | else: 265 | self.fail('Should have raised exception.') 266 | 267 | def test_annotate_empty_annotations_dict(self): 268 | annotations = {} 269 | try: 270 | self.client.annotate(self.handle, annotations, True) 271 | except ValueError, e: 272 | self.failUnless(str(e) == 'annotations dict is empty') 273 | else: 274 | self.fail('Should have raised exception.') 275 | 276 | def test_annotate_empty_annotation_type_dict(self): 277 | annotations = { 278 | 'annotation_type_1': { 279 | 'foo': 'bar'}, 280 | 'annotation_type_2': { 281 | } 282 | } 283 | try: 284 | self.client.annotate(self.handle, annotations, True) 285 | except ValueError, e: 286 | self.failUnless(str(e) == 'annotation type "annotation_type_2" is empty') 287 | else: 288 | self.fail('Should have raised exception.') 289 | 290 | def test_annotate_private_type(self): 291 | try: 292 | self.client.annotate(self.handle, EXAMPLE_ANNOTATIONS, 'not_a_bool') 293 | except TypeError, e: 294 | self.failUnless(str(e) == 'private must be of type bool') 295 | else: 296 | self.fail('Should have raised exception.') 297 | 298 | 299 | EXAMPLE_ANNOTATIONS_RESPONSE = { 300 | 'private': { 301 | 'venue': { 302 | 'profitable': 'yes', 303 | 'owner': 'John Doe'}, 304 | 'building': { 305 | 'condition': 'poor'} 306 | }, 307 | 'public': { 308 | 'venue': { 309 | 'capacity': '28,037', 310 | 'activity': 'sports'}, 311 | 'building': { 312 | 'size': 'extra small', 313 | 'material': 'wood', 314 | 'ground': 'grass'} 315 | } 316 | } 317 | 318 | EXAMPLE_ANNOTATIONS = { 319 | 'venue': { 320 | 'profitable': 'yes', 321 | 'owner': 'John Doe'}, 322 | 'building': { 323 | 'condition': 'poor'} 324 | } 325 | 326 | EXAMPLE_ANNOTATE_RESPONSE = {'status': 'success'} 327 | -------------------------------------------------------------------------------- /simplegeo/shared/test/test_record.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import re 3 | from simplegeo.shared import Feature, deep_swap 4 | from decimal import Decimal as D 5 | 6 | class FeatureTest(unittest.TestCase): 7 | 8 | def test_geojson_is_correct(self): 9 | f = Feature(coordinates=[-90, D('171.0')], properties={'record_id': 'my_id'}, strict_lon_validation=True) 10 | stringy = f.to_json() 11 | self.failUnlessEqual(stringy, '{"geometry": {"type": "Point", "coordinates": [171.0, -90]}, "type": "Feature", "id": null, "properties": {"record_id": "my_id", "private": false}}') 12 | 13 | def test_swapper(self): 14 | t1 = (2, 1) 15 | self.failUnlessEqual(deep_swap(t1), (1, 2)) 16 | 17 | linestring1 = [(2, 1), (4, 3), (6, 5), (8, 7)] 18 | self.failUnlessEqual( 19 | deep_swap(linestring1), 20 | [(1, 2), (3, 4), (5, 6), (7, 8)] 21 | ) 22 | 23 | multipolygon1 = [ 24 | [[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]], 25 | [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]], 26 | [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]] 27 | ] 28 | 29 | self.failUnlessEqual(deep_swap(multipolygon1), 30 | [[[(2.0, 102.0), (2.0, 103.0), (3.0, 103.0), (3.0, 102.0), (2.0, 102.0)]], [[(0.0, 100.0), (0.0, 101.0), (1.0, 101.0), (1.0, 100.0), (0.0, 100.0)], [(0.2, 100.2), (0.2, 100.8), (0.8, 100.8), (0.8, 100.2), (0.2, 100.2)]]] 31 | ) 32 | 33 | def test_record_constructor_useful_validation_error_message(self): 34 | try: 35 | Feature(coordinates=[181, D('10.0')], properties={'record_id': 'my_id'}) 36 | except TypeError, e: 37 | self.failUnless(str(e).startswith('The first argument'), str(e)) 38 | self.failUnless('is required to be a 2-element sequence' in str(e), str(e)) 39 | else: 40 | self.fail('Should have raised exception.') 41 | 42 | try: 43 | Feature(coordinates=[-90, D('181.0')], properties={'record_id': 'my_id'}, strict_lon_validation=True) 44 | except TypeError, e: 45 | self.failUnless('181' in str(e), str(e)) 46 | else: 47 | self.fail('Should have raised exception.') 48 | 49 | try: 50 | Feature(coordinates=[-90, D('361.0')], properties={'record_id': 'my_id'}) 51 | except TypeError, e: 52 | self.failUnless('361' in str(e), str(e)) 53 | else: 54 | self.fail('Should have raised exception.') 55 | 56 | try: 57 | Feature(coordinates=['-90', D('361.0')], properties={'record_id': 'my_id'}) 58 | except TypeError, e: 59 | err_msg_re = re.compile("argument is required to be.*number.*not: .*