From 5798844dc7f9cb62598e84d41e90453f0e1c7d97 Mon Sep 17 00:00:00 2001
From: GitHub Action UNKNOWN Mar 28, 2024 Apr 02, 2024 1 Mar 28, 2024 Apr 02, 2024 UNKNOWNMetPX-Sarracenia Developer’s Guide
sr_title
POLLING
post per file. The file’s size is taken from the directory “ls”… but its
checksum cannot be determined, so the default identity method is “cod”, asking
clients to calculate the identity Checksum On Download.
To set when to poll, use the scheduled_interval or scheduled_hour and scheduled_minute +settings. for example:
+scheduled_interval 30m
+
to poll the remote resources every thirty minutes. Alternatively:
+scheduled_hour 1,13,19
+scheduled_minute 27
+
specifies that poll be run at 1:27, 13:27, and 19:27 each day.
By default, sr_poll sends its post notification message to the broker with default exchange (the prefix xs_ followed by the broker username). The post_broker is mandatory. It can be given incomplete if it is well defined in the credentials.conf file.
@@ -1232,7 +1243,8 @@The notification protocol is defined here sr_post(7)
-poll connects to a broker. Every sleep seconds, it connects to +
poll connects to a broker. Every scheduled_interval seconds (or can used +combination of scheduled_hour and scheduled_minute) , it connects to a pollUrl (sftp, ftp, ftps). For each of the directory defined, it lists the contents. Polling is only intended to be used for recently modified files. The fileAgeMax option eliminates files that are too old diff --git a/Explanation/SarraPluginDev.html b/Explanation/SarraPluginDev.html index 3e3fcc491..78c7ab4eb 100644 --- a/Explanation/SarraPluginDev.html +++ b/Explanation/SarraPluginDev.html @@ -151,7 +151,7 @@
UNKNOWN
Mar 28, 2024
+Apr 02, 2024
gather(self)
gather messages from a source, returns a list of -messages.
on_housekeeping (self)
UNKNOWN
Mar 28, 2024
+Apr 02, 2024
A Sarracenia data pump is a web (or sftp) server with notifications for subscribers diff --git a/How2Guides/subscriber.html b/How2Guides/subscriber.html index a8d1cae2c..21bdd7d97 100644 --- a/How2Guides/subscriber.html +++ b/How2Guides/subscriber.html @@ -143,7 +143,7 @@
UNKNOWN
Mar 28, 2024
+Apr 02, 2024
The cfg is should be an sarra/config object.
stub to do the work: does nothing, marking everything done. -to be replaced in child classes that do transforms or transfers.
-Task: acknowledge messages from a gather source.
def gather(self, messageCountMax) -> list:
-Task: gather messages from a source... return a list of messages
+def gather(self, messageCountMax) -> (gather_more, messages)
+Task: gather messages from a source... return a tuple:
- in a poll, gather is always called, regardless of vip posession.
- in all other components, gather is only called when in posession
+ * gather_more ... bool whether to continue gathering
+ * messages ... list of messages
+
+ or just return a list of messages.
+
+ In a poll, gather is always called, regardless of vip posession.
+
+ In all other components, gather is only called when in posession
of the vip.
-return []
+
+return (True, list)
+ OR
+return list
def after_accept(self,worklist) -> None:
@@ -1607,7 +1616,14 @@
return a current list of messages.
+a list of messages obtained from this source.
+True … you can gather from other sources. and
+1
Mar 28, 2024
+Apr 02, 2024
UNKNOWN
diff --git a/Reference/sr3_cpump.1.html b/Reference/sr3_cpump.1.html index c43be7b6a..526448a74 100644 --- a/Reference/sr3_cpump.1.html +++ b/Reference/sr3_cpump.1.html @@ -127,7 +127,7 @@1
Mar 28, 2024
+Apr 02, 2024
UNKNOWN
diff --git a/Reference/sr3_credentials.7.html b/Reference/sr3_credentials.7.html index 87f94a2ee..47251cc6f 100644 --- a/Reference/sr3_credentials.7.html +++ b/Reference/sr3_credentials.7.html @@ -124,7 +124,7 @@7
Mar 28, 2024
+Apr 02, 2024
UNKNOWN
diff --git a/Reference/sr3_options.7.html b/Reference/sr3_options.7.html index 10580b102..cd15c698b 100644 --- a/Reference/sr3_options.7.html +++ b/Reference/sr3_options.7.html @@ -128,7 +128,7 @@7
Mar 28, 2024
+Apr 02, 2024
UNKNOWN
@@ -1477,6 +1477,23 @@When working with scheduled flows, such as polls, one can configure a duration +(no units defaults to seconds, suffixes: m-minute, h-hour) at which to run a +given activity:
+scheduled_interval 30
+
run the flow or poll every 30 seconds. If no duration is set, then the +flowcb.scheduled.Scheduled class will look for the other two time specifiers:
+scheduled_hour 1,4,5,23
+scheduled_minute 14,17,29
+
which will have the poll run each day at: 01:14, 01:17, 01:29, then the same minutes +after each of 4h, 5h and 23h.
+@@ -1512,10 +1529,12 @@shim_skip_parent_open_files (EXPERIMENTAL)
sleep <time>
-The time to wait between generating events. When files are written frequently, it is counter productive +
The time to wait between generating events. When files are written frequently, it is counter productive to produce a post for every change, as it can produce a continuous stream of changes where the transfers -cannot be done quickly enough to keep up. In such circumstances, one can group all changes made to a file +cannot be done quickly enough to keep up. In such circumstances, one can group all changes made to a file in sleep time, and produce a single post.
+When sleep is set > 0 for use with a poll it has the effect to setting scheduled_interval to that value +for compatibility reasons. It is better for poll to use scheduled settings explicitly going forward.
1
Mar 28, 2024
+Apr 02, 2024
UNKNOWN
diff --git a/Reference/sr_post.7.html b/Reference/sr_post.7.html index e6e9049db..c7a71512e 100644 --- a/Reference/sr_post.7.html +++ b/Reference/sr_post.7.html @@ -133,7 +133,7 @@7
Mar 28, 2024
+Apr 02, 2024
UNKNOWN
diff --git a/Tutorials/Install.html b/Tutorials/Install.html index ec3647e20..d246d192e 100644 --- a/Tutorials/Install.html +++ b/Tutorials/Install.html @@ -134,7 +134,7 @@UNKNOWN
Mar 28, 2024
+Apr 02, 2024
if (component not in ['poll' ]):
self.path = list(map( os.path.expanduser, self.path ))
+ else:
+ if not (hasattr(self,'scheduled_interval') or hasattr(self,'scheduled_hour') or hasattr(self,'scheduled_minute')):
+ if self.sleep > 1:
+ self.scheduled_interval = self.sleep
+ self.sleep=1
+
if self.vip and not features['vip']['present']:
logger.critical( f"vip feature requested, but missing library: {' '.join(features['vip']['modules_needed'])} " )
diff --git a/_modules/sarracenia/flow.html b/_modules/sarracenia/flow.html
index 9a72d22ff..67d05f556 100644
--- a/_modules/sarracenia/flow.html
+++ b/_modules/sarracenia/flow.html
@@ -288,7 +288,8 @@ Source code for sarracenia.flow
self.plugins['load'].extend(self.o.destfn_scripts)
# metrics - dictionary with names of plugins as the keys
- self.metricsFlowReset()
+ self.metricsFlowReset()
+ self.had_vip = True
logger.error( f'flowCallback plugin {p}/ack crashed: {ex}' )
logger.debug( "details:", exc_info=True )
+ def _run_vip_update(self) -> bool:
+
+ self.have_vip = self.has_vip()
+ if (self.o.component == 'poll') and not self.have_vip:
+ if self.had_vip:
+ logger.info("now passive on vips %s" % self.o.vip )
+ with open( self.o.novipFilename, 'w' ) as f:
+ f.write(str(nowflt()) + '\n' )
+ self.had_vip=False
+ else:
+ if not self.had_vip:
+ logger.info("now active on vip %s" % self.have_vip )
+ self.had_vip=True
+ if os.path.exists( self.o.novipFilename ):
+ os.unlink( self.o.novipFilename )
+
[docs]
def run(self):
@@ -512,7 +529,7 @@ Source code for sarracenia.flow
current_rate = 0
total_messages = 1
start_time = nowflt()
- had_vip = False
+ now=start_time
current_sleep = self.o.sleep
last_time = start_time
self.metrics['flow']['last_housekeeping'] = start_time
@@ -525,15 +542,6 @@ Source code for sarracenia.flow
logger.info(
f'pid: {os.getpid()} {self.o.component}/{self.o.config} instance: {self.o.no}'
)
- if not self.has_vip():
- logger.info( f'starting up passive, as do not possess any vip from: {self.o.vip}' )
- with open( self.o.novipFilename, 'w' ) as f:
- f.write(str(start_time) + '\n' )
- else:
- if os.path.exists( self.o.novipFilename ):
- os.unlink( self.o.novipFilename )
-
- self.runCallbacksTime(f'on_start')
spamming = True
last_gather_len = 0
@@ -544,27 +552,28 @@ Source code for sarracenia.flow
if self._stop_requested:
if stopping:
logger.info('clean stop from run loop')
-
self.close()
break
else:
- logger.info(
- 'starting last pass (without gather) through loop for cleanup.'
- )
+ logger.info( 'starting last pass (without gather) through loop for cleanup.')
stopping = True
- self.have_vip = self.has_vip()
+ self._run_vip_update()
+
+ if now > next_housekeeping or stopping:
+ next_housekeeping = self._runHousekeeping(now)
+ elif now == start_time:
+ self.runCallbacksTime(f'on_start')
+
+ self.worklist.incoming = []
+
if (self.o.component == 'poll') or self.have_vip:
if ( self.o.messageRateMax > 0 ) and (current_rate > 0.8*self.o.messageRateMax ):
logger.info("current_rate (%.2f) vs. messageRateMax(%.2f)) " % (current_rate, self.o.messageRateMax))
- self.worklist.incoming = []
-
if not stopping:
self.gather()
- else:
- self.worklist.incoming = []
last_gather_len = len(self.worklist.incoming)
if (last_gather_len == 0):
@@ -575,134 +584,14 @@ Source code for sarracenia.flow
self.filter()
- self._runCallbacksWorklist('after_accept')
-
- logger.debug(
- 'B filtered incoming: %d, ok: %d (directories: %d), rejected: %d, failed: %d stop_requested: %s have_vip: %s'
- % (len(self.worklist.incoming), len(
- self.worklist.ok), len(self.worklist.directories_ok),
- len(self.worklist.rejected), len(
- self.worklist.failed), self._stop_requested,
- self.have_vip))
-
- self.ack(self.worklist.ok)
- self.worklist.ok = []
- self.ack(self.worklist.rejected)
- self.worklist.rejected = []
- self.ack(self.worklist.failed)
-
# this for duplicate cache synchronization.
if self.worklist.poll_catching_up:
self.ack(self.worklist.incoming)
self.worklist.incoming = []
- continue
- if (self.o.component == 'poll') and not self.have_vip:
- if had_vip:
- logger.info("now passive on vips %s" % self.o.vip )
- with open( self.o.novipFilename, 'w' ) as f:
- f.write(str(nowflt()) + '\n' )
- had_vip=False
- else:
- if not had_vip:
- logger.info("now active on vip %s" % self.have_vip )
- had_vip=True
- if os.path.exists( self.o.novipFilename ):
- os.unlink( self.o.novipFilename )
-
- # normal processing, when you are active.
- self.do()
-
- # need to acknowledge here, because posting will delete message-id
- self.ack(self.worklist.ok)
- self.ack(self.worklist.rejected)
- self.ack(self.worklist.failed)
-
- # adjust message after action is done, but before 'after_work' so adjustment is possible.
- for m in self.worklist.ok:
- if ('new_baseUrl' in m) and (m['baseUrl'] !=
- m['new_baseUrl']):
- m['old_baseUrl'] = m['baseUrl']
- m['_deleteOnPost'] |= set(['old_baseUrl'])
- m['baseUrl'] = m['new_baseUrl']
- if ('new_retrievePath' in m) :
- m['old_retrievePath'] = m['retrievePath']
- m['retrievePath'] = m['new_retrievePath']
- m['_deleteOnPost'] |= set(['old_retrievePath'])
-
- # if new_file does not match relPath, then adjust relPath so it does.
- if 'relPath' in m and m['new_file'] != m['relPath'].split('/')[-1]:
- if not 'new_relPath' in m:
- if len(m['relPath']) > 1:
- m['new_relPath'] = '/'.join( m['relPath'].split('/')[0:-1] + [ m['new_file'] ])
- else:
- m['new_relPath'] = m['new_file']
- else:
- if len(m['new_relPath']) > 1:
- m['new_relPath'] = '/'.join( m['new_relPath'].split('/')[0:-1] + [ m['new_file'] ] )
- else:
- m['new_relPath'] = m['new_file']
-
- if ('new_relPath' in m) and (m['relPath'] != m['new_relPath']):
- m['old_relPath'] = m['relPath']
- m['_deleteOnPost'] |= set(['old_relPath'])
- m['relPath'] = m['new_relPath']
- m['old_subtopic'] = m['subtopic']
- m['_deleteOnPost'] |= set(['old_subtopic','subtopic'])
- m['subtopic'] = m['new_subtopic']
-
- if '_format' in m:
- m['old_format'] = m['_format']
- m['_deleteOnPost'] |= set(['old_format'])
- m['_format'] = m['post_format']
-
- # restore adjustment to fileOp
- if 'post_fileOp' in m:
- m['fileOp'] = m['post_fileOp']
-
- if self.o.download and 'retrievePath' in m:
- # retrieve paths do not propagate after download.
- del m['retrievePath']
-
-
- self._runCallbacksWorklist('after_work')
-
- self.ack(self.worklist.rejected)
- self.worklist.rejected = []
- self.ack(self.worklist.failed)
-
- if len(self.plugins["post"]) > 0:
- self.post()
- self._runCallbacksWorklist('after_post')
-
- self._runCallbacksWorklist('report')
- self._runCallbackMetrics()
-
- if hasattr(self.o, 'metricsFilename' ) and os.path.isdir(os.path.dirname(self.o.metricsFilename)):
- metrics=json.dumps(self.metrics)
- with open(self.o.metricsFilename, 'w') as mfn:
- mfn.write(metrics+"\n")
- if self.o.logMetrics:
- if self.o.logRotateInterval >= 24*60*60:
- tslen=8
- elif self.o.logRotateInterval > 60:
- tslen=14
- else:
- tslen=16
- timestamp=time.strftime("%Y%m%d-%H%M%S", time.gmtime())
- with open(self.o.metricsFilename + '.' + timestamp[0:tslen], 'a') as mfn:
- mfn.write( f'\"{timestamp}\" : {metrics},\n')
-
- # removing old metrics files
- logger.info( f"looking for old metrics for {self.o.metricsFilename}" )
- old_metrics=sorted(glob.glob(self.o.metricsFilename+'.*'))[0:-self.o.logRotateCount]
- for o in old_metrics:
- logger.info( f"removing old metrics file: {o} " )
- os.unlink(o)
-
- self.worklist.ok = []
- self.worklist.directories_ok = []
- self.worklist.failed = []
+ else: # normal processing, when you are active.
+ self.work()
+ self.post()
now = nowflt()
run_time = now - start_time
@@ -1187,15 +1076,36 @@ Source code for sarracenia.flow
self.reject(m, 304, "unmatched pattern %s" % url)
self.worklist.incoming = filtered_worklist
- logger.debug(
- 'end len(incoming)=%d, rejected=%d' %
- (len(self.worklist.incoming), len(self.worklist.rejected)))
+
+ logger.debug( 'end len(incoming)=%d, rejected=%d' % (len(self.worklist.incoming), len(self.worklist.rejected)))
+
+ self._runCallbacksWorklist('after_accept')
+
+ logger.debug( 'B filtered incoming: %d, ok: %d (directories: %d), rejected: %d, failed: %d stop_requested: %s have_vip: %s'
+ % (len(self.worklist.incoming), len(self.worklist.ok), len(self.worklist.directories_ok),
+ len(self.worklist.rejected), len(self.worklist.failed), self._stop_requested, self.have_vip))
+
+ self.ack(self.worklist.ok)
+ self.worklist.ok = []
+ self.ack(self.worklist.rejected)
+ self.worklist.rejected = []
+ self.ack(self.worklist.failed)
+
def gather(self) -> None:
so_far=0
+ keep_going=True
for p in self.plugins["gather"]:
try:
- new_incoming = p(self.o.batch-so_far)
+ retval = p(self.o.batch-so_far)
+
+ # To avoid having to modify all existing gathers, support old API.
+ if type(retval) == tuple:
+ keep_going, new_incoming = retval
+ elif type(retval) == list:
+ new_incoming = retval
+ else:
+ logger.error( f"flowCallback plugin gather routine {p} returned unexpected type: {type(retval)}. Expected tuple of boolean and list of new messages" )
except Exception as ex:
logger.error( f'flowCallback plugin {p} crashed: {ex}' )
logger.debug( "details:", exc_info=True )
@@ -1206,9 +1116,30 @@ Source code for sarracenia.flow
so_far += len(new_incoming)
# if we gathered enough with a subset of plugins then return.
- if so_far >= self.o.batch:
+ if not keep_going or (so_far >= self.o.batch):
+ if (self.o.component == 'poll' ):
+ self.worklist.poll_catching_up=True
+
return
+ # gather is an extended version of poll.
+ if self.o.component != 'poll':
+ return
+
+ if len(self.worklist.incoming) > 0:
+ logger.info('ingesting %d postings into duplicate suppression cache' % len(self.worklist.incoming) )
+ self.worklist.poll_catching_up = True
+ return
+ else:
+ self.worklist.poll_catching_up = False
+
+ if self.have_vip:
+ for plugin in self.plugins['poll']:
+ new_incoming = plugin()
+ if len(new_incoming) > 0:
+ self.worklist.incoming.extend(new_incoming)
+
+
def do(self) -> None:
@@ -1221,21 +1152,116 @@ Source code for sarracenia.flow
logger.debug('processing %d messages worked!' % len(self.worklist.ok))
+ def work(self) -> None:
+
+ self.do()
+
+ # need to acknowledge here, because posting will delete message-id
+ self.ack(self.worklist.ok)
+ self.ack(self.worklist.rejected)
+ self.ack(self.worklist.failed)
+
+ # adjust message after action is done, but before 'after_work' so adjustment is possible.
+ for m in self.worklist.ok:
+ if ('new_baseUrl' in m) and (m['baseUrl'] !=
+ m['new_baseUrl']):
+ m['old_baseUrl'] = m['baseUrl']
+ m['_deleteOnPost'] |= set(['old_baseUrl'])
+ m['baseUrl'] = m['new_baseUrl']
+ if ('new_retrievePath' in m) :
+ m['old_retrievePath'] = m['retrievePath']
+ m['retrievePath'] = m['new_retrievePath']
+ m['_deleteOnPost'] |= set(['old_retrievePath'])
+
+ # if new_file does not match relPath, then adjust relPath so it does.
+ if 'relPath' in m and m['new_file'] != m['relPath'].split('/')[-1]:
+ if not 'new_relPath' in m:
+ if len(m['relPath']) > 1:
+ m['new_relPath'] = '/'.join( m['relPath'].split('/')[0:-1] + [ m['new_file'] ])
+ else:
+ m['new_relPath'] = m['new_file']
+ else:
+ if len(m['new_relPath']) > 1:
+ m['new_relPath'] = '/'.join( m['new_relPath'].split('/')[0:-1] + [ m['new_file'] ] )
+ else:
+ m['new_relPath'] = m['new_file']
+
+ if ('new_relPath' in m) and (m['relPath'] != m['new_relPath']):
+ m['old_relPath'] = m['relPath']
+ m['_deleteOnPost'] |= set(['old_relPath'])
+ m['relPath'] = m['new_relPath']
+ m['old_subtopic'] = m['subtopic']
+ m['_deleteOnPost'] |= set(['old_subtopic','subtopic'])
+ m['subtopic'] = m['new_subtopic']
+
+ if '_format' in m:
+ m['old_format'] = m['_format']
+ m['_deleteOnPost'] |= set(['old_format'])
+ m['_format'] = m['post_format']
+
+ # restore adjustment to fileOp
+ if 'post_fileOp' in m:
+ m['fileOp'] = m['post_fileOp']
+
+ if self.o.download and 'retrievePath' in m:
+ # retrieve paths do not propagate after download.
+ del m['retrievePath']
+
+ self._runCallbacksWorklist('after_work')
+
+ self.ack(self.worklist.rejected)
+ self.worklist.rejected = []
+ self.ack(self.worklist.failed)
+
+
+
def post(self) -> None:
- # work-around for python3.5 not being able to copy re.match issue:
- # https://github.com/MetPX/sarracenia/issues/857
- if sys.version_info.major == 3 and sys.version_info.minor <= 6:
- for m in self.worklist.ok:
- if '_matches' in m:
- del m['_matches']
+ if len(self.plugins["post"]) > 0:
- for p in self.plugins["post"]:
- try:
- p(self.worklist)
- except Exception as ex:
- logger.error( f'flowCallback plugin {p} crashed: {ex}' )
- logger.debug( "details:", exc_info=True )
+ # work-around for python3.5 not being able to copy re.match issue:
+ # https://github.com/MetPX/sarracenia/issues/857
+ if sys.version_info.major == 3 and sys.version_info.minor <= 6:
+ for m in self.worklist.ok:
+ if '_matches' in m:
+ del m['_matches']
+
+ for p in self.plugins["post"]:
+ try:
+ p(self.worklist)
+ except Exception as ex:
+ logger.error( f'flowCallback plugin {p} crashed: {ex}' )
+ logger.debug( "details:", exc_info=True )
+
+ self._runCallbacksWorklist('after_post')
+ self._runCallbacksWorklist('report')
+ self._runCallbackMetrics()
+
+ if hasattr(self.o, 'metricsFilename' ) and os.path.isdir(os.path.dirname(self.o.metricsFilename)):
+ metrics=json.dumps(self.metrics)
+ with open(self.o.metricsFilename, 'w') as mfn:
+ mfn.write(metrics+"\n")
+ if self.o.logMetrics:
+ if self.o.logRotateInterval >= 24*60*60:
+ tslen=8
+ elif self.o.logRotateInterval > 60:
+ tslen=14
+ else:
+ tslen=16
+ timestamp=time.strftime("%Y%m%d-%H%M%S", time.gmtime())
+ with open(self.o.metricsFilename + '.' + timestamp[0:tslen], 'a') as mfn:
+ mfn.write( f'\"{timestamp}\" : {metrics},\n')
+
+ # removing old metrics files
+ logger.info( f"looking for old metrics for {self.o.metricsFilename}" )
+ old_metrics=sorted(glob.glob(self.o.metricsFilename+'.*'))[0:-self.o.logRotateCount]
+ for o in old_metrics:
+ logger.info( f"removing old metrics file: {o} " )
+ os.unlink(o)
+
+ self.worklist.ok = []
+ self.worklist.directories_ok = []
+ self.worklist.failed = []
[docs]
diff --git a/_modules/sarracenia/flow/poll.html b/_modules/sarracenia/flow/poll.html
index db0c01e7e..8187180a3 100644
--- a/_modules/sarracenia/flow/poll.html
+++ b/_modules/sarracenia/flow/poll.html
@@ -106,7 +106,6 @@ Source code for sarracenia.flow.poll
'timeCopy': True,
'randomize': False,
'post_on_start': False,
- 'sleep': -1,
'nodupe_ttl': 7 * 60 * 60,
'fileAgeMax': 30 * 24 * 60 * 60,
}
@@ -142,16 +141,17 @@ Source code for sarracenia.flow.poll
else:
logger.info( f"Good! post_exchange: {px} and exchange: {self.o.exchange} match so multiple instances to share a poll." )
- if not 'poll' in ','.join(self.plugins['load']):
+ if not 'scheduled' in ','.join(self.plugins['load']):
+ self.plugins['load'].append('sarracenia.flowcb.scheduled.poll.Poll')
+
+ if not 'flowcb.poll.Poll' in ','.join(self.plugins['load']):
logger.info( f"adding poll plugin, because missing from: {self.plugins['load']}" )
self.plugins['load'].append('sarracenia.flowcb.poll.Poll')
if options.vip:
- self.plugins['load'].insert(
- 0, 'sarracenia.flowcb.gather.message.Message')
+ self.plugins['load'].insert( 0, 'sarracenia.flowcb.gather.message.Message')
- self.plugins['load'].insert(0,
- 'sarracenia.flowcb.post.message.Message')
+ self.plugins['load'].insert( 0, 'sarracenia.flowcb.post.message.Message')
if self.o.nodupe_ttl < self.o.fileAgeMax:
logger.warning( f"nodupe_ttl < fileAgeMax means some files could age out of the cache and be re-ingested ( see : https://github.com/MetPX/sarracenia/issues/904")
@@ -159,46 +159,7 @@ Source code for sarracenia.flow.poll
if not features['ftppoll']['present']:
if hasattr( self.o, 'pollUrl' ) and ( self.o.pollUrl.startswith('ftp') ):
logger.critical( f"attempting to configure an FTP poll pollUrl={self.o.pollUrl}, but missing python modules: {' '.join(features['ftppoll']['modules_needed'])}" )
-
-
-
-[docs]
- def do(self):
- """
- stub to do the work: does nothing, marking everything done.
- to be replaced in child classes that do transforms or transfers.
- """
-
- # mark all remaining messages as rejected.
- if self.worklist.poll_catching_up:
- # in catchup mode, just reading previously posted messages.
- self.worklist.rejected = self.worklist.incoming
- else:
- self.worklist.ok = self.worklist.incoming
-
- logger.debug('processing %d messages worked! (stop requested: %s)' %
- (len(self.worklist.incoming), self._stop_requested))
- self.worklist.incoming = []
-
-
-
- def gather(self):
-
- super().gather()
-
- if len(self.worklist.incoming) > 0:
- logger.info('ingesting %d postings into duplicate suppression cache' % len(self.worklist.incoming) )
- self.worklist.poll_catching_up = True
- return
- else:
- self.worklist.poll_catching_up = False
-
- if self.have_vip:
- for plugin in self.plugins['poll']:
- new_incoming = plugin()
- if len(new_incoming) > 0:
- self.worklist.incoming.extend(new_incoming)
-
+
diff --git a/_modules/sarracenia/flowcb.html b/_modules/sarracenia/flowcb.html
index ee8d95bd4..875f918c8 100644
--- a/_modules/sarracenia/flowcb.html
+++ b/_modules/sarracenia/flowcb.html
@@ -156,14 +156,23 @@ Source code for sarracenia.flowcb
Task: acknowledge messages from a gather source.
- def gather(self, messageCountMax) -> list::
+ def gather(self, messageCountMax) -> (gather_more, messages) ::
- Task: gather messages from a source... return a list of messages
+ Task: gather messages from a source... return a tuple:
- in a poll, gather is always called, regardless of vip posession.
- in all other components, gather is only called when in posession
+ * gather_more ... bool whether to continue gathering
+ * messages ... list of messages
+
+ or just return a list of messages.
+
+ In a poll, gather is always called, regardless of vip posession.
+
+ In all other components, gather is only called when in posession
of the vip.
- return []
+
+ return (True, list)
+ OR
+ return list
def after_accept(self,worklist) -> None::
diff --git a/_modules/sarracenia/flowcb/gather/file.html b/_modules/sarracenia/flowcb/gather/file.html
index 4c3e4c203..c1db00f21 100644
--- a/_modules/sarracenia/flowcb/gather/file.html
+++ b/_modules/sarracenia/flowcb/gather/file.html
@@ -792,19 +792,19 @@ Source code for sarracenia.flowcb.gather.file
if len(self.queued_messages) > self.o.batch:
messages = self.queued_messages[0:self.o.batch]
self.queued_messages = self.queued_messages[self.o.batch:]
- return messages
+ return (True, messages)
elif len(self.queued_messages) > 0:
messages = self.queued_messages
self.queued_messages = []
if self.o.sleep < 0:
- return messages
+ return (True, messages)
else:
messages = []
if self.primed:
- return self.wakeup()
+ return (True, self.wakeup())
cwd = os.getcwd()
@@ -840,7 +840,7 @@ Source code for sarracenia.flowcb.gather.file
messages = messages[0:self.o.batch]
self.primed = True
- return messages
+ return (True, messages)
diff --git a/_modules/sarracenia/flowcb/gather/message.html b/_modules/sarracenia/flowcb/gather/message.html
index ecced0281..518885dde 100644
--- a/_modules/sarracenia/flowcb/gather/message.html
+++ b/_modules/sarracenia/flowcb/gather/message.html
@@ -112,14 +112,16 @@ Source code for sarracenia.flowcb.gather.message
[docs]
def gather(self, messageCountMax) -> list:
"""
- return a current list of messages.
+ return:
+ True ... you can gather from other sources. and:
+ a list of messages obtained from this source.
"""
if hasattr(self,'consumer') and hasattr(self.consumer,'newMessages'):
- return self.consumer.newMessages()
+ return (True, self.consumer.newMessages())
else:
logger.warning( f'not connected. Trying to connect to {self.o.broker}')
self.consumer = sarracenia.moth.Moth.subFactory(self.od)
- return []
+ return (True, [])
def ack(self, mlist) -> None:
diff --git a/_modules/sarracenia/flowcb/log.html b/_modules/sarracenia/flowcb/log.html
index 5738d05ad..8e0224454 100644
--- a/_modules/sarracenia/flowcb/log.html
+++ b/_modules/sarracenia/flowcb/log.html
@@ -144,7 +144,7 @@ Source code for sarracenia.flowcb.log
if set(['gather']) & self.o.logEvents:
logger.info( f' messageCountMax: {messageCountMax} ')
- return []
+ return (True, [])
def _messageStr(self, msg):
if self.o.logMessageDump:
diff --git a/_modules/sarracenia/flowcb/poll.html b/_modules/sarracenia/flowcb/poll.html
index 5e869f2a7..e33c8cf7c 100644
--- a/_modules/sarracenia/flowcb/poll.html
+++ b/_modules/sarracenia/flowcb/poll.html
@@ -192,7 +192,6 @@ Source code for sarracenia.flowcb.poll
* options are passed to sarracenia.Transfer classes for their use as well.
-
Poll uses sarracenia.transfer (ftp, sftp, https, etc... )classes to
requests lists of files using those protocols using built-in logic.
diff --git a/_modules/sarracenia/flowcb/poll/airnow.html b/_modules/sarracenia/flowcb/poll/airnow.html
index 16aaa8422..077729867 100644
--- a/_modules/sarracenia/flowcb/poll/airnow.html
+++ b/_modules/sarracenia/flowcb/poll/airnow.html
@@ -105,7 +105,7 @@ Source code for sarracenia.flowcb.poll.airnow
def poll(self):
- sleep = self.o.sleep
+ sleep = self.o.scheduled_interval
gathered_messages = []
for Hours in range(1, 3):
diff --git a/_modules/sarracenia/flowcb/retry.html b/_modules/sarracenia/flowcb/retry.html
index 4f758fed5..f906d3457 100644
--- a/_modules/sarracenia/flowcb/retry.html
+++ b/_modules/sarracenia/flowcb/retry.html
@@ -155,9 +155,9 @@ Source code for sarracenia.flowcb.retry
"""
if not features['retry']['present'] or not self.o.retry_refilter:
- return []
+ return (True, [])
- if qty <= 0: return []
+ if qty <= 0: return (True, [])
message_list = self.download_retry.get(qty)
@@ -170,7 +170,7 @@ Source code for sarracenia.flowcb.retry
m['_deleteOnPost'] = set( [ '_isRetry' ] )
- return message_list
+ return (True, message_list)
diff --git a/_modules/sarracenia/flowcb/scheduled.html b/_modules/sarracenia/flowcb/scheduled.html
index 1b0f52b6b..31240200e 100644
--- a/_modules/sarracenia/flowcb/scheduled.html
+++ b/_modules/sarracenia/flowcb/scheduled.html
@@ -164,10 +164,17 @@ Source code for sarracenia.flowcb.scheduled
sched_min = sum([ x.split(',') for x in self.o.scheduled_minute ],[])
self.minutes = list(map( lambda x: int(x), sched_min))
self.minutes.sort()
+
+ self.default_wait=300
+
logger.debug( f'minutes: {self.minutes}')
now=datetime.datetime.fromtimestamp(time.time(),datetime.timezone.utc)
- self.update_appointments(now)
+ self.update_appointments(now)
+ self.first_interval=True
+
+ if self.o.scheduled_interval <= 0 and not self.appointments:
+ logger.info( f"no scheduled_interval or appointments (combination of scheduled_hour and scheduled_minute) set defaulting to every {self.default_wait} seconds" )
def gather(self,messageCountMax):
@@ -176,7 +183,7 @@ Source code for sarracenia.flowcb.scheduled
self.wait_until_next()
if self.stop_requested or self.housekeeping_needed:
- return []
+ return (False, [])
logger.info('time to run')
@@ -188,7 +195,7 @@ Source code for sarracenia.flowcb.scheduled
m = sarracenia.Message.fromFileInfo(relPath, self.o, st)
gathered_messages.append(m)
- return gathered_messages
+ return (True, gathered_messages)
def on_housekeeping(self):
@@ -245,27 +252,39 @@ Source code for sarracenia.flowcb.scheduled
sleepfor=appointment-now
- logger.info( f"{appointment} duration: {sleepfor}" )
+ logger.info( f"appointment at: {appointment}, need to wait: {sleepfor})" )
self.wait_seconds( sleepfor )
def wait_until_next( self ):
if self.o.scheduled_interval > 0:
+ if self.first_interval:
+ self.first_interval=False
+ return
+
self.wait_seconds(datetime.timedelta(seconds=self.o.scheduled_interval))
return
if ( len(self.o.scheduled_hour) > 0 ) or ( len(self.o.scheduled_minute) > 0 ):
now = datetime.datetime.fromtimestamp(time.time(),datetime.timezone.utc)
next_appointment=None
+ missed_appointments=[]
for t in self.appointments:
if now < t:
next_appointment=t
break
+ else:
+ logger.info( f'already too late to {t} skipping' )
+ missed_appointments.append(t)
+
+ if missed_appointments:
+ for ma in missed_appointments:
+ self.appointments.remove(ma)
if next_appointment is None:
# done for the day...
- tomorrow = datetime.date.today()+datetime.timedelta(days=1)
+ tomorrow = datetime.datetime.fromtimestamp(time.time(),datetime.timezone.utc)+datetime.timedelta(days=1)
midnight = datetime.time(0,0,tzinfo=datetime.timezone.utc)
midnight = datetime.datetime.combine(tomorrow,midnight)
self.update_appointments(midnight)
@@ -276,8 +295,16 @@ Source code for sarracenia.flowcb.scheduled
logger.info( f"sleep interrupted, returning for housekeeping." )
else:
self.appointments.remove(next_appointment)
- logger.info( f"ok {len(self.appointments)} appointments left today" )
+ logger.info( f"ok {len(self.appointments)} appointments left today" )
+ return
+
+ # default wait...
+
+ if self.first_interval:
+ self.first_interval=False
+ return
+ self.wait_seconds(self.default_wait)
if __name__ == '__main__':
diff --git a/_sources/Explanation/CommandLineGuide.rst.txt b/_sources/Explanation/CommandLineGuide.rst.txt
index 42afb424b..6ad09967a 100644
--- a/_sources/Explanation/CommandLineGuide.rst.txt
+++ b/_sources/Explanation/CommandLineGuide.rst.txt
@@ -939,6 +939,18 @@ post per file. The file's size is taken from the directory "ls"... but its
checksum cannot be determined, so the default identity method is "cod", asking
clients to calculate the identity Checksum On Download.
+To set when to poll, use the *scheduled_interval* or *scheduled_hour* and *scheduled_minute*
+settings. for example::
+
+ scheduled_interval 30m
+
+to poll the remote resources every thirty minutes. Alternatively::
+
+ scheduled_hour 1,13,19
+ scheduled_minute 27
+
+specifies that poll be run at 1:27, 13:27, and 19:27 each day.
+
By default, sr_poll sends its post notification message to the broker with default exchange
(the prefix *xs_* followed by the broker username). The *post_broker* is mandatory.
It can be given incomplete if it is well defined in the credentials.conf file.
@@ -1101,7 +1113,8 @@ notify about the new product.
The notification protocol is defined here `sr_post(7) <../Reference/sr_post.7.html>`_
-**poll** connects to a *broker*. Every *sleep* seconds, it connects to
+**poll** connects to a *broker*. Every *scheduled_interval* seconds (or can used
+combination of *scheduled_hour* and *scheduled_minute*) , it connects to
a *pollUrl* (sftp, ftp, ftps). For each of the *directory* defined, it lists
the contents. Polling is only intended to be used for recently modified
files. The *fileAgeMax* option eliminates files that are too old
diff --git a/_sources/Explanation/SarraPluginDev.rst.txt b/_sources/Explanation/SarraPluginDev.rst.txt
index cc7364354..8927b46c5 100644
--- a/_sources/Explanation/SarraPluginDev.rst.txt
+++ b/_sources/Explanation/SarraPluginDev.rst.txt
@@ -585,12 +585,17 @@ for detailed information about call signatures and return values, etc...
| | permanent name. |
| | |
| | return the new name for the downloaded/sent file. |
+| | |
+---------------------+----------------------------------------------------+
| download(self,msg) | replace built-in downloader return true on success |
| | takes message as argument. |
+---------------------+----------------------------------------------------+
| gather(self) | gather messages from a source, returns a list of |
| | messages. |
+| | can also return a tuple where the first element |
+| | is a boolean flag keep_going indicating whether |
+| | to stop gather processing. |
+| | |
+---------------------+----------------------------------------------------+
| | Called every housekeeping interval (minutes) |
| | used to clean cache, check for occasional issues. |
diff --git a/_sources/How2Guides/Email_Ingesting_With_Sarracenia.rst.txt b/_sources/How2Guides/Email_Ingesting_With_Sarracenia.rst.txt
index ff9c0c0f0..44f823d81 100644
--- a/_sources/How2Guides/Email_Ingesting_With_Sarracenia.rst.txt
+++ b/_sources/How2Guides/Email_Ingesting_With_Sarracenia.rst.txt
@@ -50,7 +50,7 @@ What did we get?::
post_broker amqp://tsource@${FLOWBROKER}
post_exchange xs_tsource
- sleep 60
+ scheduled_interval 60
pollUrl ://@:/
diff --git a/_sources/How2Guides/FlowCallbacks.rst.txt b/_sources/How2Guides/FlowCallbacks.rst.txt
index 8469f2412..876a92f9e 100644
--- a/_sources/How2Guides/FlowCallbacks.rst.txt
+++ b/_sources/How2Guides/FlowCallbacks.rst.txt
@@ -215,7 +215,11 @@ Other entry_points, extracted from sarracenia/flowcb/__init__.py ::
def gather(self):
- Task: gather notification messages from a source... return a list of notification messages.
+ Task: gather notification messages from a source... return either:
+ * a list of notification messages, or
+ * a tuple, (bool:keep_going, list of messages)
+ * to curtail further gathers in this cycle.
+
return []
def metrics_report(self) -> dict:
diff --git a/_sources/How2Guides/Hydro_Examples.rst.txt b/_sources/How2Guides/Hydro_Examples.rst.txt
index 5e398516d..07b580579 100644
--- a/_sources/How2Guides/Hydro_Examples.rst.txt
+++ b/_sources/How2Guides/Hydro_Examples.rst.txt
@@ -40,7 +40,7 @@ station observations and predictions data through a GET RESTful web service, ava
and Currents website `_. For example, if you want to access the
water temperature data from the last hour in Honolulu, you can navigate to `https://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1612340&product=water_temperature&units=metric&time_zone=gmt&application=web_services&format=csv`.
A new observation gets recorded every six minutes, so if you wanted to advertise solely new data through
-Sarracenia, you would configure an sr_poll instance to connect to the API, sleep every hour, and build
+Sarracenia, you would configure an sr_poll instance to connect to the API, set a one hour *scheduled_interval* , and build
it a GET request to announce every time it woke up (this operates under the potentially misguided assumption
that the data source is maintaining their end of the bargain). To download this shiny new file, you would connect
an sr_subscribe to the same exchange it got announced on, and it would retrieve the URL, which a *do_download*
diff --git a/_sources/Reference/sr3_options.7.rst.txt b/_sources/Reference/sr3_options.7.rst.txt
index 3ef81e96e..db4111bb3 100644
--- a/_sources/Reference/sr3_options.7.rst.txt
+++ b/_sources/Reference/sr3_options.7.rst.txt
@@ -1578,6 +1578,25 @@ sanity_log_dead (default: 1.5*housekeeping)
The **sanity_log_dead** option sets how long to consider too long before restarting
a component.
+scheduled_interval,scheduled_hour,scheduled_minute
+--------------------------------------------------
+
+When working with scheduled flows, such as polls, one can configure a duration
+(no units defaults to seconds, suffixes: m-minute, h-hour) at which to run a
+given activity::
+
+ scheduled_interval 30
+
+run the flow or poll every 30 seconds. If no duration is set, then the
+flowcb.scheduled.Scheduled class will look for the other two time specifiers::
+
+ scheduled_hour 1,4,5,23
+ scheduled_minute 14,17,29
+
+
+which will have the poll run each day at: 01:14, 01:17, 01:29, then the same minutes
+after each of 4h, 5h and 23h.
+
shim_defer_posting_to_exit (EXPERIMENTAL)
-----------------------------------------
@@ -1613,11 +1632,14 @@ shim_skip_parent_open_files (EXPERIMENTAL)
sleep
-def gather(self, messageCountMax) -> list:
-Task: gather messages from a source... return a list of messages
+def gather(self, messageCountMax) -> (gather_more, messages)
+Task: gather messages from a source... return a tuple:
- in a poll, gather is always called, regardless of vip posession.
- in all other components, gather is only called when in posession
+ * gather_more ... bool whether to continue gathering
+ * messages ... list of messages
+
+ or just return a list of messages.
+
+ In a poll, gather is always called, regardless of vip posession.
+
+ In all other components, gather is only called when in posession
of the vip.
-return []
+
+return (True, list)
+ OR
+return list
def after_accept(self,worklist) -> None:
diff --git a/fr/CommentFaire/FlowCallbacks.html b/fr/CommentFaire/FlowCallbacks.html
index 53c9eb7b1..f5901cad9 100644
--- a/fr/CommentFaire/FlowCallbacks.html
+++ b/fr/CommentFaire/FlowCallbacks.html
@@ -299,6 +299,8 @@ Points d’entréedef gather(self):
Task: gather notification messages from a source... return a list of notification messages.
+ can also return tuple (keep_going, new_messages) where keep_going is a flag
+ that when False stops processing of further gather routines.
return []
"""
diff --git a/fr/CommentFaire/Ingestion_de_email_avec_Sarracenia.html b/fr/CommentFaire/Ingestion_de_email_avec_Sarracenia.html
index bfc1d2da4..1f682e731 100644
--- a/fr/CommentFaire/Ingestion_de_email_avec_Sarracenia.html
+++ b/fr/CommentFaire/Ingestion_de_email_avec_Sarracenia.html
@@ -169,7 +169,7 @@ Extending Polling ProtocolsUNKNOWN
date:
-Mar 28, 2024
+Apr 02, 2024
Une pompe de données Sarracenia est un serveur Web (ou sftp) avec des notifications pour que les
diff --git a/fr/CommentFaire/subscriber.html b/fr/CommentFaire/subscriber.html
index 138beba78..43dcce3bc 100644
--- a/fr/CommentFaire/subscriber.html
+++ b/fr/CommentFaire/subscriber.html
@@ -133,7 +133,7 @@
Enregistrement de révisionUNKNOWN
date:
-Mar 28, 2024
+Apr 02, 2024
diff --git "a/fr/Contribution/D\303\251veloppement.html" "b/fr/Contribution/D\303\251veloppement.html"
index 151bf38ef..50e3ebf53 100644
--- "a/fr/Contribution/D\303\251veloppement.html"
+++ "b/fr/Contribution/D\303\251veloppement.html"
@@ -142,7 +142,7 @@ Guide du développeur MetPX-SarraceniaUNKNOWN
date:
-Mar 28, 2024
+Apr 02, 2024
diff --git "a/fr/Contribution/mod\303\250le_de_page_man.html" "b/fr/Contribution/mod\303\250le_de_page_man.html"
index 5a56cc72d..ec64e49b2 100644
--- "a/fr/Contribution/mod\303\250le_de_page_man.html"
+++ "b/fr/Contribution/mod\303\250le_de_page_man.html"
@@ -131,7 +131,7 @@ sr_titre
1
Date:
-Mar 28, 2024
+Apr 02, 2024
Version:
UNKNOWN
diff --git a/fr/Explication/GuideLigneDeCommande.html b/fr/Explication/GuideLigneDeCommande.html
index cde3c9794..e2cacc4d3 100644
--- a/fr/Explication/GuideLigneDeCommande.html
+++ b/fr/Explication/GuideLigneDeCommande.html
@@ -1048,6 +1048,16 @@ SONDAGE (POLLING)scheduled_interal 30m
+
+
+pour sonder à toute les trente minutes, ou bien:
+scheduled_hour 1,13,19
+scheduled_minute 27
+
+
+pour sonder trois fois par jour à 1h27, 13h27 et 19h27.
Par défaut, sr_poll envoie son message de publication au courtier avec l’échange par défaut
(le préfixe xs_ suivi du nom d’utilisateur du courtier). Le post_broker est obligatoire.
Il peut être incomplet s’il est bien défini dans le fichier credentials.conf.
@@ -1185,7 +1195,8 @@ POLL<
présent, modifié, ou créé dans le répertoire distant, le programme
informe qu’il y a nouveau produit.
Le protocle de notification est défini ici sr3_post(7)
-poll se connecte à un broker. À toutes les secondes de sleep, il se connecte à
+
poll se connecte à un broker. À toutes les secondes de scheduled_interval (où bien
+à des moment spécifié par scheduled_hour et scheduled_minute), il se connecte à
une pollUrl (sftp, ftp, ftps). Pour chacun des path définis, les contenus sont listés.
Le poll est seulement destinée à être utilisée pour les fichiers récemment modifiés.
L’option fileAgeMax élimine les fichiers trop anciens. Lorsqu’un fichier correspondant
diff --git a/fr/Explication/SarraPluginDev.html b/fr/Explication/SarraPluginDev.html
index 597ef9df0..223219129 100644
--- a/fr/Explication/SarraPluginDev.html
+++ b/fr/Explication/SarraPluginDev.html
@@ -135,7 +135,7 @@
Travailler avec des pluginsUNKNOWN
date:
-Mar 28, 2024
+Apr 02, 2024
@@ -620,7 +620,10 @@ Points de rappel de fluxgather(self)
Rassembler les messages a la source, retourne une
-une liste de messages.
+une liste de messages.
+on peut également retourner un tuple dont le
+première élément est une valeur booléen keep_going
+qui peut arreter l´execution des gather.
on_housekeeping (self)
1
Mar 28, 2024
+Apr 02, 2024
UNKNOWN
diff --git a/fr/Reference/sr3_cpump.1.html b/fr/Reference/sr3_cpump.1.html index a2d4c0d4e..04271c15f 100644 --- a/fr/Reference/sr3_cpump.1.html +++ b/fr/Reference/sr3_cpump.1.html @@ -127,7 +127,7 @@1
Mar 28, 2024
+Apr 02, 2024
UNKNOWN
diff --git a/fr/Reference/sr3_credentials.7.html b/fr/Reference/sr3_credentials.7.html index d21830bcb..7eb5e3d9c 100644 --- a/fr/Reference/sr3_credentials.7.html +++ b/fr/Reference/sr3_credentials.7.html @@ -127,7 +127,7 @@7
Mar 28, 2024
+Apr 02, 2024
UNKNOWN
diff --git a/fr/Reference/sr3_options.7.html b/fr/Reference/sr3_options.7.html index 08d380073..a0e79fc34 100644 --- a/fr/Reference/sr3_options.7.html +++ b/fr/Reference/sr3_options.7.html @@ -127,7 +127,7 @@7
Mar 28, 2024
+Apr 02, 2024
UNKNOWN
@@ -1478,6 +1478,24 @@L’option sanity_log_dead définit la durée à prendre en compte avant de redémarrer un composant.
+Lorsque vous travaillez avec des flux cédulés, tels que des sondages, vous pouvez configurer une durée +(unité: seconde par défaut, suffixes : m-minute, h-heure) à laquelle exécuter un +sondage devrait être lancer:
+scheduled_interval 30
+
Ceci partirai le flux ou sondage toutes les 30 secondes. Si aucune scheduled_interval n’est +définie, alors La classe flowcb.scheduled.Scheduled recherchera les deux autres +spécificateurs de temps
+scheduled_hour 1,4,5,23
+scheduled_minute 14,17,29
+
afin de specifier de partir un sondage chaque jour à: 01h14, 01h17, 01h29, puis les mêmes minutes +après chacune des 4h, 5h et 23h.
+(option spécifique à libsrshim) @@ -1510,6 +1528,10 @@
1
Mar 28, 2024
+Apr 02, 2024
UNKNOWN
diff --git a/fr/Reference/sr_post.7.html b/fr/Reference/sr_post.7.html index c7795dab2..3c0e7c778 100644 --- a/fr/Reference/sr_post.7.html +++ b/fr/Reference/sr_post.7.html @@ -127,7 +127,7 @@7
Mar 28, 2024
+Apr 02, 2024
UNKNOWN
diff --git a/fr/Tutoriel/Installer.html b/fr/Tutoriel/Installer.html index b398add57..5bcc1996c 100644 --- a/fr/Tutoriel/Installer.html +++ b/fr/Tutoriel/Installer.html @@ -133,7 +133,7 @@UNKNOWN
Mar 28, 2024
+Apr 02, 2024
Je=}qQvk$BTAAKC&e9-zX0)yYi+>(G07TR$MPfK;+u#st!cvi64z_oBc{P088 z5wIX*HcfaKe__QUkRRFO-v|ERvlvRxLRhFyzY^X#c#*wzXUEiW!T2KCiiIoNGo{^t z_O}*`@b?x*SwWPs8<8I&4lF&&Xj-&cxeOe4ZB@BA*Wg+LF`LY%x8v!E%;vYOEm-JP zL0Eq_)($eW+p8;0{5A;`VxUDgA>z}ek!QjGA&SkKf2V`F?nA+~eV?ULZf9+JTx!q%%eXOGJ>~^sP+nF!J<>;fP>Q>c(i-9`5>cBU3 z_`g^vf4=(o*Hm*Q&fgLY{CoU&1wnuR)fa{9?{o=@aRmExZU}B2l8D;7xE03=DBcV5 zp~ f= zF!(hPR|I~Z&h%KZ!7t*7pILij&a4Va+@b`mfB4U=P0(Q&fY%(p@?hBx8!%Zd7jA2i z=C@a@uvub(;~Za}>o4%eHkfFkt-nKUFlb8PjGLD=g>S6ytZk6S^nuc_MLW_$0ZOcx z#0n(Hk5WYKLEa3$`(IV5ms)9!BHNrl2rY*5`A|ni$ysa-9pVV~ }opFU6_uy!JZnAL50-f y{ z)YHgvf|Yf&d1CW+){XhH-)ljl4otiyKf-GwUaKAEnN| O)(zo?0xif2{~c zC9KhYKfRhRe As2&rj~J&d8vqnW~Cpf53UN-v+goN zH8?jp+`s-IbNph7;ZLlow+;bqe;3&+@?tLDy_7?#I!^rh=xr3FaGe1gvMH z^B^*Vpc>8hJP%bD^YXneg*u3KpGcf<;EkJn0|UTZg)TWAC~=B{e>^D;7LYmXI Ep8il e}H9QL~ZBLG}M8}V5vCA$ob8i{0*)Gvz$rjy-S-FaVtniio!0P z5H+JvgVQ^?gOz~6aW4cVRyoN7S;E_9q7gUR)Uh8aC3h-6=VqR>ETox4ymfb$=iWu& znJY!3D>D`)0E>^)YjQPxKOfG2CZ86=4--uuC|-7NuO16ke|*6L*sKBQh>Lm)45Xh@ z#g%A76_OH)+fOl5DEE1)qYg*2aDzN+6{yof`4Ip#6>!@;KIu|ub`z^fr4*=3?%)YE zxIZzm>_&6Jp{85tyHf&w$MG@czT1CTp z5wRJhbqLbP0zV`zC=>zaFiriLJ(1f--SW@Ja*@Mr7-2D_b9>PQxe)Kk3y6RVE)app z2)e~6jlwqcK d~%uqQ3nLzGj< zcs?sOqgfAiDc%;1vd9GAHqw*{Y8oz<@j8;7%A6}156Xp*`obbtNyJV~>6(d(gqtdw za6B%of7aLF;fWcnDQTUBRS~!-^d3&q%*0cTs%&AeVdti(>qshF2Q1mKDc2^qFU>XP zl9^Vy1zM-Mc?qqkuVKlofDK)}zt)D9(XaU58erI8(M83$u<=rZ=hNgdh#st+t^cVD zGB^AIcAe@n8_jga8_K__L9|{cQ(a0Jt%lf55PQ&<|3-%sW;!oi@-jCi4pBBTR_@ zl~S+3bxbbC9u>K|1lFDhK4J`UXD8=?Pw-&PMr5`Lv??~^&+;?XC=}MA;s}s-VK^3> zbE8uET%)kb#H*-B#9ow`b`?=GBNpu>s3ME}cdZh1h^vi{vAIjw8q~*KpbH66zg*uI zf5EmcTyS?#7Rpu^uit!M-?M$J5IVlAX2A`t{>JuGXagaRw)Qi;sZG>M>BH3ZE%yGm z?U>af4)b@JH&oXL%D>g8%u=0z832yTVc#`w$+QG$#Dn-M0c_yL2E2luxHl1k+Hxv_ zjPOz9(f3ANz&~%yT`{rX@2tJ+(4al5e{#4{_5#cp;05^%FZ7w-2dsQVmc9O(x@GSA zv^kaleYl-0tT`FxLHfL4imqUx6@1%*R*nU;euWyjgitere9@qG5u%Y;jsb+O<&!>< z$Y!(Q?zQm*W *d*>ol zi&U25u?07`tnHY_=A=qd^3AaL9ByYBL8uO?4Si(a`@sv=v2{nmB9}iH^Qr{8gcsxw z@cR{ck8cRR(V-_oVPOJF0$$0ae_?G`!Lc5#bEylX0KIrV(!1DAs4=}``5Av^bq}9) zD5Rm2`!0(&Fn?%Grw;3rc!#!Y@n*o@)zf61$13YQR;=Kzpy2AY4y>zLWnImR74H=k zZ{ft1{bFrs>KxwbbV=1T&fo`YtQnGJ*jv`{t48gSI4iHNp9ZXx>5Ft3f8r<$?1ig$ z5ZKnmZ`m);lP(mkB%;*9RZ}+HSwDD(z7k`ia->L!^pcC;vR^30cdD063LGOJ$WL6V zRdz^k$Z_{%=1{7e<+uf{5N~yFaEhr%ONAQoC%(&OOe?n=*wJJgI51U&C7%P>EV}E1 z1)J90=<2D+O%$$qEHEU-e`v43Q8A163is^IQwY-%mo>AsOGR1 V;7UYu{(AyD$W!$@RI#B&p^C# zhuTiZs+7ar%3sD*#N7}$DQQ@p)uD6>C+?TvHv*ilwQ5iV6Baq@f53J*u^(XXJ_sx? z=-RV2fzSDSJYbB?`x4%x4Zu~ a z#%0$=7*jmHP@;^4>pS#GZ&b>os`J$bNICs5fzD)VRqSZIUEMbwQ~P)v8@XSzCu@-$ z!14}OPbSi>1l)H7f7Rg2bYXp1F2u6p56ukD@D>B#vhP_1ax8f<=>aR8A-#I6AzX&w zlOet&@+Q&))?5vqYkN=aB=3gcBB+GLlsD+WHWWW-EkjJ%x6m*`k#~772;X7JWMtEd z`&|6Ew+zxqD=d|Pho%_qwctqS&i0Vc#(e^7b@$KILAS0Ee;f4pfdyBSr7;>!F`j5S z^=KbVGoUZEV~K-yWw#s$v*r?T$O3R!>y0=AmP6#$r*Oy#Y`N%;8+3iKHR{h9eWL3W z55Q_SJH<4g0!(B`edAN0!TwsTpujDl30~r~@#vFVYeG>@ry2-rb U4J`OO zO@fbhEH2e#f5^~*vWgnCAy^Is1lb#Lu7**BKA2VaK!cv^ ed%a1)?x_f-)UlnDQkk+C$iZj18YWtHN>-{d#byYa7 t4PkWpIRD&`^aX^lyHZDWeKi56anTu8t`Vt2 L3 z^x4RUv7O7e@Lkpq8dUM@E; Xgn;8{tCEBR!rFV z)y{38an+O`<*Q5-_MWT#>o?kynOs6nXz-K@3xI;aIu<=$XY0+q*%+ayxF|om{7_*$ zRjD7`&vnsXkcV6A4fRW}(d-%O8_T#hEJ{#Xe31*|uYVy;zxmB4Fmn?3QSLJJ7vTCK ze+kxsN6yK{fC-%Nmay{T%RHeMmV|ASZx`7aauW@`r}M3Gj*A=1rD@i+?chb#czqJE zc&n!~yA6921v^M<3lgUok`)9%ju>*;vBZG&5>E_6eA|Up37ToH#nIc9YWk}8wLnTW zeC1zEgy0e4$Uxtjoe7@aNUW{?7^RMlf5vj0vJ9sLSHPSpp*oO-_?7T-`=Xwx+|A%z z%l9@^q%#~uLbf#xS_{Z0V$ !i?BQx;zXI~!ub(ue<` zoqZ>A*=Q^#e+73MKGbF5 T)^zeWQxR`coPwXaN+(e+PeU zrtU EOi}lve%k-DX<4tpM^=f%33-9Pu7_3VKX<|9SqwfWL*WQqr z4kT7~^ saAwDXh6%I5@=(x@sGvy$l zUZ38qLA`6;OqD8Wq|*!T>7E+bhg^*Pn(V1g$!9UKH$qd%z%y1@5WeEi+|n}ebd-Cv z)}w!gRxZVUT)__=J))tlzuLA!n<~p5MFTxPIpkWdIihH=ZmLv>f3aV4{MD#RvV@U@ zc`{F=MF0*=w36C*eOH;$B-F=&~sX^*AD`ic(>S1~p$juV`N7h~oi<%*4BV@`dT^4x& zkp~cO0Glxb*>(_&f7~if=noqI={rr;$R%llcmr^S&@oNY)kOZI$7D5w7gHvq4Wze= z7Kv*yG|8m22OuU+ugf&HaD83Df^W#jiB%ZjnX{?D0agp~3hAxnps|r1-{)ON6tCX3 zJ)HUQD2Css7Kv{dkMftex6NNRa)i}D+>Q3y4r@yhKJ!4re**Q(6s$+?-&%I)S}~he zd5(3527r$BxTQXrM#9yialPSN8ATIdSpD3;CBxa2@VMlT5{OG=Tq5H(Cn$&>AZu{n zyw!& h6$G zD{8oES%t5ZzEt15Bk$QOGRId{OhP_k%S_y&7@bav>%6(SAir2N#@*>=Cr=8|*5ETP znAy=q@jLW-DJ~S}mWel95_Z8W+(qTdjHu^IJ2dX6f9$E&qD@Ieh3GsFJ|AVlRr)iZ z+xyh>H7x2B{S~6pTle{AHMmlLlSQ+tu 4hiinJb`J}4$QT U=&b^N^XLSjUR%}8q?lWoF(c<#gHS&Cyi@OQpZ%L*OHu;sq<}YS{94`O( zvJJ(vf1=-P-cNwte8SD=xlr6WCAxY>w>OLFcrwSE4Hx9gbor6o-b~=$L-=z!y;@G@ ztv-fo6w^I9D%g0!*Z)uMV$>wIaarTa=GLrcJfKC~NxpRFtD)m^dNaHt^U3va{)Yw2 z?bCd3l{%qlXZwfVQd5E{$X*X`e(r&7pILtjf7wsQiu>mHODl_Ay!purpX}tRdm>Df z_1S1?810-SL9yZyRI+!aD>0QiWvcV!Hmw&`Q>nsg#ZUx iGF8 ziMDKwt4X(Pk*f*vGy8!T+#mWOr~+P3mLG4&w^z3xer`{YKXHEuhMjaB%@f$`=?CbK ze{Vi$cE385o9>Nn$5^1|gv)N2HsH&~>3D-XzwN813_b>(Pp*c`$(W3;rjwgx`^=3g zwQMgA`eB)&EhZB(yjtA0=M{;qTs*VV6T2ejV!oQ)UR}ZH@pAg13sY?2`lea{WnA%c z_0D8@e*$lM>-_7Ip{?%!kKO|=O{m{9e=|s$1Y1rP&}it9;!RE0-uDDW?SDmU)=M6i z(e2g;ms0Q63zw3NX_AJ0(c@r?$@0^z3ng9`_&X~fD$Cn`dSlz9N8BpSdpT%OKC|AL zWjC44Z%32GqPH~m+`#B0SF4fHts q7=XWSe$}@5EpNt0u~M|k@Cxt*(;Pvcls>O>;jzS(EJ`|Tf()Zs)yZk zdhSx+hr7@%nph5a-iSlEiyO904sFv>i~0W3&CSo&?TpX_d~D_~2jRhhH_Mzf3avNR z1-gp&*Wk|7$~1pXQ>=?l>LwQKfBc0JZCixe=^9hGk;Qs*enF%VdH(Nt^1r74@elu$ z`a}p4*n BLa^9hx65agGd)p#i6%9VOhu_v1aZhg3Edv4!5P+a~ar zpfZ2{CPB$;yntrP$qt;x@LL6i=ji!chWd&CEQCr}g0Kepn _VpTEVJ zLOh5gqMB8H*5s-cDrDXI$+e&QiK#&Q{QNBsC+B1g6%1dMF=e1Se`8~aRz4VpMsOGM zy6ptM|NM>p=9gPlYC9dLLIVpZQ0xGOf+Mh#E48Y586ftQf U2A>UmI!E z|4d~r!Roa5S*HPRouMo<;6xSF|Hz8jv?Fy~?do0!F_Evx?)e+OBNmCH6~3*d^|a>I za^pg=w-a!Q2ELLa^gwZP@UxCyGt}g1{rs=^Nmv|KMns%*e lS#OJ!oe}s#Z_PBD+E*YRt!4i)QHF_11 z+mMaGr4N;AC1C_JYPhINTZtkvWAkS5yFtkgil==CL*P?C?)A!!MQ1#S r>*)_@~4y)H`b@*A-}Iv9ocC6tDm!(z+C*OMQ1~ElkzX)&-vdvXug}w zyI$5je?+1K)>c&|kJkI5W5#gaJ?l~_ySh`8g}lcT35=0IEbjEI(~XxFM;4R>Xj;*- zm(}OMPb> 1WGPr%fav4PUYNb5Xf2Wl`>C^|631^>t&7`@fDo<1cV(vEx$z~C- zV3&4sCA?2@!2~pn+}Of%K6n(2q9ZGc-n2L7DQTf&c9I&fEdvo49&i!(1tJ7Rv{DPZ z32>V|>q&r$oJZp1-e>>8+I1S=w ceP`q8Uf00(x z%)W_yhN2Q(d>oO*ZP`q}gwxUHmZ(L1V>e>m9h<<>2n)5`F5nF;7454z|IQkJnQj4& zuxGB0n)@p_)}FaS;@#ErKdvJ;VIyUgl^Rb&=^0K#SXGk^pCO48`5`_N`K&{Mie#u? zM~!97sS&W3oWn(Vx$eCN&r$HRf4Gr?g^U+K@K0Fd8cQcxT@}J!0TxbNhDQYW0C7N$ zzga-w#abd1y(#E@P5$L^jT&ww<$m;d{@ )PF0?+c7kq*lBWZH`&vq?9g@Kz}*Q>DlYCy`ypKh zJZ57y3?xoUL||*?xx?6e>cVFyaX%@jQ;_?nok?eP@?Gl$Wzld0jTcRBUZ+_7_mVO# zO}}VO*|9MWH1z>Z_hPshgqFK&nkMO{um=B{y@DR(|M_q53i_4V?d*JdU4J%>(o9XM zm&3NyPrM(tBn9_W@fmsZwwErqBh2OVH;+~6EZ~$Yd _Y!y6tM8|x3B zrsD}=|6NZP6EY+>)6bLPr$#Mj_pYHPl(=^j4d_&ufnW) TRBME 4^E zYwT}y(0?#S|AC|H ly@ zz-dmr#_3oYw+?9TEk!(js2YD4EV%1{j~!N_*9OKDuD|u@2IkyrFpSdGuZuUrXqj~o z$>O1zB+RH~+~;q#{9%!RCNwTujqCHRke{&N=l_w=xM*?SKqaF0U`~%CAbw0pc#t-r zQ;9Jtyb=e7_`6zK8-L+!kHTV943T27W(iK%4L)rX(4~0){0&yNcm%PwnMBY3=FtY4 zV%cXOc FP9w_Y z8W)$18WS2t!F{&i$lt~2c|G!*)_jX3nxLj?46#Oz?X!$gOI1CBDD3j|$(0FTE5{E- zSS^9Csk?sg4wiEN;GRYY;eeEHg@Dzu>zmY8Bh;UGWcAL&&n&X=WV(dN f$$e2ju;7r~*k zk?Lxc;Tn&Sum(m $B-F)ST4kK#^9l_3myAn{lO{QOOXHk0vf z@4)8U4l9Ce)Zs>HY{;bqV93MZV2uoGB+`1waKxv#VShuB9^UtqA0}emJgl5St6}?Q zrMUqLuZ(j3{ollE0 &J CMK4<70~D5ZBV;g?v sltTnk9tnE7NutpU^41C(|@uWJ<%(KW>qGhr%G^XhhN>^d|;_W z-?2PXEtiDy&}Jf0@N|g5QqPb;E;qxUVAt2EE%RJbINWccACbs%@^h=oYs0DQ5zk&U z=FU+Hv)JeVPB;!`dwz6QjiWK2t;wRSsfj2 m4dMuENftg|IN&N+)y>P<|8G~7Scohzc0Q-oI<%wjoxTL%f1 zzD?jFo*>1-n?8x7ea2 tY_SXqFjkCPG*; zbA$0UZN>Qn h{oaGx6 zxstOrwZ5jVwF&t3IdGo}Yv_GASqRt9G#eZ!M6|$0l(0BNjYA*~$>Zn{hHDB`HGiQU zcjm2wDd@AVt&oBc?x;3Mg?c@=KF@-bdZ%~2E_bt`5xDBWIjei`Q@-xMV^!Os2I?Dt z#_T {?;ci8{HjS`0FdLhRAlOj*b88De-ZeA2?}fQ1Oal zVqnLgbks#Oi4MlvxRg+a4-LB5U`bvzYm~Nv+$DP?^090cO;NeC!w^TC=zk)O+jjFB zO`!(E=qhgk1ss1ajWuQ37c|g6JT7J8H1C3ed|!LB;%`$ZHsR+6q-=7 zX`8YgU>lTYHZH>@SbGEa?SO&1KDiRvz+XWSu~_}a;1vWnG1yWz=e(nK0-H|yi*ZL> zs! ?O23@LanvcSMgVuz#>PDd%hCGQX&6W-YJ@D3E1wS6Duw*(^rKIWDi_jqNwz z(ks NVnU;Uql=HMNQk3q+L}tGXEG4JgXjV4Iq;c@dOjS-R(Kz#$$#jr38>2yD6#K6 zxOWkob{J_yZ!K^~1Sf|Z*Oye1O+ZU{50`dAmjGiRMtqXLuO}JVuGqwILu>Cw3A);Z z!K!Ck|1_|cSrjsz5fO!taXpPHxPP0SW|cRGQiF^v7iS%9=35{OZK4s=)DwF@P=D|3 zWNT6lDiL5fAz!?MtACP{SNx42#5#ZX4n2#UgRj;wa(9(R1WH_R)+*w1C$->=*eY!D zL3hoUVxPa|>!+3a&d~L-x@BnD^^3>VbegdGVha=RH`Z%7s1mb3-&;Y%{`}B_YZVei zeuuO6H-p!&2)rh*2j7z*Vt;;UX6f5raPLy9+~%o{l`1(D4}T=Ld8*UtvjT6~{0!Dp zsZnRG;$!h4hL9n72j-j@)=YvQRj)l@TSwwVT%2`3Jx5#3L7qso9o8M U^c2HxJdC1YDMMIm_}rG$ zh*mS)^FN9xuz$&qQE=Nw+JHc;d>veaH)}m%ldi%;g$`8}A!nCt!N+i-`jDt*qfY(S za8pT`aBtfkT1`z%brvb3@g|kEp(h(=)kz3K3hT?IP9JJm=3=+(*CNSXI8>_&X <4`{hM_0D0T}0l23}qf@R}fW z1DQu5I?eTw^@?1_x4||(Zt?Xr@D@krFKQgPjiI8Ofobm@?3=D&erZQZ^9Zs;21k 55`g39c{%xl^ 0NC9)kUlw7iZPIWodOYFG0?1^|--o|dcPWjd{UNvIFsxIr_^1D`Sva4_lQpX@0 zyJfWwjEdLW)PIQGKT~ajO*nEem>aHvo$^S&Z-1azb%-j>j0`fLZ6=IOV5^U4!6|1R z{XC0an;yr#mlg%!hx*U)!C&jsE|nf w*O~aFFGR-*RJHN_i4%+f}Y;Ot^N? z@{1Mu6ED(M?82rrgcq*1!eL{?dd<|g?z~`)^WW63S^lfYhu$=vy1R~D!hwnxXh|SA z1%CxVuW)wG9wgH5YqKZ@XP#*cZffE{c%)oE#OaP(r2-=C-wt7giSwVcEu5tT^!mtO=Yr3p-g-vuR-FW z?30eii+L>%$2s!o$J=eN07(_IIAQwwV8hA=Yu4>Zzle9yF&^_3H)R`^_YIkF(SHr* zi{T$&jc&{U$Tb6a*_3$2a#v&}jg3-P3czTqcD(X)(71^XI1>vg8qXTBQ016%_W1lw zJU6I}qtlNA!@V>Sm)ulddr-+n7ZM6*+2>W_o=1h-6KWFCGUtvYk&<@qZ4Y6PY-!Bb zs$i?gPfUtWh5SbhW(_lq&z106Du4WxQTl}E;Y>eSneg9;Fg!)4@e#oJv2dJ8*J _x!xoQ1Kd|MC|HWjGW%Jt1^l*dXp|I&3pfzp%fe}(Uk7a| zBNay?e5AjZ1;3Vc;bOp}eSa;QSaK^#pSUjICnesilI=2@5Olx?k!w?sH)L?kMx4ae z=UXnGH^^{8M}(Hp2-YdAieXHRIbk6>P7QB@XeYfcC@(csovE))^%jNI$U2(olkODp zRIw^Md8(T!o~X^vna9bYX`Qe{^O1OVNPPj1I{LT4;{}4C6;7@1;D351l|Uca3*^OS zIR)3kFLn5N!Me_uc0H$p@RjnQquIw&%$q9ZoiT|joDQ|5?s#`SnOt&qbRfG{VMN8b z3S))2
Lg zC~&kgCAaD`)~tx;hJSxrI@2VGT p5_1WB_(QF{Ld@ zoxr?@5L}JLko?L34L-5;H4PHiacRX#WnD%`K*%X3YM2%&=!xA#v$d+=bA&O%$C^f~ zibc#Vxjoww1wknXU6|G4a<(xy68CH(qL4g_W(xRx5U)=5_iiL_l!a`8O?7@GqtwBg zRzh^)M_Eknfqzqt4BrGd`0>0)=4q`lWqbBpV}+ucdj>Gdf9olasF GM=#$8EkXi$=Zo;OH-qVCu!F8RV`%##^~ zyB?APC?|C1vuiCr4!v`2-%9UNKS|q>DMF0#>HHHuX@64KxW0?F0i6nE3C@@egCPuB zBa_0JHFT&})BO^0(a;D3OwlejIx2GnsL0f)+9Jj8TGD-l7HyAslts0uMI=+NU} zFo~<;P=FXW%-TvQ9DIA{uJP<%O9m>!m_Oh(URwHbohorX56^U<%Ux X+r;d3! w?7#UQeiCc}?IDoaR zve&=V?ez;cCi6Pq1I<#r;`Ph;&WP&9vmfX|ER(lmSA@94C&HK6H7UULfF8B|G;GU2 zL4f*7M*KlvKYsmn;5IM9)NxQzkO#EqY~02IG=HmQ1{3o;h<0C-q$42}2}$u%g0~l4 z|Fm3c^5XlzNo4Qgf~5ULTUIAH=&t|@FApO7i*}6O_QdGzDWkVqjPBiJo8Gl!A!PX< zJ#Bn#I?Wr|G}3Wwr$a^yy2oXj!hjr?m0Q-rGN7qCa_3 F0xq@pbu~qGM zL4$5J-0k!KB>5|J4i$v@3myUNBzw)aCx4Z^r1-7;rKL7>LrYZTPK{Ry&dd#o2r_B9 z!ia`;gyUMct8l#qUm?N;fd_^?J|wRtOFe9`7{YjXzD&d;Sv>NY$J0!X*i2*keDI>s zlK2rywns;JfF74a=~V(c`ayDbn7NvIphdABx3qP7RRrEZkuUI|rT=S+{x{rv-+%Vn z&J2z)eoE10hj`RtA@(;DjSaF24h4j4lQgN=Eh}TdrV??vD P!d7oY!lN5qmiX!+7i&D2Ubtcj&wCiG z{B>R9Q{~Op?P3_Ubm{k1fCc{6?|;+z;;r@Od+YVD+i5Q#@YP@P=KI0xU+d}j-Ifzu zl>FqodJ9IKHV~w6<$6Dy4x|pfvt!!4-#vA>8ak%$L4e=E4G2(XA*fNo8Jlup4S)c1 zZAI}W)iw91pG1>~mN1;P^$q?O)GO7cjDdTI!sY7a^7>qM+@>6E`vvC_D}Oc^XihZ~ z!tDM2NRZz^IDflZPM7nEwz4w=6Wlu0M>wO~o6G5kPxIk&dV8Z!M%lLLKt)9vPq>#| zBO0t_P28l(Sgw{s4iT;Jhl%#LJ*bx$Es8{Z@PjaR<1YFo#Ny|h+u35e(AU1VBNv>l zalf6oaNoQ!P)nu?@$+y#9e=*Rnk>lpSCgC1)A{Yq_2g!0Ks9h#<%eU#UD)LypKqsg zGW@hyOiwwmNyMG^{I5G2uV6v7xuHM-t~L`AX1ocvPuvynU@u>iEzx_%{pv#=W{1q; zc4W$-hIwFiRxQ?}h7rJ2L>^FY#p)PkRD9N>;s*)04@Fn5)VBf(`G2gjvR=un7P9+y z-M *=6GUw`3m=$o{)iwfom^n~N?);*L+~#$JPoVXU6Dl!NfMmGEaad@6JS zV8ih(9J)ugi>Fzr=LV6^;T=gs8*LayaC{k@2iSN(jJ(510mdcv?)h6DxtjZ3dJndA zG+3|GQ22|$Jg1G0z<)Bv^%(CzeOR2sI*Ia{J<9@7O$|;OduemU%6KROqwHrjzT=8h zMJ{A`kXkzmr`^DlbIKR&z$-)K(nLEaj3CEqWD=xAL?#fKyNt)kY|;vco_=4Q63S*p zuqxVYpO-N$h&=xzdxC}2a%()PW@EdWb%{s4w995M8JhcqR)0$9ZC@9*AagnW VTNb+wpA;mJbezShe9-GV78*5t|kQ>goGm zQ&vP%*sNh)X5>% hJuw^E&m-CzK8Cd(}& zQEI#_bq05MAM*x|MPLG8-9lG$g>V*(OLY00x+)HSK|QZ&5joSGMW2Zub;z-`PTd3? zwNO89S|wIg9Lvq@m_P_QC(r*tcwmiQ&*olpB8sP=bbp)PQ@I%nAtHplfL8fsjapee z0mDyYEWCJU9=Lrj_r+aGqY)<`0}vjCyudP#xAB1kX%?x5XjB}o3BMX*xMu0nY(i;0 zD2%`Xwe*6n%S92HD-U&A;&ex~@iFq8WpcW!t4z|tBhQWXTDw}62xdMa<+Zm%2(H$h z1Wur-4S%C9gK-pB%%d?#;lDMv`(dRLVx$%4r%oKFKKB$E2nqQhb$gWUw@kl zN9|m&4)6?quTvvjDWeb1awoA}uVfovLr*R&6oNH1hKP2hO1BH{_%n;=*i84a@RU72 zf6D`EHA8>wlsjUtRjSBA2kxK~b}<{P)Fs{Y`jlS|mod*B_eQ185$maF!)6YFeaf$# z%YS0#E`sM6vpyl&$y;>q?YY$Sla&D|hj`H?IEJ3mh+{aS )=j!oRiDXn2zUfB9d-8@YWQ5JyoYnFMq>X@-W~z6=5f|q^tGDVmOi!RUd~jxqtrV zgO^oqb$hf|SEY#GPO*RegB96#cW$!#YQ;23T>n1S=THZNe&g9I@?tLDy(A0qvYuJy zq5hu2)t218xMpv&nqCu9$1Ge2tQ5v!z->Iryz7JIcuVGn&?6?m#@|w+#D_mxtbgk! z&9ic6*_Ed+s_B=CU;RpPz^$ILsAj<>*-RH~a``ogF~1$$q?FdeT=Da$u+1&rV&2X0 zZ9P5MG{~lO-__koQq8Rz19m*f3ppeF>+fFTnu2;qNj0(5OWY8yQsOpx>P5g77*TmN z!Q~|1vG#6gLe~)aTEr(={CC_88-D~|kwqqf_fdVG&gv6Z@O8xceXwXU$?bA^)iV-| z8_*D{G>aAQ*KADh1qU4M>onYo &&T3%hu!zZtXFyTfW-XJ|&eZ zeyOGaod2>mO&n&6qw)*;3=&ihI2NH~h=Yx2m0i@=f@;8880&Q}Bn4DPFG$JauV1$% zwuh_CXfMPBR7MP*<-UkMGk@yyo5S&_lOMyrMi+Kva1) s&&i21C)C=1uF~4C}XZTu2;TQ_`Ver#b4_a%8$%{Lr_wz2y zPOui@nhzOZ$W8SX2`fE#19qppt3XuUPr~96zdHLgE3@*~IOfaBr+@IuRasmMkiwJ< z2GI8&Tp1ascVGv4SLR->T3sy+8m58e#|!cH9a-eBUp5gebIM!C1mb6ob*pjLb_8Z& zY;FT hPDYM6{BMR{|qsnSr>|#uS zxd8S=I?$|X!p=-yR3Y*z8z6=0&aCDepw2{3487+#yX+$S0)N?{YzDDkHl`*{_|-(~ zqZ^9sy$cllpzQEjZA!Ed!Jj>S1%|UM9{(#NzGCBWH+}^d?FRA`T6GFUV=2ZJaL!HU z8)O&wZWQgONh0(?WkW2+F)%UBzr~D4mThy>$B5Z*!z{Cf!~*{`p{Kq>AC1tt!~*{` zpl6QAj|y>Ccz CJe0^TB``T!MzTE5dCZDQsPA?9@xF*XV?`CCdiTzqPC^2Kf=A{O<3_3lZxbdCi(Z z7VqUtgJeW5-N4ntQQ)NfPK+dAW$Hfl?8sseV%*p^-}Uj|Lrl2PIEzVGlIJ`oxTBXb z^%KXJQ-3X( +{Ep z!WC#%a$tOf^CgXLTQkw7I*S=Gp1_ID<@AO>UVp&dYSYWhPYdyK`7_Iw>&X&4L4CRz zPrh`>8dj<;)`y5YK6*-f5%lD$G4`4`|Nd{{eM|CjA7tYqx!8z{@D>ay4MQs3V)5kT zG0e`ns;wdSU5pu }dAsdZw?CJ+eNuZRwNJ;;)I?INA6XKb$WA;9w kvGgE-E-fAGiHt?!@cEmJCd?nRXeKFQT}78EfC4bde(LMrP! Kjj&`7Q!W!Y^)H+9VqGH!<&(K1Eo=Yp7EIEM~wQrcFF4_ z2}PPFg))ffeH5g=)0RaJkpI-zpMTxThcD&TyzsgupBlhE7Fy+yN~|!fBZ>jG*Y+#c z08Yn~o8|OUE>OG}%_kT#yjr|8Wt<#3@nhms@xws#4xJE+X;#-hz6x!chvP|V+F}5Z zSq-=4mdQ_V7R%wy$Yea{+18q-aLg>>j!^WrdMn9P<6YS#e}isq#2bCGet+Pkp_L?_ z6>K&Z`xHO?@Pq7gSV&9CO{n;O_5Ts~F3XYQSlZxzo`Ta_WJX(15uvE;>WqnsHihKM zP_-^z?ovdRm6f?s?v$Jrbdt%7F4fUyy_}jim|fd!y0X{*Zg#)kWS(Sj4nPugg1`aj zHDgs7asuak=K~1hjsq6&y? EVQ(O7Uh{A ztS QsrZWfut*S-0_SipWx z@UJ7cLAXD982AnUT|m&cpA%7D|71y*9}H^_kribg2Ag6G<9`7}+yF4g-4W;4XIH1w zvxzsqnoSPRExL($xU90US@yys$BtZ;P_yU19$inL|5`5#vaa-GHk(|}Y$)Ev=#e}e zx`c|#ZyRU!{0j!5hvW7-Dk7^*T5e5>70%35SThWDUfmO0G2sND_*K`#t|W)>_%og= z#9vJ%X+ahaDS!HWw$l$*n+QtKn6)1b;Or7S$@*e$^%X|GvR)%9f`koQ{W!g2z`+W3 ziiG)n5ZCR+8~+D?omNHoH4HQN-5d61P+`Fo7NFq0uQlX>&71un{!!L?VUzAW51bPN zy7R-??9jsCGQiVh8FU(<_?b@gYV50^<`4)T<1*nal7E$B)gj$sf^eI+Yo#QV=My$E zvZxb_b$TDVc7KKP{L|&<$(fbS2X~81b)7y~Sc6mku$A^6y#oX*{JX+4cq2Y11L~Z| zdl>)xi}(GZknpBl;)Q(*cEh+~czJPr_|pWsQfFtU7e84MGVd7H@Zib=n}-K&f`9Y0 za_vnv>VNauTUKRXv7Z*U%fNY$M6Da-b_UC;aJR^n;g5~Y)VVt3s;R{SdUM9g)nubT zzy1gg|36L~jOSymGl1r&m3@|3(UN;7$zUr|p44MrcS<@;5$nCjBW5C69zyR~eE(%0 zL?xg1V*T_eUBLpoBX2d<)$H)}Y68CbJ?QhFUVk2+vQdf^L)>%kPKvA7d-v& -+ z-QM@;9jt)ALpRvbxM%>O*Wu%y^3Ri3)5H0k{jFzB>5Rur7;s(734(_xNt}BCLix+- z#f4(Qb{!Qs?e$6=_`x;s#gGen+2W^K^Ke^}doL3Qh`{4f{?^X!o(M#r3<}?^bQ;@8 zRDV{I*M!Bv>o@;Q&yAiYzjdbdrS#h%xrf8c5NStX4_v$^Q4Y5nK-dUEJ*emZ@Gt-_ zMQ~xZ{JDy_+fm5syA+Pcj^d{TJZwYZ75;QNnftR5RM**ZS0n*SdiFj|k8Fu$jbs5E zyu*|YrPDjU7i$bl9qyqN>7C;^Op3Nl0e>z^<>(HuOwnyA8|SUUbPW4J%%dg(4t=1H z)XxxM$@w vZAXI_!~dJItteAoN@>N`nBR9(NeOjI7Mf>Bi!2bdimAKF$$t%p7T&%9L_U4O3+ zL0UIJq1Gw0lF_8=m$?ZCe*& HZfGi|OMAZ9>3mi! 5^Xq6~g`)a7czhZrqKi=c({t^sn&vlJCV4 zB0P+mf;>#1`wpumE1x##AEG?~HLHWY)oRfMQ~5P7j=jVzPygfc;zezUOx{S z=ET~xMboA&*hjjMzhQcn#acrh4%TDOe}y|>a5p@w4Bqjg%Gw&H$1wCAJ%2OLy_@JJ zj?!`s-iQ5S9c^~TTmk)2ggpNZCJp8+Xt&(iw+xOrjb2~IB}2B-Pmz~A|58VfN}YT^ zK(V{zbYbO-0*aAzvnV9*)Uu?!4SfV5+KpN^)I%{yubO*?cE??T{rp!5d;Ue#H2gl7 zaS13&*71i84y;4rZ-RWe-hX8hROk~7gzXdF^M3^%5B-;2q#_TpXtjI#0#@+mPb2w4 z#f-ZlStqW(?~zx_^r1Z|1qMTZ=O*G79Kknk#&?&jE!-1$l)ROX;tZDqgv+wxcXY); ze#0G)B`Y6pv&WO{p~?G_Rcgo<&jL>f*0=khtcS85$f74okR6o;n}4uerz?KmZWWdx z_hk{Gv!y3kR*aTUJ8tpmCSk4zMIo_P}L5@Su&-FJa-8iNEybpH9!mWkmOjfd(*2 zsvXuaLk70K!#e!$kr>GFgI}}IIOfwAtW2eOSS&*^{oI_90}tz>GBi5=-9;|AGaJZ0 zC@}}W_G&;JAIq3>w|{_rPT;b;1s#?5;LxSMH%DB0R)}E*)WiAi^3rGt?uKn7#06`$ z1XTF5Q3)NmA gL%?pPF>H;rie_t@uvjnd2>$aw^)$q= zPi(S-`muIW2JOOA#s>HN(y02 M$xPaJdaz__!ZHSR5~^+^-V9_P;Av8E{UDRVXVb-fouA`!~$5gBth9 z#niki@q}8Z{;Ac7sqgLt_1EbJ7DKAAzP76H#dOHMd_Qs$Sd0<7nJ~^^cZ;gmYKyzG zisoLsd-;myB!4grH@EYo43i3)7I6p@HuC`u(#f{rgqu*0xCnezD8et;gaiy8bW^7w z6h|eXS5&(utUW2=g>ZGw`VY_{E(@{V@S{t@+>QNzQ00W+>o4@Ty8y5=Ge>c}3g|v0 zch9!cZUH|j**6<{+dT)^4_XU*wJ w;V*ytw7hzTzt z@NnrA_`j zcQ_{uH>-*@nqrmrCaCUVTp9eiD(a0&b-i_$r-k{tr-9^*uy!g`y(v@0WV7Z%sCunx zTSI~>UVq(0%n2|DhsFx|voBcvg?o=SK`htaVUmY;5u7Gz?*HWXQnJ?WF@$Yh1t7qM zi6AcSKE(M4JOg|iEU8Oh-5mV?f#n&?Of=c4)WD|K*le?X%O+NS{KihOx$jMdQMe%z z 6Yj^FKrW)=38V2va-=V=YKzRo$9VpF-p5*oszH}RKOwcW`+{) z$t=^VFx=`+;W@%wJ^n-8ciy}n-aP*&%k6gN^d(-_3Hqu_-_1Fa AO(0HDWBroCK`I{LM#q3T?Fc;o@=8d+l$!`5(zh8Sc1e;)gB^P;ED!NO z+<$GFKd>T!RdA73;9;*>5f$CIfE6PK_On|z!qwCq^t{7l<(+Untm=hcvmz_Hk%3q1 z5i>vO#`<6|cPl{lMy{pJ3#;#-O9dL<_o(uM_o(WdYdxW) zO)>VY7x%B 7MMsdN;Y63B z2=Li66jGKxSAGhlQJL4?5>fff+q;pp8kqJ%iOM@V`H4j6z^Mcs@Dg+!ayN`vu5qER zOqVGu*{?j2O)PZN b~L zMUE2Iz2;>e@@uh~v};1>2FAP{+BB~;yDJ3`gY8M7MBt8!&IoRD #)%GZkLw$4?tdqJY?=~qNDdBcaJn*kD%Xa@E2t4soBK~6n-&X7 zFqgxtO;FA*8I;2|ddf9|L{9{fnB2l#t#A*}4nk ~M01s;c99}lV7R~*~D%z}D zQSxP0a2be)TKQ{gCo*Xs0hMX6LO%p)NDS;<72={Sv{R c^t{?{NaYgpy41N%;iOS6Aw|&5-tj-=q4S1~b z;;yzY?u2qMEi~D$8UCUetS4(=#se;(s_P=s=2jICBj9;0023xc0)H#I!7Z*z+B}!T zo9fQ#P4xgoz%c|+-&W^7zjhIwU(QyBF)UH$cjoO!DuOoH@^yjIsyx5>jSW^}meGwg zC1E4Fg_7#juB+E-`oJDG+L{k#>M3jiNrb$01XICv6p0^S6bagS!>Z{V;v0%_^2>ZH zyOOgF_;gDc=Zb1?s(*0bRg4pE=+cFhvNRGaPdVP*QX8~S#SqXGBXHdrBe0aJmi?6M z% WVoJiY(q0u {(r`brWAt9^bun&1Kf;9 zH^un-6YzrHjH*bTl(yyJZji!pO_~92);43pt5Rs)o)rOXBl3yfzEvdq2y+b4-Rz+v z0#{13hg&ykx#nBO@SnO* 8r)|Yo|31Ni5f4y-~01))>~Q(Li0TLvJpk zz^%Z;;%LYvxr!qnP{rVn5w=`HB_aGm+mqB_T{UqY#9>_{;wUsbE1wKjzU``=(s3nj zhu&FJg=+xho41--r0G!NZp+NP@0d&B%yiRW&QGqUxPNBNh!oAj^>Up?MTAGaSp0n0 z-rmyW@U1n UgEYZ89O!N{s4V8Iwdt}9=~=D^Igw+Ze|g7BJYNE48OKF* z{K|~Xf{C({tp_VC& #N9k zm3snvWSTgy5icxB;FVp{S-VEM7(-8kx-;UeVuWMz6(1HiP9esy9)O(I!BD$4?k8;d z;>rIwy2*q5$*+pBTorI|(9t_P{Wv?E{lUAQAO19P;)r0ZZ7N5WfgTuoH@HTs7inq# z#(zsV1{752Gp3g8o)o2e4=_=;vvkF!Jv?;Iq@RDe ??HrB*^Gzos+*T6^)$ty)5{`CSirYjLN61-i(N@gV2wmf45{~WalKAaAAnoNPjd= zsW#k6;{M%oe;Lq|foG^osLPk@%#m^z D(``bFyDw?AsD^bAWKP3xYqlkW zX`QtP4u*jCnlKWiBt8sY *cT;l-5>U3(wWfQ61u=QeSwB|q?25x*|g#i*>lb(%f!jz!EM6={n- z&A6;^Jb`-