Package Dropbox :: Package web2py :: Package gluon :: Module globals
[hide private]
[frames] | no frames]

Source Code for Module Dropbox.web2py.gluon.globals

  1  #!/usr/bin/env python 
  2  # -*- coding: utf-8 -*- 
  3   
  4  """ 
  5  This file is part of the web2py Web Framework 
  6  Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu> 
  7  License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) 
  8   
  9  Contains the classes for the global used variables: 
 10   
 11  - Request 
 12  - Response 
 13  - Session 
 14   
 15  """ 
 16   
 17  from storage import Storage, List 
 18  from streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE 
 19  from xmlrpc import handler 
 20  from contenttype import contenttype 
 21  from html import xmlescape, TABLE, TR, PRE, URL 
 22  from http import HTTP, redirect 
 23  from fileutils import up 
 24  from serializers import json, custom_json 
 25  import settings 
 26  from utils import web2py_uuid, secure_dumps, secure_loads 
 27  from settings import global_settings 
 28  import hashlib 
 29  import portalocker 
 30  import cPickle 
 31  import cStringIO 
 32  import datetime 
 33  import re 
 34  import Cookie 
 35  import os 
 36  import sys 
 37  import traceback 
 38  import threading 
 39   
 40  FMT = '%a, %d-%b-%Y %H:%M:%S PST' 
 41  PAST = 'Sat, 1-Jan-1971 00:00:00' 
 42  FUTURE = 'Tue, 1-Dec-2999 23:59:59' 
 43   
 44  try: 
 45      from gluon.contrib.minify import minify 
 46      have_minify = True 
 47  except ImportError: 
 48      have_minify = False 
 49   
 50  regex_session_id = re.compile('^([\w\-]+/)?[\w\-\.]+$') 
 51   
 52  __all__ = ['Request', 'Response', 'Session'] 
 53   
 54  current = threading.local()  # thread-local storage for request-scope globals 
 55   
 56  css_template = '<link href="%s" rel="stylesheet" type="text/css" />' 
 57  js_template = '<script src="%s" type="text/javascript"></script>' 
 58  coffee_template = '<script src="%s" type="text/coffee"></script>' 
 59  less_template = '<link href="%s" rel="stylesheet/less" type="text/css" />' 
 60  css_inline = '<style type="text/css">\n%s\n</style>' 
 61  js_inline = '<script type="text/javascript">\n%s\n</script>' 
 62   
 63   
64 -class Request(Storage):
65 66 """ 67 defines the request object and the default values of its members 68 69 - env: environment variables, by gluon.main.wsgibase() 70 - cookies 71 - get_vars 72 - post_vars 73 - vars 74 - folder 75 - application 76 - function 77 - args 78 - extension 79 - now: datetime.datetime.today() 80 - restful() 81 """ 82
83 - def __init__(self):
84 Storage.__init__(self) 85 self.wsgi = Storage() # hooks to environ and start_response 86 self.env = Storage() 87 self.cookies = Cookie.SimpleCookie() 88 self.get_vars = Storage() 89 self.post_vars = Storage() 90 self.vars = Storage() 91 self.folder = None 92 self.application = None 93 self.function = None 94 self.args = List() 95 self.extension = 'html' 96 self.now = datetime.datetime.now() 97 self.utcnow = datetime.datetime.utcnow() 98 self.is_restful = False 99 self.is_https = False 100 self.is_local = False 101 self.global_settings = settings.global_settings
102
103 - def compute_uuid(self):
104 self.uuid = '%s/%s.%s.%s' % ( 105 self.application, 106 self.client.replace(':', '_'), 107 self.now.strftime('%Y-%m-%d.%H-%M-%S'), 108 web2py_uuid()) 109 return self.uuid
110
111 - def user_agent(self):
112 from gluon.contrib import user_agent_parser 113 session = current.session 114 user_agent = session._user_agent or \ 115 user_agent_parser.detect(self.env.http_user_agent) 116 if session: 117 session._user_agent = user_agent 118 user_agent = Storage(user_agent) 119 for key, value in user_agent.items(): 120 if isinstance(value, dict): 121 user_agent[key] = Storage(value) 122 return user_agent
123
124 - def requires_https(self):
125 """ 126 If request comes in over HTTP, redirect it to HTTPS 127 and secure the session. 128 """ 129 cmd_opts = global_settings.cmd_options 130 #checking if this is called within the scheduler or within the shell 131 #in addition to checking if it's not a cronjob 132 if ((cmd_opts and (cmd_opts.shell or cmd_opts.scheduler)) 133 or global_settings.cronjob or self.is_https): 134 current.session.secure() 135 else: 136 current.session.forget() 137 redirect(URL(scheme='https', args=self.args, vars=self.vars))
138 139 140
141 - def restful(self):
142 def wrapper(action, self=self): 143 def f(_action=action, _self=self, *a, **b): 144 self.is_restful = True 145 method = _self.env.request_method 146 if len(_self.args) and '.' in _self.args[-1]: 147 _self.args[- 148 1], _self.extension = _self.args[-1].rsplit('.', 1) 149 current.response.headers['Content-Type'] = \ 150 contenttype(_self.extension.lower()) 151 if not method in ['GET', 'POST', 'DELETE', 'PUT']: 152 raise HTTP(400, "invalid method") 153 rest_action = _action().get(method, None) 154 if not rest_action: 155 raise HTTP(400, "method not supported") 156 try: 157 return rest_action(*_self.args, **_self.vars) 158 except TypeError, e: 159 exc_type, exc_value, exc_traceback = sys.exc_info() 160 if len(traceback.extract_tb(exc_traceback)) == 1: 161 raise HTTP(400, "invalid arguments") 162 else: 163 raise e
164 f.__doc__ = action.__doc__ 165 f.__name__ = action.__name__ 166 return f
167 return wrapper 168 169
170 -class Response(Storage):
171 172 """ 173 defines the response object and the default values of its members 174 response.write( ) can be used to write in the output html 175 """ 176
177 - def __init__(self):
178 Storage.__init__(self) 179 self.status = 200 180 self.headers = dict() 181 self.headers['X-Powered-By'] = 'web2py' 182 self.body = cStringIO.StringIO() 183 self.session_id = None 184 self.cookies = Cookie.SimpleCookie() 185 self.postprocessing = [] 186 self.flash = '' # used by the default view layout 187 self.meta = Storage() # used by web2py_ajax.html 188 self.menu = [] # used by the default view layout 189 self.files = [] # used by web2py_ajax.html 190 self.generic_patterns = [] # patterns to allow generic views 191 self.delimiters = ('{{', '}}') 192 self._vars = None 193 self._caller = lambda f: f() 194 self._view_environment = None 195 self._custom_commit = None 196 self._custom_rollback = None
197
198 - def write(self, data, escape=True):
199 if not escape: 200 self.body.write(str(data)) 201 else: 202 self.body.write(xmlescape(data))
203
204 - def render(self, *a, **b):
205 from compileapp import run_view_in 206 if len(a) > 2: 207 raise SyntaxError( 208 'Response.render can be called with two arguments, at most') 209 elif len(a) == 2: 210 (view, self._vars) = (a[0], a[1]) 211 elif len(a) == 1 and isinstance(a[0], str): 212 (view, self._vars) = (a[0], {}) 213 elif len(a) == 1 and hasattr(a[0], 'read') and callable(a[0].read): 214 (view, self._vars) = (a[0], {}) 215 elif len(a) == 1 and isinstance(a[0], dict): 216 (view, self._vars) = (None, a[0]) 217 else: 218 (view, self._vars) = (None, {}) 219 self._vars.update(b) 220 self._view_environment.update(self._vars) 221 if view: 222 import cStringIO 223 (obody, oview) = (self.body, self.view) 224 (self.body, self.view) = (cStringIO.StringIO(), view) 225 run_view_in(self._view_environment) 226 page = self.body.getvalue() 227 self.body.close() 228 (self.body, self.view) = (obody, oview) 229 else: 230 run_view_in(self._view_environment) 231 page = self.body.getvalue() 232 return page
233
234 - def include_meta(self):
235 s = '\n'.join( 236 '<meta name="%s" content="%s" />\n' % (k, xmlescape(v)) 237 for k, v in (self.meta or {}).iteritems()) 238 self.write(s, escape=False)
239
240 - def include_files(self, extensions=None):
241 242 """ 243 Caching method for writing out files. 244 By default, caches in ram for 5 minutes. To change, 245 response.cache_includes = (cache_method, time_expire). 246 Example: (cache.disk, 60) # caches to disk for 1 minute. 247 """ 248 from gluon import URL 249 250 files = [] 251 has_js = has_css = False 252 for item in self.files: 253 if extensions and not item.split('.')[-1] in extensions: 254 continue 255 if item in files: 256 continue 257 if item.endswith('.js'): 258 has_js = True 259 if item.endswith('.css'): 260 has_css = True 261 files.append(item) 262 263 if have_minify and ((self.optimize_css and has_css) or (self.optimize_js and has_js)): 264 # cache for 5 minutes by default 265 key = hashlib.md5(repr(files)).hexdigest() 266 267 cache = self.cache_includes or (current.cache.ram, 60 * 5) 268 269 def call_minify(files=files): 270 return minify.minify(files, 271 URL('static', 'temp'), 272 current.request.folder, 273 self.optimize_css, 274 self.optimize_js)
275 if cache: 276 cache_model, time_expire = cache 277 files = cache_model('response.files.minified/' + key, 278 call_minify, 279 time_expire) 280 else: 281 files = call_minify() 282 s = '' 283 for item in files: 284 if isinstance(item, str): 285 f = item.lower().split('?')[0] 286 if self.static_version: 287 item = item.replace( 288 '/static/', '/static/_%s/' % self.static_version, 1) 289 if f.endswith('.css'): 290 s += css_template % item 291 elif f.endswith('.js'): 292 s += js_template % item 293 elif f.endswith('.coffee'): 294 s += coffee_template % item 295 elif f.endswith('.less'): 296 s += less_template % item 297 elif isinstance(item, (list, tuple)): 298 f = item[0] 299 if f == 'css:inline': 300 s += css_inline % item[1] 301 elif f == 'js:inline': 302 s += js_inline % item[1] 303 self.write(s, escape=False)
304
305 - def stream( 306 self, 307 stream, 308 chunk_size=DEFAULT_CHUNK_SIZE, 309 request=None, 310 attachment=False, 311 filename=None, 312 ):
313 """ 314 if a controller function:: 315 316 return response.stream(file, 100) 317 318 the file content will be streamed at 100 bytes at the time 319 320 Optional kwargs: 321 (for custom stream calls) 322 attachment=True # Send as attachment. Usually creates a 323 # pop-up download window on browsers 324 filename=None # The name for the attachment 325 326 Note: for using the stream name (filename) with attachments 327 the option must be explicitly set as function parameter(will 328 default to the last request argument otherwise) 329 """ 330 331 headers = self.headers 332 # for attachment settings and backward compatibility 333 keys = [item.lower() for item in headers] 334 if attachment: 335 if filename is None: 336 attname = "" 337 else: 338 attname = filename 339 headers["Content-Disposition"] = \ 340 "attachment;filename=%s" % attname 341 342 if not request: 343 request = current.request 344 if isinstance(stream, (str, unicode)): 345 stream_file_or_304_or_206(stream, 346 chunk_size=chunk_size, 347 request=request, 348 headers=headers, 349 status=self.status) 350 351 # ## the following is for backward compatibility 352 if hasattr(stream, 'name'): 353 filename = stream.name 354 355 if filename and not 'content-type' in keys: 356 headers['Content-Type'] = contenttype(filename) 357 if filename and not 'content-length' in keys: 358 try: 359 headers['Content-Length'] = \ 360 os.path.getsize(filename) 361 except OSError: 362 pass 363 364 env = request.env 365 # Internet Explorer < 9.0 will not allow downloads over SSL unless caching is enabled 366 if request.is_https and isinstance(env.http_user_agent, str) and \ 367 not re.search(r'Opera', env.http_user_agent) and \ 368 re.search(r'MSIE [5-8][^0-9]', env.http_user_agent): 369 headers['Pragma'] = 'cache' 370 headers['Cache-Control'] = 'private' 371 372 if request and env.web2py_use_wsgi_file_wrapper: 373 wrapped = env.wsgi_file_wrapper(stream, chunk_size) 374 else: 375 wrapped = streamer(stream, chunk_size=chunk_size) 376 return wrapped
377
378 - def download(self, request, db, chunk_size=DEFAULT_CHUNK_SIZE, attachment=True):
379 """ 380 example of usage in controller:: 381 382 def download(): 383 return response.download(request, db) 384 385 downloads from http://..../download/filename 386 """ 387 388 if not request.args: 389 raise HTTP(404) 390 name = request.args[-1] 391 items = re.compile('(?P<table>.*?)\.(?P<field>.*?)\..*')\ 392 .match(name) 393 if not items: 394 raise HTTP(404) 395 (t, f) = (items.group('table'), items.group('field')) 396 try: 397 field = db[t][f] 398 except AttributeError: 399 raise HTTP(404) 400 try: 401 (filename, stream) = field.retrieve(name) 402 except IOError: 403 raise HTTP(404) 404 headers = self.headers 405 headers['Content-Type'] = contenttype(name) 406 if attachment: 407 headers['Content-Disposition'] = \ 408 'attachment; filename="%s"' % filename.replace('"','\"') 409 return self.stream(stream, chunk_size=chunk_size, request=request)
410
411 - def json(self, data, default=None):
412 return json(data, default=default or custom_json)
413
414 - def xmlrpc(self, request, methods):
415 """ 416 assuming:: 417 418 def add(a, b): 419 return a+b 420 421 if a controller function \"func\":: 422 423 return response.xmlrpc(request, [add]) 424 425 the controller will be able to handle xmlrpc requests for 426 the add function. Example:: 427 428 import xmlrpclib 429 connection = xmlrpclib.ServerProxy( 430 'http://hostname/app/contr/func') 431 print connection.add(3, 4) 432 433 """ 434 435 return handler(request, self, methods)
436
437 - def toolbar(self):
438 from html import DIV, SCRIPT, BEAUTIFY, TAG, URL, A 439 BUTTON = TAG.button 440 admin = URL("admin", "default", "design", 441 args=current.request.application) 442 from gluon.dal import DAL 443 dbstats = [] 444 dbtables = {} 445 infos = DAL.get_instances() 446 for k,v in infos.iteritems(): 447 dbstats.append(TABLE(*[TR(PRE(row[0]),'%.2fms' % 448 (row[1]*1000)) 449 for row in v['dbstats']])) 450 dbtables[k] = dict(defined=v['dbtables']['defined'] or '[no defined tables]', 451 lazy=v['dbtables']['lazy'] or '[no lazy tables]') 452 u = web2py_uuid() 453 backtotop = A('Back to top', _href="#totop-%s" % u) 454 return DIV( 455 BUTTON('design', _onclick="document.location='%s'" % admin), 456 BUTTON('request', 457 _onclick="jQuery('#request-%s').slideToggle()" % u), 458 BUTTON('response', 459 _onclick="jQuery('#response-%s').slideToggle()" % u), 460 BUTTON('session', 461 _onclick="jQuery('#session-%s').slideToggle()" % u), 462 BUTTON('db tables', 463 _onclick="jQuery('#db-tables-%s').slideToggle()" % u), 464 BUTTON('db stats', 465 _onclick="jQuery('#db-stats-%s').slideToggle()" % u), 466 DIV(BEAUTIFY(current.request), backtotop, 467 _class="hidden", _id="request-%s" % u), 468 DIV(BEAUTIFY(current.session), backtotop, 469 _class="hidden", _id="session-%s" % u), 470 DIV(BEAUTIFY(current.response), backtotop, 471 _class="hidden", _id="response-%s" % u), 472 DIV(BEAUTIFY(dbtables), backtotop, _class="hidden", 473 _id="db-tables-%s" % u), 474 DIV(BEAUTIFY( 475 dbstats), backtotop, _class="hidden", _id="db-stats-%s" % u), 476 SCRIPT("jQuery('.hidden').hide()"), _id="totop-%s" % u 477 )
478 479
480 -class Session(Storage):
481 482 """ 483 defines the session object and the default values of its members (None) 484 """ 485
486 - def connect( 487 self, 488 request=None, 489 response=None, 490 db=None, 491 tablename='web2py_session', 492 masterapp=None, 493 migrate=True, 494 separate=None, 495 check_client=False, 496 cookie_key=None, 497 cookie_expires=None, 498 compression_level=None 499 ):
500 """ 501 separate can be separate=lambda(session_name): session_name[-2:] 502 and it is used to determine a session prefix. 503 separate can be True and it is set to session_name[-2:] 504 """ 505 if request is None: 506 request = current.request 507 if response is None: 508 response = current.response 509 if separate is True: 510 separate = lambda session_name: session_name[-2:] 511 self._unlock(response) 512 if not masterapp: 513 masterapp = request.application 514 response.session_id_name = 'session_id_%s' % masterapp.lower() 515 response.session_data_name = 'session_data_%s' % masterapp.lower() 516 response.session_cookie_expires = cookie_expires 517 518 # Load session data from cookie 519 cookies = request.cookies 520 521 # check if there is a session_id in cookies 522 if response.session_id_name in cookies: 523 response.session_id = \ 524 cookies[response.session_id_name].value 525 else: 526 response.session_id = None 527 528 # check if there is session data in cookies 529 if response.session_data_name in cookies: 530 session_cookie_data = cookies[response.session_data_name].value 531 else: 532 session_cookie_data = None 533 534 # if we are supposed to use cookie based session data 535 if cookie_key: 536 response.session_storage_type = 'cookie' 537 response.session_cookie_key = cookie_key 538 response.session_cookie_compression_level = compression_level 539 if session_cookie_data: 540 data = secure_loads(session_cookie_data, cookie_key, 541 compression_level=compression_level) 542 if data: 543 self.update(data) 544 # else if we are supposed to use file based sessions 545 elif not db: 546 response.session_storage_type = 'file' 547 if global_settings.db_sessions is True \ 548 or masterapp in global_settings.db_sessions: 549 return 550 response.session_new = False 551 client = request.client and request.client.replace(':', '.') 552 if response.session_id: 553 if regex_session_id.match(response.session_id): 554 response.session_filename = \ 555 os.path.join(up(request.folder), masterapp, 556 'sessions', response.session_id) 557 else: 558 response.session_id = None 559 # do not try load the data from file is these was data in cookie 560 if response.session_id and not session_cookie_data: 561 # os.path.exists(response.session_filename): 562 try: 563 response.session_file = \ 564 open(response.session_filename, 'rb+') 565 try: 566 portalocker.lock(response.session_file, 567 portalocker.LOCK_EX) 568 response.session_locked = True 569 self.update(cPickle.load(response.session_file)) 570 response.session_file.seek(0) 571 oc = response.session_filename.split('/')[-1]\ 572 .split('-')[0] 573 if check_client and client != oc: 574 raise Exception("cookie attack") 575 except: 576 response.session_id = None 577 finally: 578 pass 579 #This causes admin login to break. Must find out why. 580 #self._close(response) 581 except: 582 response.session_file = None 583 if not response.session_id: 584 uuid = web2py_uuid() 585 response.session_id = '%s-%s' % (client, uuid) 586 if separate: 587 prefix = separate(response.session_id) 588 response.session_id = '%s/%s' % \ 589 (prefix, response.session_id) 590 response.session_filename = \ 591 os.path.join(up(request.folder), masterapp, 592 'sessions', response.session_id) 593 response.session_new = True 594 # else the session goes in db 595 else: 596 response.session_storage_type = 'db' 597 if global_settings.db_sessions is not True: 598 global_settings.db_sessions.add(masterapp) 599 if response.session_file: 600 self._close(response) 601 if settings.global_settings.web2py_runtime_gae: 602 # in principle this could work without GAE 603 request.tickets_db = db 604 if masterapp == request.application: 605 table_migrate = migrate 606 else: 607 table_migrate = False 608 tname = tablename + '_' + masterapp 609 table = db.get(tname, None) 610 Field = db.Field 611 if table is None: 612 db.define_table( 613 tname, 614 Field('locked', 'boolean', default=False), 615 Field('client_ip', length=64), 616 Field('created_datetime', 'datetime', 617 default=request.now), 618 Field('modified_datetime', 'datetime'), 619 Field('unique_key', length=64), 620 Field('session_data', 'blob'), 621 migrate=table_migrate, 622 ) 623 table = db[tname] # to allow for lazy table 624 try: 625 626 # Get session data out of the database 627 (record_id, unique_key) = response.session_id.split(':') 628 if record_id == '0': 629 raise Exception('record_id == 0') 630 # Select from database 631 if not session_cookie_data: 632 rows = db(table.id == record_id).select() 633 # Make sure the session data exists in the database 634 if len(rows) == 0 or rows[0].unique_key != unique_key: 635 raise Exception('No record') 636 # rows[0].update_record(locked=True) 637 # Unpickle the data 638 session_data = cPickle.loads(rows[0].session_data) 639 self.update(session_data) 640 except Exception: 641 record_id = None 642 unique_key = web2py_uuid() 643 session_data = {} 644 response.session_id = '%s:%s' % (record_id, unique_key) 645 response.session_db_table = table 646 response.session_db_record_id = record_id 647 response.session_db_unique_key = unique_key 648 rcookies = response.cookies 649 rcookies[response.session_id_name] = response.session_id 650 rcookies[response.session_id_name]['path'] = '/' 651 if cookie_expires: 652 rcookies[response.session_id_name][ 653 'expires'] = cookie_expires.strftime(FMT) 654 # if not cookie_key, but session_data_name in cookies 655 # expire session_data_name from cookies 656 if session_cookie_data: 657 rcookies[response.session_data_name] = 'expired' 658 rcookies[response.session_data_name]['path'] = '/' 659 rcookies[response.session_data_name]['expires'] = PAST 660 if self.flash: 661 (response.flash, self.flash) = (self.flash, None)
662
663 - def clear(self):
664 previous_session_hash = self.pop('_session_hash', None) 665 Storage.clear(self) 666 if previous_session_hash: 667 self._session_hash = previous_session_hash
668
669 - def is_new(self):
670 if self._start_timestamp: 671 return False 672 else: 673 self._start_timestamp = datetime.datetime.today() 674 return True
675
676 - def is_expired(self, seconds=3600):
677 now = datetime.datetime.today() 678 if not self._last_timestamp or \ 679 self._last_timestamp + datetime.timedelta(seconds=seconds) > now: 680 self._last_timestamp = now 681 return False 682 else: 683 return True
684
685 - def secure(self):
686 self._secure = True
687
688 - def forget(self, response=None):
689 self._close(response) 690 self._forget = True
691 705
706 - def _unchanged(self):
707 previous_session_hash = self.pop('_session_hash', None) 708 if not previous_session_hash and not \ 709 any(value is not None for value in self.itervalues()): 710 return True 711 session_pickled = cPickle.dumps(dict(self)) 712 session_hash = hashlib.md5(session_pickled).hexdigest() 713 if previous_session_hash == session_hash: 714 return True 715 else: 716 self._session_hash = session_hash 717 return False
718
719 - def _try_store_in_db(self, request, response):
720 # don't save if file-based sessions, 721 # no session id, or session being forgotten 722 # or no changes to session 723 if response.session_storage_type != 'db' or not response.session_id \ 724 or self._forget or self._unchanged(): 725 return False 726 727 table = response.session_db_table 728 record_id = response.session_db_record_id 729 unique_key = response.session_db_unique_key 730 731 dd = dict(locked=False, 732 client_ip=request.client.replace(':', '.'), 733 modified_datetime=request.now, 734 session_data=cPickle.dumps(dict(self)), 735 unique_key=unique_key) 736 if record_id: 737 table._db(table.id == record_id).update(**dd) 738 else: 739 record_id = table.insert(**dd) 740 741 cookies, session_id_name = response.cookies, response.session_id_name 742 cookies[session_id_name] = '%s:%s' % (record_id, unique_key) 743 cookies[session_id_name]['path'] = '/' 744 return True
745 750
751 - def _try_store_in_file(self, request, response):
752 if response.session_storage_type != 'file': 753 return False 754 try: 755 if not response.session_id or self._forget or self._unchanged(): 756 return False 757 if response.session_new or not response.session_file: 758 # Tests if the session sub-folder exists, if not, create it 759 session_folder = os.path.dirname(response.session_filename) 760 if not os.path.exists(session_folder): 761 os.mkdir(session_folder) 762 response.session_file = open(response.session_filename, 'wb') 763 portalocker.lock(response.session_file, portalocker.LOCK_EX) 764 response.session_locked = True 765 766 if response.session_file: 767 cPickle.dump(dict(self), response.session_file) 768 response.session_file.truncate() 769 finally: 770 self._close(response) 771 return True
772
773 - def _unlock(self, response):
774 if response and response.session_file and response.session_locked: 775 try: 776 portalocker.unlock(response.session_file) 777 response.session_locked = False 778 except: # this should never happen but happens in Windows 779 pass
780
781 - def _close(self, response):
782 if response and response.session_file: 783 self._unlock(response) 784 try: 785 response.session_file.close() 786 del response.session_file 787 except: 788 pass
789