Full Code of bgreenlee/pygtail for AI

main 6ba4c2e3b24d cached
19 files
61.1 KB
15.5k tokens
67 symbols
1 requests
Download .txt
Repository: bgreenlee/pygtail
Branch: main
Commit: 6ba4c2e3b24d
Files: 19
Total size: 61.1 KB

Directory structure:
gitextract_bk990au9/

├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── README.txt
├── debian/
│   ├── changelog
│   ├── compat
│   ├── control
│   ├── copyright
│   ├── rules
│   └── source/
│       └── format
├── makedoc.sh
├── pygtail/
│   ├── __init__.py
│   ├── core.py
│   └── test/
│       ├── __init__.py
│       └── test_pygtail.py
├── pypi.txt
├── setup.py
└── tox.ini

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
*.pyc
*.egg-info
.coverage
build
dist
.tox
debian/.debhelper/**
debian/files
debian/pygtail/**
debian/tmp/**
debian/python-pygtail*
debian/python3-pygtail*
.pybuild/


================================================
FILE: .travis.yml
================================================
language: python
python:
  - 2.7
  - pypy
  - 3.6
  - 3.7
  - 3.8
  - 3.9
  - 3.10
  - 3.11
script: python setup.py test


================================================
FILE: LICENSE
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 2, June 1991

 Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The licenses for most software are designed to take away your
freedom to share and change it.  By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users.  This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it.  (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.)  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.

  To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have.  You must make sure that they, too, receive or can get the
source code.  And you must show them these terms so they know their
rights.

  We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.

  Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software.  If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.

  Finally, any free program is threatened constantly by software
patents.  We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary.  To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.

  The precise terms and conditions for copying, distribution and
modification follow.

                    GNU GENERAL PUBLIC LICENSE
   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

  0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License.  The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language.  (Hereinafter, translation is included without limitation in
the term "modification".)  Each licensee is addressed as "you".

Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope.  The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.

  1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.

You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.

  2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:

    a) You must cause the modified files to carry prominent notices
    stating that you changed the files and the date of any change.

    b) You must cause any work that you distribute or publish, that in
    whole or in part contains or is derived from the Program or any
    part thereof, to be licensed as a whole at no charge to all third
    parties under the terms of this License.

    c) If the modified program normally reads commands interactively
    when run, you must cause it, when started running for such
    interactive use in the most ordinary way, to print or display an
    announcement including an appropriate copyright notice and a
    notice that there is no warranty (or else, saying that you provide
    a warranty) and that users may redistribute the program under
    these conditions, and telling the user how to view a copy of this
    License.  (Exception: if the Program itself is interactive but
    does not normally print such an announcement, your work based on
    the Program is not required to print an announcement.)

These requirements apply to the modified work as a whole.  If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works.  But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.

Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.

In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.

  3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:

    a) Accompany it with the complete corresponding machine-readable
    source code, which must be distributed under the terms of Sections
    1 and 2 above on a medium customarily used for software interchange; or,

    b) Accompany it with a written offer, valid for at least three
    years, to give any third party, for a charge no more than your
    cost of physically performing source distribution, a complete
    machine-readable copy of the corresponding source code, to be
    distributed under the terms of Sections 1 and 2 above on a medium
    customarily used for software interchange; or,

    c) Accompany it with the information you received as to the offer
    to distribute corresponding source code.  (This alternative is
    allowed only for noncommercial distribution and only if you
    received the program in object code or executable form with such
    an offer, in accord with Subsection b above.)

The source code for a work means the preferred form of the work for
making modifications to it.  For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable.  However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.

If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.

  4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License.  Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.

  5. You are not required to accept this License, since you have not
signed it.  However, nothing else grants you permission to modify or
distribute the Program or its derivative works.  These actions are
prohibited by law if you do not accept this License.  Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.

  6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions.  You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.

  7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all.  For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.

If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.

It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices.  Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.

This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.

  8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded.  In such case, this License incorporates
the limitation as if written in the body of this License.

  9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

Each version is given a distinguishing version number.  If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation.  If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.

  10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission.  For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this.  Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.

                            NO WARRANTY

  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.

  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

Also add information on how to contact you by electronic and paper mail.

If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:

    Gnomovision version 69, Copyright (C) year name of author
    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.

You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary.  Here is a sample; alter the names:

  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
  `Gnomovision' (which makes passes at compilers) written by James Hacker.

  <signature of Ty Coon>, 1 April 1989
  Ty Coon, President of Vice

This General Public License does not permit incorporating your program into
proprietary programs.  If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

================================================
FILE: README.md
================================================
pygtail
=======

A python "port" of [logcheck's logtail2](http://logcheck.org).

Pygtail reads log file lines that have not been read. It will even handle log
files that have been rotated.

Usage
-----

From the command line:

    Usage: pygtail.py [options] logfile

    Print log file lines that have not been read.

    Options:
      -h, --help            show this help message and exit
      -o OFFSET_FILE, --offset-file=OFFSET_FILE
                            File to which offset data is written (default:
                            <logfile>.offset).
      -p, --paranoid        Update the offset file every time we read a line
                            (as opposed to only when we reach the end of the
                            file).
      -n N, --every-n=N     Update the offset file every N'th time we read a
                            line (as opposed to only when we reach the end of
                            the file).
      --no-copytruncate     Don't support copytruncate-style log rotation.
                            Instead, if the log file shrinks, print a warning.
      --read-from-end       Read log file from the end if offset file is
                            missing. Useful for large files.
      --log-pattern         Custom log rotation glob pattern. Use %s to
                            represent the original filename. You may use this
                            multiple times to provide multiple patterns.
      --full_lines          Only log when line ends in a newline `\n`
                            (default: False)
      --encoding ENCODING   Encoding to use for reading files (default: system
                            encoding)
      --version             Print version and exit.

In your code:

```python
from pygtail import Pygtail

for line in Pygtail("some.log"):
    sys.stdout.write(line)
```

An example showing iterating over lines with offsets and manual control over offset updates:

```python
import pygtail

tail = pygtail.Pygtail(logfile, save_on_end=False, copytruncate=False)

for line, offset in tail.with_offsets():
    # Do someting

 # figure out right offset to save
 tail.write_offset_to_file(right_offset)
```

Contributing
------------

Pull requests are very much welcome, but I will not merge your changes if you don't include a test. Run tests with `python setup.py test`.

Build status
------------

[![Build Status](https://secure.travis-ci.org/bgreenlee/pygtail.png)](http://travis-ci.org/bgreenlee/pygtail)




================================================
FILE: README.txt
================================================
pygtail
=======

A python "port" of `logcheck's logtail2 <http://logcheck.org>`__.

Pygtail reads log file lines that have not been read. It will even
handle log files that have been rotated.

Usage
-----

From the command line:

::

    Usage: pygtail.py [options] logfile

    Print log file lines that have not been read.

    Options:
      -h, --help            show this help message and exit
      -o OFFSET_FILE, --offset-file=OFFSET_FILE
                            File to which offset data is written (default:
                            <logfile>.offset).
      -p, --paranoid        Update the offset file every time we read a line
                            (as opposed to only when we reach the end of the
                            file).
      -n N, --every-n=N     Update the offset file every N'th time we read a
                            line (as opposed to only when we reach the end of
                            the file).
      --no-copytruncate     Don't support copytruncate-style log rotation.
                            Instead, if the log file shrinks, print a warning.
      --read-from-end       Read log file from the end if offset file is
                            missing. Useful for large files.
      --log-pattern         Custom log rotation glob pattern. Use %s to
                            represent the original filename. You may use this
                            multiple times to provide multiple patterns.
      --full_lines          Only log when line ends in a newline `\n`
                            (default: False)
      --encoding ENCODING   Encoding to use for reading files (default: system
                            encoding)
      --version             Print version and exit.

In your code:

.. code:: python

    from pygtail import Pygtail

    for line in Pygtail("some.log"):
        sys.stdout.write(line)


An example showing iterating over lines with offsets and manual control over offset updates:

.. code:: python

    import pygtail

    tail = pygtail.Pygtail(logfile, save_on_end=False, copytruncate=False)

    for line, offset in tail.with_offsets():
        # Do someting

    # figure out right offset to save
    tail.write_offset_to_file(right_offset)


Contributing
------------

Pull requests are very much welcome, but I will not merge your changes if you don't include a test. Run tests with `python setup.py test`.

Build status
------------

|Build Status|

.. |Build Status| image:: https://secure.travis-ci.org/bgreenlee/pygtail.png
   :target: http://travis-ci.org/bgreenlee/pygtail


================================================
FILE: debian/changelog
================================================
pygtail (0.14.0) lucid; urgency=low

  * Added with_offsets iterator and write_offset_to_file method. (thanks @pajowu!)
  * Included LICENSE file in distribution.

 -- Brad Greenlee <brad@footle.org>  Sun, 6 Nov 2022 06:48:00 -0800

pygtail (0.13.0) lucid; urgency=low

  * Add option to set the file encoding. (thanks @Robert-Lebedeu and @ondrej-bouda!)
  * Add test for renamecreate when file is rotated to unknown name and pygtail is freshly reading file (thanks @arekm!)
  * Add support for failing test proposed in #42

 -- Brad Greenlee <brad@footle.org>  Sun, 30 Oct 2022 11:48:00 -0700

 pygtail (0.12.0) lucid; urgency=low

  * Add option to disable save on end. (thanks @pajowu!)

 -- Brad Greenlee <brad@footle.org>  Thu, 14 Apr 2022 08:31:00 -0700

pygtail (0.11.1) lucid; urgency=low

  * Update debian packaging

 -- Sebastien GALLET <bibi21000@gmail.com>  Mon, 11 Oct 2019 11:30:00 +0100

pygtail (0.11.0) lucid; urgency=low

  * Add option to only log lines ending in a newline (thanks @MDunitz!)

 -- Brad Greenlee <brad@footle.org>  Tue, 16 Jul 2019 05:12:00 -0700

pygtail (0.10.1) lucid; urgency=low

  * Fix keyword argument mismatch (thanks @ushuz!)

 -- Brad Greenlee <brad@footle.org>  Mon, 04 Mar 2019 22:18:00 -0800

pygtail (0.10.0) lucid; urgency=low

  * Add --log-pattern flag to allow for custom rotated log filename patterns. (thanks @silenben)

 -- Brad Greenlee <brad@footle.org> Tue, 20 Nov 2018 04:48:00 -0800

pygtail (0.9.0) lucid; urgency=low

  * Add --read-from-end flag to read the log file from the end if the offset file is missing. (thanks Dmitry Sytsko)

 -- Brad Greenlee <brad@footle.org> Thu, 02 Aug 2018 16:52:00 -0700

pygtail (0.8.0) lucid; urgency=low

  * Add handling for "renamecreate" semantics where the currently processed file gets renamed and the
    original file gets recreated. This is the behavior of certain logfile rollers such as
    TimeBasedRollingPolicy in Java's Logback library. (thanks @bobtiernay-okta)

 -- Brad Greenlee <brad@footle.org>  Sat, 10 Mar 2018 08:39:18 -0800

pygtail (0.7.0) lucid; urgency=low

  * Add handling for logrotate dateext dateformat -%Y%m%d-%s log files, with and without
    delaycompress (thanks @akashawasthi)

 -- Brad Greenlee <brad@footle.org>  Thu, 30 Jun 2016 20:25:05 -0700

pygtail (0.6.1) lucid; urgency=low

  * Enable line buffering to avoid reading incomplete lines (thanks @olystretch)

 -- Brad Greenlee <brad@footle.org>  Thu, 05 Nov 2015 20:13:52 -0800

pygtail (0.6.0) lucid; urgency=low

  * Added flag to write offset file only after every n lines, and ability to optionally call a
    function whenever the offset file is written (thanks @btilly)

 -- Brad Greenlee <brad@footle.org>  Thu, 08 Oct 2015 05:19:19 -0700

pygtail (0.5.3) lucid; urgency=low

  * Fixed Python 3 compatibility (thanks @sbraz)

 -- Brad Greenlee <brad@footle.org>  Thu, 16 Apr 2015 17:30:43 -0700

pygtail (0.5.2) lucid; urgency=medium

  * Fixed issue with potentially incorrect offset due to python's read-ahead buffer (thanks @czchen)

 -- Brad Greenlee <brad@footle.org>  Thu, 26 Mar 2015 09:10:36 -0400

pygtail (0.5.1) lucid; urgency=low

  * Fixed print statement to be Python 3 compatible (thanks @benburry)

 -- Brad Greenlee <brad@footle.org>  Fri, 4 Jan 2015 20:36:00 -0800

pygtail (0.5.0) lucid; urgency=low

  * Add handling of rotated and immediately compressed files (thanks @NotSqrt)

 -- Brad Greenlee <brad@footle.org>  Fri, 17 Oct 2014 09:39:00 -0700

pygtail (0.4.0) lucid; urgency=low

  * Make support for copytruncate log rotation the default.
  * Add --no-copytruncate commandline switch.
  * Print warning if copytruncate is disabled and the log file shrinks

 -- Brad Greenlee <brad@footle.org>  Fri, 9 May 2014 17:31:00 -0700

pygtail (0.3.0) lucid; urgency=low

  * Add support for copytruncate style logrotation (contributed by https://github.com/atward)

 -- Brad Greenlee <brad@footle.org>  Fri, 9 May 2014 16:00:00 -0700

pygtail (0.2.2) lucid; urgency=low

  * Add parsing support for python's TimedRotatingFileHandler (contributed by https://github.com/darknight)

 -- Brad Greenlee <brad@footle.org>  Wed, 21 Aug 2013 15:50:00 -0700

pygtail (0.2.1) lucid; urgency=low

  * Add ReStructuredText README for PyPi.

 -- Brad Greenlee <brad@footle.org>  Wed, 06 Jul 2011 12:37:08 -0700

pygtail (0.2) lucid; urgency=low

  * Add setup.py and debianize.

 -- Brad Greenlee <brad@footle.org>  Wed, 05 Jul 2011 15:04:42 -0700


================================================
FILE: debian/compat
================================================
7


================================================
FILE: debian/control
================================================
Source: pygtail
Section: python
Priority: optional
Maintainer: Brad Greenlee <brad@footle.org>
Build-Depends: debhelper (>= 7)
Build-Depends-Indep: dh-python, python-all, python-setuptools,
 python3-all, python3-setuptools
Standards-Version: 3.8.3
Homepage: http://github.com/bgreenlee/pygtail

Package: python-pygtail
Architecture: all
Depends: ${python:Depends}, ${misc:Depends}
Description: Reads log file lines that have not been read. (Python 2)
 Pygtail reads log file lines that have not been read. It will even
 handle log files that have been rotated.
 .
 This package installs the program for Python 2.

Package: python3-pygtail
Architecture: all
Depends: ${python3:Depends}, ${misc:Depends}
Description: Reads log file lines that have not been read. (Python 3)
 Pygtail reads log file lines that have not been read. It will even
 handle log files that have been rotated.
 .
 This package installs the program for Python 3.


================================================
FILE: debian/copyright
================================================
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: pygtail
Upstream-Contact: Brad Greenlee <brad@footle.org>
Source: https://github.com/bgreenlee/pygtail

Files: *
Copyright: 2011 Brad Greenlee <brad@footle.org>
License: GPL v2
 http://www.gnu.org/licenses/gpl-2.0.html


================================================
FILE: debian/rules
================================================
#!/usr/bin/make -f

#export DH_VERBOSE = 1
export PYBUILD_NAME = pygtail

%:
	dh $@ --with python2,python3 --buildsystem=pybuild
		
#~ override_dh_auto_test:
#~ 	dh_auto_test
#~ 	set -ex; cd pygtail && for python in $(PYTHON3_VERSIONS) $(PYTHON2_VERSIONS); do \
#~ 			$$python test/test_pygtail.py; \
#~ 	done


================================================
FILE: debian/source/format
================================================
1.0


================================================
FILE: makedoc.sh
================================================
#!/bin/bash

pandoc -f markdown -t rst -o README.txt README.md


================================================
FILE: pygtail/__init__.py
================================================
from pygtail.core import __version__
from pygtail.core import Pygtail


================================================
FILE: pygtail/core.py
================================================
#!/usr/bin/python -tt
# -*- coding: utf-8 -*-

# pygtail - a python "port" of logtail2
# Copyright (C) 2011 Brad Greenlee <brad@footle.org>
#
# Derived from logcheck <http://logcheck.org>
# Copyright (C) 2003 Jonathan Middleton <jjm@ixtab.org.uk>
# Copyright (C) 2001 Paul Slootman <paul@debian.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

from __future__ import print_function
from os import fstat, stat
import os
from os.path import exists, getsize
import sys
import glob
import gzip
import io
from optparse import OptionParser

__version__ = '0.14.0'


PY3 = sys.version_info[0] == 3

if PY3:
    text_type = str
else:
    text_type = unicode


def force_text(s, encoding='utf-8', errors='strict'):
    if isinstance(s, text_type):
        return s
    return s.decode(encoding, errors)


class Offset:
    """Data-class to store file-offsets"""

    def __init__(self, counter, inode, offset):
        self.counter = counter
        self.inode = inode
        self.offset = offset

    def __eq__(self, other):
        return self.counter == other.counter and self.offset == other.offset

    def __lt__(self, other):
        return self.counter < other.counter or (
            self.counter == other.counter and self.offset < other.offset
        )

    def __le__(self, other):
        return self.__lt__(other) or self.__eq__(other)

    def __gt__(self, other):
        return not self.__le__(other)

    def __ge__(self, other):
        return not self.__le__(other) or self.__eq__(other)

    def __repr__(self):
        return "Offset(counter=%d, inode=%d, offset=%d" % (
            self.counter,
            self.inode,
            self.offset,
        )


class PygtailIteratorWithOffsets:
    def __init__(self, pygtail):
        self._pygtail = pygtail

    def __next__(self):
        return self.next()

    def __iter__(self):
        return self

    def next(self):
        next_line = self._pygtail.next()
        offset = self._pygtail._filehandle().tell()
        inode = fstat(self._pygtail._filehandle().fileno()).st_ino
        counter = self._pygtail._counter
        offset_instance = Offset(counter, inode, offset)
        return next_line, offset_instance


class Pygtail(object):
    """
    Creates an iterable object that returns only unread lines.

    Keyword arguments:
    offset_file   File to which offset data is written (default: <logfile>.offset).
    paranoid      Update the offset file every time we read a line (as opposed to
                  only when we reach the end of the file (default: False))
    every_n       Update the offset file every n'th line (as opposed to only when
                  we reach the end of the file (default: 0))
    on_update     Execute this function when offset data is written (default False)
    copytruncate  Support copytruncate-style log rotation (default: True)
    log_patterns  List of custom rotated log patterns to match (default: None)
    full_lines    Only log when line ends in a newline `\n` (default: False)
    save_on_end   Automatically save the offset once the end of the file is reached (default: True)
    """
    def __init__(self, filename, offset_file=None, paranoid=False, copytruncate=True,
                 every_n=0, on_update=False, read_from_end=False, log_patterns=None, full_lines=False,
                 save_on_end=True, encoding=None):
        self.filename = filename
        self.paranoid = paranoid
        self.every_n = every_n
        self.on_update = on_update
        self.copytruncate = copytruncate
        self.read_from_end = read_from_end
        self.log_patterns = log_patterns
        self.full_lines = full_lines
        self.save_on_end = save_on_end
        self.encoding = encoding
        self.offset_file = offset_file or "%s.offset" % self.filename
        self.offset_file_inode = 0
        self.offset = 0
        self.since_update = 0
        self.fh = None
        self.rotated_logfile = None
        self._counter = 0

        # if offset file exists and non-empty, open and parse it
        if exists(self.offset_file) and getsize(self.offset_file):
            offset_fh = open(self.offset_file, "r")
            (self.offset_file_inode, self.offset) = \
                [int(line.strip()) for line in offset_fh]
            offset_fh.close()
            if self.offset_file_inode != stat(self.filename).st_ino or \
                    stat(self.filename).st_size < self.offset:
                # The inode has changed or filesize has reduced so the file
                # might have been rotated.
                # Look for the rotated file and process that if we find it.
                self.rotated_logfile = self._determine_rotated_logfile()
                # If copytruncate is enabled and we can't find the rotated logfile, all we can do is reset.
                if self.copytruncate and self.rotated_logfile is None:
                    sys.stderr.write("[pygtail] [WARN] log file was rotated to unknown location. Resetting.\n")
                    self.offset = 0
                    self.update_offset_file()


    def __del__(self):
        if self._filehandle():
            self._filehandle().close()

    def __iter__(self):
        return self

    def next(self):
        """
        Return the next line in the file, updating the offset.
        """
        try:
            line = self._get_next_line()
        except StopIteration:
            # we've reached the end of the file; if we're processing the
            # rotated log file or the file has been renamed, we can continue with the actual file; otherwise
            # update the offset file
            if self._is_new_file():
                self.rotated_logfile = None
                self.fh.close()
                self.offset = 0
                # open up current logfile and continue
                try:
                    line = self._get_next_line()
                except StopIteration:  # oops, empty file
                    if self.save_on_end:
                        self.update_offset_file()
                    raise
            else:
                if self.save_on_end:
                    self.update_offset_file()
                raise

        if self.paranoid:
            self.update_offset_file()
        elif self.every_n and self.every_n <= self.since_update:
            self.update_offset_file()

        return line

    def with_offsets(self):
        """Returns an iterator that yields lines with their internal offset state"""
        return PygtailIteratorWithOffsets(self)

    def __next__(self):
        """`__next__` is the Python 3 version of `next`"""
        return self.next()

    def readlines(self):
        """
        Read in all unread lines and return them as a list.
        """
        return [line for line in self]

    def read(self):
        """
        Read in all unread lines and return them as a single string.
        """
        lines = self.readlines()
        if lines:
            try:
                return ''.join(lines)
            except TypeError:
                return ''.join(force_text(line) for line in lines)
        else:
            return None

    def _is_closed(self):
        if not self.fh:
            return True
        try:
            return self.fh.closed
        except AttributeError:
            if isinstance(self.fh, gzip.GzipFile):
                # python 2.6
                return self.fh.fileobj is None
            else:
                raise

    def _filehandle(self):
        """
        Return a filehandle to the file being tailed, with the position set
        to the current offset.
        """
        if not self.fh or self._is_closed():
            self._counter += 1
            filename = self.rotated_logfile or self.filename
            if filename.endswith('.gz'):
                self.fh = gzip.open(filename, 'r')
            elif PY3:
                self.fh = open(filename, "r", 1, encoding=self.encoding)
            else:
                self.fh = io.open(filename, "r", 1, encoding=self.encoding)
            if self.read_from_end and not exists(self.offset_file):
                self.fh.seek(0, os.SEEK_END)
            else:
                self.fh.seek(self.offset)

        return self.fh

    def update_offset_file(self):
        """
        Update the offset file with the current inode and offset.
        """
        if self.on_update:
            self.on_update()
        tmp_filename = self.offset_file + ".tmp"
        offset = self._filehandle().tell()
        inode = fstat(self._filehandle().fileno()).st_ino
        fh = open(tmp_filename, "w")
        fh.write("%s\n%s\n" % (inode, offset))
        fh.close()
        os.rename(tmp_filename, self.offset_file)  # atomic file update, safer
        self.since_update = 0

    def write_offset_to_file(self, offset):
        """Writes an `Offset` to the offset file"""
        if self.on_update:
            self.on_update()
        fh = open(self.offset_file, "w")
        fh.write("%s\n%s\n" % (offset.inode, offset.offset))
        fh.close()

    def _determine_rotated_logfile(self):
        """
        We suspect the logfile has been rotated, so try to guess what the
        rotated filename is, and return it.
        """
        rotated_filename = self._check_rotated_filename_candidates()
        if rotated_filename and exists(rotated_filename):
            if stat(rotated_filename).st_ino == self.offset_file_inode:
                return rotated_filename

            # if the inode hasn't changed, then the file shrank; this is expected with copytruncate,
            # otherwise print a warning
            if stat(self.filename).st_ino == self.offset_file_inode:
                if self.copytruncate:
                    return rotated_filename
                else:
                    sys.stderr.write(
                        "[pygtail] [WARN] file size of %s shrank, and copytruncate support is "
                        "disabled (expected at least %d bytes, was %d bytes).\n" %
                        (self.filename, self.offset, stat(self.filename).st_size))

        return None

    def _check_rotated_filename_candidates(self):
        """
        Check for various rotated logfile filename patterns and return the first
        match we find.
        """
        # savelog(8)
        candidate = "%s.0" % self.filename
        if (exists(candidate) and exists("%s.1.gz" % self.filename) and
            (stat(candidate).st_mtime > stat("%s.1.gz" % self.filename).st_mtime)):
            return candidate

        # logrotate(8)
        # with delaycompress
        candidate = "%s.1" % self.filename
        if exists(candidate):
            return candidate

        # without delaycompress
        candidate = "%s.1.gz" % self.filename
        if exists(candidate):
            return candidate

        rotated_filename_patterns = [
            # logrotate dateext rotation scheme - `dateformat -%Y%m%d` + with `delaycompress`
            "%s-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]",
            # logrotate dateext rotation scheme - `dateformat -%Y%m%d` + without `delaycompress`
            "%s-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9].gz",
            # logrotate dateext rotation scheme - `dateformat -%Y%m%d-%s` + with `delaycompress`
            "%s-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]",
            # logrotate dateext rotation scheme - `dateformat -%Y%m%d-%s` + without `delaycompress`
            "%s-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9].gz",
            # for TimedRotatingFileHandler
            "%s.[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]",
        ]
        if self.log_patterns:
            rotated_filename_patterns.extend(self.log_patterns)

        # break into directory and filename components to support cases where the
        # the file is prepended as part of rotation
        file_dir, rel_filename = os.path.split(self.filename)
        for rotated_filename_pattern in rotated_filename_patterns:
            candidates = glob.glob(os.path.join(file_dir, rotated_filename_pattern % rel_filename))
            if candidates:
                candidates.sort()
                return candidates[-1]  # return most recent

        # no match
        return None

    def _is_new_file(self):
        # Processing rotated logfile or at the end of current file which has been renamed
        return self.rotated_logfile or \
               self._filehandle().tell() == fstat(self._filehandle().fileno()).st_size and \
               fstat(self._filehandle().fileno()).st_ino != stat(self.filename).st_ino

    def _get_next_line(self):
        curr_offset = self._filehandle().tell()
        line = self._filehandle().readline()
        if self.full_lines:
            if not line.endswith('\n'):
                self._filehandle().seek(curr_offset)
                raise StopIteration
        if not line:
            raise StopIteration
        self.since_update += 1
        return line


def main():
    # command-line parsing
    cmdline = OptionParser(usage="usage: %prog [options] logfile",
        description="Print log file lines that have not been read.")
    cmdline.add_option("--offset-file", "-o", action="store",
        help="File to which offset data is written (default: <logfile>.offset).")
    cmdline.add_option("--paranoid", "-p", action="store_true",
        help="Update the offset file every time we read a line (as opposed to"
             " only when we reach the end of the file).")
    cmdline.add_option("--every-n", "-n", action="store",
        help="Update the offset file every n'th time we read a line (as opposed to"
             " only when we reach the end of the file).")
    cmdline.add_option("--no-copytruncate", action="store_true",
        help="Don't support copytruncate-style log rotation. Instead, if the log file"
             " shrinks, print a warning.")
    cmdline.add_option("--read-from-end", action="store_true",
        help="Read log file from the end if offset file is missing. Useful for large files.")
    cmdline.add_option("--log-pattern", action="append",
        help="Custom log rotation glob pattern. Use %s to represent the original filename."
             " You may use this multiple times to provide multiple patterns.")
    cmdline.add_option("--full_lines", action="store_true",
                       help="Only log when line ends in a newline (\\n)")
    cmdline.add_option("--encoding", action="store",
        help="Encoding to use for reading files (default: system encoding)")
    cmdline.add_option("--version", action="store_true",
        help="Print version and exit.")

    options, args = cmdline.parse_args()

    if options.version:
        print("pygtail version", __version__)
        sys.exit(0)

    if (len(args) != 1):
        cmdline.error("Please provide a logfile to read.")

    if options.every_n:
        options.every_n = int(options.every_n)
    pygtail = Pygtail(args[0],
                      offset_file=options.offset_file,
                      paranoid=options.paranoid,
                      every_n=options.every_n,
                      copytruncate=not options.no_copytruncate,
                      read_from_end=options.read_from_end,
                      log_patterns=options.log_pattern,
                      full_lines=options.full_lines,
                      encoding=options.encoding)

    for line in pygtail:
        sys.stdout.write(line)


if __name__ == "__main__":
    main()


================================================
FILE: pygtail/test/__init__.py
================================================
#

================================================
FILE: pygtail/test/test_pygtail.py
================================================
import os
import sys

try:
    # python 2.6
    import unittest2 as unittest
except ImportError:
    import unittest
import shutil
import tempfile
import gzip
import io

from pygtail import Pygtail


PY2 = sys.version_info[0] == 2


class PygtailTest(unittest.TestCase):
    # TODO:
    # - test for non-default offset file
    # - test for savelog and datext rotation schemes

    def setUp(self):
        self.test_lines = ["1\n", "2\n", "3\n"]
        self.test_str = ''.join(self.test_lines)
        self.logfile = tempfile.NamedTemporaryFile(delete=False)
        self.logfile.write(self.test_str.encode('utf-8'))
        self.logfile.close()

    def append(self, str):
        # append the give string to the temp logfile
        fh = open(self.logfile.name, "a")
        fh.write(str)
        fh.close()

    def copytruncate(self):
        shutil.copyfile(self.logfile.name, "%s.1" % self.logfile.name)
        fh = open(self.logfile.name, "w")
        fh.close()

    def tearDown(self):
        filename = self.logfile.name
        for tmpfile in [filename, filename + ".offset", filename + ".1", filename + ".1.gz"]:
            if os.path.exists(tmpfile):
                os.remove(tmpfile)

    def test_read(self):
        pygtail = Pygtail(self.logfile.name)
        self.assertEqual(pygtail.read(), self.test_str)

    def test_readlines(self):
        pygtail = Pygtail(self.logfile.name)
        self.assertEqual(pygtail.readlines(), self.test_lines)

    def test_subsequent_read_with_no_new_data(self):
        pygtail = Pygtail(self.logfile.name)
        self.assertEqual(pygtail.read(), self.test_str)
        self.assertEqual(pygtail.read(), None)

    def test_subsequent_read_with_new_data(self):
        pygtail = Pygtail(self.logfile.name)
        self.assertEqual(pygtail.read(), self.test_str)
        new_lines = "4\n5\n"
        self.append(new_lines)
        new_pygtail = Pygtail(self.logfile.name)
        self.assertEqual(new_pygtail.read(), new_lines)

    def test_read_from_the_file_end(self):
        pygtail = Pygtail(self.logfile.name, read_from_end=True)
        self.assertEqual(pygtail.read(), None)
        new_lines = "4\n5\n"
        self.append(new_lines)
        new_pygtail = Pygtail(self.logfile.name, read_from_end=True)
        self.assertEqual(new_pygtail.read(), new_lines)

    def test_logrotate_without_delay_compress(self):
        new_lines = ["4\n5\n", "6\n7\n"]
        pygtail = Pygtail(self.logfile.name)
        pygtail.read()
        self.append(new_lines[0])

        # put content to gzip file
        gzip_handle = gzip.open("%s.1.gz" % self.logfile.name, 'wb')
        with open(self.logfile.name, 'rb') as logfile:
            gzip_handle.write(logfile.read())
        gzip_handle.close()

        with open(self.logfile.name, 'w'):
            # truncate file
            pass

        self.append(new_lines[1])
        pygtail = Pygtail(self.logfile.name)
        self.assertEqual(pygtail.read(), ''.join(new_lines))

    def test_logrotate_with_delay_compress(self):
        new_lines = ["4\n5\n", "6\n7\n"]
        pygtail = Pygtail(self.logfile.name)
        pygtail.read()
        self.append(new_lines[0])
        os.rename(self.logfile.name, "%s.1" % self.logfile.name)
        self.append(new_lines[1])
        pygtail = Pygtail(self.logfile.name)
        self.assertEqual(pygtail.read(), ''.join(new_lines))

    def test_logrotate_with_dateext_with_delaycompress(self):
        new_lines = ["4\n5\n", "6\n7\n"]
        pygtail = Pygtail(self.logfile.name)
        pygtail.read()
        self.append(new_lines[0])
        os.rename(self.logfile.name, "%s-20160616" % self.logfile.name)
        self.append(new_lines[1])
        pygtail = Pygtail(self.logfile.name)
        self.assertEqual(pygtail.read(), ''.join(new_lines))

    def test_logrotate_with_dateext_without_delaycompress(self):
        new_lines = ["4\n5\n", "6\n7\n"]
        pygtail = Pygtail(self.logfile.name)
        pygtail.read()
        self.append(new_lines[0])

        # put content to gzip file
        gzip_handle = gzip.open("%s-20160616.gz" % self.logfile.name, 'wb')
        with open(self.logfile.name, 'rb') as logfile:
            gzip_handle.write(logfile.read())
        gzip_handle.close()

        with open(self.logfile.name, 'w'):
            # truncate file
            pass

        self.append(new_lines[1])
        pygtail = Pygtail(self.logfile.name)
        self.assertEqual(pygtail.read(), ''.join(new_lines))

    def test_logrotate_with_dateext2_with_delaycompress(self):
        new_lines = ["4\n5\n", "6\n7\n"]
        pygtail = Pygtail(self.logfile.name)
        pygtail.read()
        self.append(new_lines[0])
        os.rename(self.logfile.name, "%s-20160616-1466093571" % self.logfile.name)
        self.append(new_lines[1])
        pygtail = Pygtail(self.logfile.name)
        self.assertEqual(pygtail.read(), ''.join(new_lines))

    def test_logrotate_with_dateext2_without_delaycompress(self):
        new_lines = ["4\n5\n", "6\n7\n"]
        pygtail = Pygtail(self.logfile.name)
        pygtail.read()
        self.append(new_lines[0])

        # put content to gzip file
        gzip_handle = gzip.open("%s-20160616-1466093571.gz" % self.logfile.name, 'wb')
        with open(self.logfile.name, 'rb') as logfile:
            gzip_handle.write(logfile.read())
        gzip_handle.close()

        with open(self.logfile.name, 'w'):
            # truncate file
            pass

        self.append(new_lines[1])
        pygtail = Pygtail(self.logfile.name)
        self.assertEqual(pygtail.read(), ''.join(new_lines))

    def test_timed_rotating_file_handler(self):
        new_lines = ["4\n5\n", "6\n7\n"]
        pygtail = Pygtail(self.logfile.name)
        pygtail.read()
        self.append(new_lines[0])
        os.rename(self.logfile.name, "%s.2016-06-16" % self.logfile.name)
        self.append(new_lines[1])
        pygtail = Pygtail(self.logfile.name)
        self.assertEqual(pygtail.read(), ''.join(new_lines))

    def test_custom_rotating_file_handler_with_prepend(self):
        new_lines = ["4\n5\n", "6\n7\n"]
        pygtail = Pygtail(self.logfile.name)
        pygtail.read()
        self.append(new_lines[0])
        file_dir, rel_filename = os.path.split(self.logfile.name)
        os.rename(self.logfile.name, os.path.join(file_dir, "custom_log_pattern.%s" % rel_filename))
        self.append(new_lines[1])
        pygtail = Pygtail(self.logfile.name, log_patterns=["custom_log_pattern.%s"])
        self.assertEqual(pygtail.read(), ''.join(new_lines))

    def test_copytruncate_off_smaller(self):
        self.test_readlines()
        self.copytruncate()
        new_lines = "4\n5\n"
        self.append(new_lines)

        sys.stderr = captured = io.BytesIO() if PY2 else io.StringIO()
        pygtail = Pygtail(self.logfile.name, copytruncate=False)
        captured_value = captured.getvalue()
        sys.stderr = sys.__stderr__

        assert_class = self.assertRegex if sys.version_info >= (3, 1) else self.assertRegexpMatches
        assert_class(captured_value, r".*?\bWARN\b.*?\bshrank\b.*")
        self.assertEqual(pygtail.read(), None)

    def test_copytruncate_on_smaller(self):
        self.test_readlines()
        self.copytruncate()
        new_lines = "4\n5\n"
        self.append(new_lines)
        pygtail = Pygtail(self.logfile.name, copytruncate=True)
        self.assertEqual(pygtail.read(), new_lines)

    def _test_copytruncate_larger(self, onoff):
        self.test_readlines()
        self.copytruncate()
        self.append(self.test_str)
        new_lines = "4\n5\n"
        self.append(new_lines)
        pygtail = Pygtail(self.logfile.name, copytruncate=onoff)
        self.assertEqual(pygtail.read(), new_lines)

    def test_copytruncate_larger_off(self):
        self._test_copytruncate_larger(False)

    def test_copytruncate_larger_on(self):
        self._test_copytruncate_larger(True)

    def test_offset_file(self):
        pygtail = Pygtail(self.logfile.name, paranoid=True)

        log_inode = os.stat(self.logfile.name).st_ino

        next(pygtail)
        with open(self.logfile.name + '.offset', 'r') as f:
            inode, offset = int(next(f)), int(next(f))
        self.assertEqual(inode, log_inode)
        self.assertEqual(offset, 2)

        next(pygtail)
        with open(self.logfile.name + '.offset', 'r') as f:
            inode, offset = int(next(f)), int(next(f))
        self.assertEqual(inode, log_inode)
        self.assertEqual(offset, 4)

        next(pygtail)
        with open(self.logfile.name + '.offset', 'r') as f:
            inode, offset = int(next(f)), int(next(f))
        self.assertEqual(inode, log_inode)
        self.assertEqual(offset, 6)

    def test_on_update_with_paranoid(self):
        updates = [0]

        def record_update():
            updates[0] += 1

        pygtail = Pygtail(self.logfile.name, paranoid=True,
                          on_update=record_update)

        self.assertEqual(updates[0], 0)
        next(pygtail)
        self.assertEqual(updates[0], 1)
        next(pygtail)
        self.assertEqual(updates[0], 2)
        next(pygtail)
        self.assertEqual(updates[0], 3)

    def test_on_update_without_paranoid(self):
        updates = [0]

        def record_update():
            updates[0] += 1

        pygtail = Pygtail(self.logfile.name, on_update=record_update)

        self.assertEqual(updates[0], 0)
        for line in pygtail:
            self.assertEqual(updates[0], 0)
        self.assertEqual(updates[0], 1)

    def test_every_n(self):
        updates = [0]
        # We save before returning the second line.
        # We save at the end of the file with all 3 recorded.
        expected = [1, 3]
        previous_lines = 0

        def record_update():
            self.assertEqual(previous_lines, expected[updates[0]])
            updates[0] += 1

        pygtail = Pygtail(self.logfile.name, every_n=2, on_update=record_update)

        self.assertEqual(updates[0], 0)
        for line in pygtail:
            previous_lines += 1

    def test_renamecreate(self):
        """
        Tests "renamecreate" semantics where the currently processed file gets renamed and the
        original file gets recreated. This is the behavior of certain logfile rollers such as
        TimeBasedRollingPolicy in Java's Logback library.
        """
        new_lines = ["4\n5\n", "6\n7\n"]
        pygtail = Pygtail(self.logfile.name)
        pygtail.read()
        os.rename(self.logfile.name, "%s.2018-03-10" % self.logfile.name)
        # append will recreate the original log file
        self.append(new_lines[0])
        self.append(new_lines[1])
        self.assertEqual(pygtail.read(), ''.join(new_lines))

    def test_renamecreate_unknown_rotated_name(self):
        """
        Tests "renamecreate" semantics where the currently processed file gets renamed and the
        original file gets recreated. Rolled file has unknown name to pygtail. logrotate from
        Linux has this behaviour when rotating into separate directory.
        """
        new_lines = ["4\n5\n", "6\n7\n"]
        pygtail = Pygtail(self.logfile.name)
        pygtail.read()
        os.rename(self.logfile.name, "%s.unknown-name" % self.logfile.name)
        # append will recreate the original log file
        self.append(new_lines[0])
        self.append(new_lines[1])
        # reopen using Pytgail
        sys.stderr = captured = io.BytesIO() if PY2 else io.StringIO()
        pygtail = Pygtail(self.logfile.name)
        captured_value = captured.getvalue()
        sys.stderr = sys.__stderr__
        assert_class = self.assertRegex if sys.version_info >= (3, 1) else self.assertRegexpMatches
        assert_class(captured_value, r".*?\bWARN\b.*?\bResetting\b.*")
        self.assertEqual(pygtail.read(), ''.join(new_lines))


    def test_full_lines(self):
        """
        Tests lines are logged only when they have a new line at the end. This is useful to ensure that log lines
        aren't unintentionally split up.
        """
        pygtail = Pygtail(self.logfile.name, full_lines=True)
        new_lines = "4\n5,"
        last_line = "5.5\n6\n"

        self.append(new_lines)
        pygtail.read()
        self.append(last_line)
        self.assertEqual(pygtail.read(), "5,5.5\n6\n")


    def test_save_on_end(self):
        """
        Test save offset is not automatically saved once the end of the file is reached
        """
        updates = [0]

        def record_update():
            updates[0] += 1

        pygtail = Pygtail(self.logfile.name, save_on_end=False, on_update=record_update)

        self.assertEqual(updates[0], 0)
        for line in pygtail:
            self.assertEqual(updates[0], 0)
        self.assertEqual(updates[0], 0)
        pygtail.update_offset_file()
        self.assertEqual(updates[0], 1)


    def test_iterator_with_offsets(self):
        """
        Test save offset is not automatically saved once the end of the file is reached
        """
        updates = [0]

        def record_update():
            updates[0] += 1

        pygtail = Pygtail(self.logfile.name, save_on_end=False, on_update=record_update, copytruncate=False,)

        self.assertEqual(updates[0], 0)
        lines, offsets = list(zip(*pygtail.with_offsets()))
        self.assertEqual(len(lines), 3)
        self.assertEqual(updates[0], 0)
        pygtail.write_offset_to_file(offsets[1])
        self.assertEqual(updates[0], 1)

        for i in range(len(offsets)-1):
            self.assertLess(offsets[i], offsets[i+1])

        pygtail = Pygtail(self.logfile.name, save_on_end=False, on_update=record_update, copytruncate=False,)
        lines_new, offsets_new = list(zip(*pygtail.with_offsets()))
        self.assertEqual(len(lines_new), 1)
        self.assertEqual(updates[0], 1)
        self.assertEqual(offsets_new, offsets[2:])

    def test_offset_comparisons(self):
        """Test comparison operators of the Offset dataclass"""
        pygtail = Pygtail(self.logfile.name)
        _, offsets = list(zip(*pygtail.with_offsets()))
        for i in range(len(offsets)-1, ):
            self.assertLess(offsets[i], offsets[i+1])
            self.assertLessEqual(offsets[i], offsets[i+1])
            self.assertLessEqual(offsets[i], offsets[i])
            self.assertGreater(offsets[i+1], offsets[i])
            self.assertGreaterEqual(offsets[i], offsets[i])
            self.assertGreaterEqual(offsets[i+1], offsets[i])

def main():
    unittest.main(buffer=True)


if __name__ == "__main__":
    main()


================================================
FILE: pypi.txt
================================================
To update the version on PyPi:

1. Update __version__ in pygtail/core.py
2. Update debian/changelog
3. rm dist/*
4. python3 setup.py sdist bdist_wheel
5. twine --repository pygtail upload dist/* # if ~/.pypirc is setup, otherwise username: __token__, password: <pygtail API token>


================================================
FILE: setup.py
================================================
import os
from setuptools import setup

from pygtail import __version__


def main():
    cwd = os.path.dirname(os.path.abspath(__file__))
    path = os.path.join(cwd, 'README.txt')
    readme = open(path, 'r').read()

    setup(
        name = 'pygtail',
        version = __version__,
        description = 'Reads log file lines that have not been read.',
        license = 'GPL v2',
        author = 'Brad Greenlee',
        author_email = 'brad@footle.org',
        keywords = ['logging', 'tail', 'logtail2'],
        url = 'http://github.com/bgreenlee/pygtail',
        packages = ['pygtail'],
        entry_points = {
            'console_scripts': ['pygtail=pygtail.core:main']
            },
        test_suite='pygtail.test',
        classifiers = [
            "Development Status :: 4 - Beta",
            "Intended Audience :: Developers",
            "License :: OSI Approved :: GNU General Public License (GPL)",
            "Operating System :: MacOS :: MacOS X",
            "Operating System :: POSIX :: Linux",
            "Operating System :: Unix",
            "Programming Language :: Python",
            "Programming Language :: Python :: 2.6",
            "Programming Language :: Python :: 2.7",
            "Programming Language :: Python :: 3",
            "Topic :: Software Development :: Libraries :: Python Modules",
            "Topic :: System :: Logging"
            ],
        long_description = readme
        )


if __name__ == '__main__':
    main()


================================================
FILE: tox.ini
================================================
# Tox (http://codespeak.net/~hpk/tox/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.

[tox]
minversion=1.8.0
envlist = py{26,27,36,37,38,39,310,311}, pypy

[testenv]
commands = python setup.py test
deps =
	py26: unittest2
Download .txt
gitextract_bk990au9/

├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── README.txt
├── debian/
│   ├── changelog
│   ├── compat
│   ├── control
│   ├── copyright
│   ├── rules
│   └── source/
│       └── format
├── makedoc.sh
├── pygtail/
│   ├── __init__.py
│   ├── core.py
│   └── test/
│       ├── __init__.py
│       └── test_pygtail.py
├── pypi.txt
├── setup.py
└── tox.ini
Download .txt
SYMBOL INDEX (67 symbols across 3 files)

FILE: pygtail/core.py
  function force_text (line 46) | def force_text(s, encoding='utf-8', errors='strict'):
  class Offset (line 52) | class Offset:
    method __init__ (line 55) | def __init__(self, counter, inode, offset):
    method __eq__ (line 60) | def __eq__(self, other):
    method __lt__ (line 63) | def __lt__(self, other):
    method __le__ (line 68) | def __le__(self, other):
    method __gt__ (line 71) | def __gt__(self, other):
    method __ge__ (line 74) | def __ge__(self, other):
    method __repr__ (line 77) | def __repr__(self):
  class PygtailIteratorWithOffsets (line 85) | class PygtailIteratorWithOffsets:
    method __init__ (line 86) | def __init__(self, pygtail):
    method __next__ (line 89) | def __next__(self):
    method __iter__ (line 92) | def __iter__(self):
    method next (line 95) | def next(self):
  class Pygtail (line 104) | class Pygtail(object):
    method __init__ (line 120) | def __init__(self, filename, offset_file=None, paranoid=False, copytru...
    method __del__ (line 160) | def __del__(self):
    method __iter__ (line 164) | def __iter__(self):
    method next (line 167) | def next(self):
    method with_offsets (line 200) | def with_offsets(self):
    method __next__ (line 204) | def __next__(self):
    method readlines (line 208) | def readlines(self):
    method read (line 214) | def read(self):
    method _is_closed (line 227) | def _is_closed(self):
    method _filehandle (line 239) | def _filehandle(self):
    method update_offset_file (line 260) | def update_offset_file(self):
    method write_offset_to_file (line 275) | def write_offset_to_file(self, offset):
    method _determine_rotated_logfile (line 283) | def _determine_rotated_logfile(self):
    method _check_rotated_filename_candidates (line 306) | def _check_rotated_filename_candidates(self):
    method _is_new_file (line 355) | def _is_new_file(self):
    method _get_next_line (line 361) | def _get_next_line(self):
  function main (line 374) | def main():

FILE: pygtail/test/test_pygtail.py
  class PygtailTest (line 20) | class PygtailTest(unittest.TestCase):
    method setUp (line 25) | def setUp(self):
    method append (line 32) | def append(self, str):
    method copytruncate (line 38) | def copytruncate(self):
    method tearDown (line 43) | def tearDown(self):
    method test_read (line 49) | def test_read(self):
    method test_readlines (line 53) | def test_readlines(self):
    method test_subsequent_read_with_no_new_data (line 57) | def test_subsequent_read_with_no_new_data(self):
    method test_subsequent_read_with_new_data (line 62) | def test_subsequent_read_with_new_data(self):
    method test_read_from_the_file_end (line 70) | def test_read_from_the_file_end(self):
    method test_logrotate_without_delay_compress (line 78) | def test_logrotate_without_delay_compress(self):
    method test_logrotate_with_delay_compress (line 98) | def test_logrotate_with_delay_compress(self):
    method test_logrotate_with_dateext_with_delaycompress (line 108) | def test_logrotate_with_dateext_with_delaycompress(self):
    method test_logrotate_with_dateext_without_delaycompress (line 118) | def test_logrotate_with_dateext_without_delaycompress(self):
    method test_logrotate_with_dateext2_with_delaycompress (line 138) | def test_logrotate_with_dateext2_with_delaycompress(self):
    method test_logrotate_with_dateext2_without_delaycompress (line 148) | def test_logrotate_with_dateext2_without_delaycompress(self):
    method test_timed_rotating_file_handler (line 168) | def test_timed_rotating_file_handler(self):
    method test_custom_rotating_file_handler_with_prepend (line 178) | def test_custom_rotating_file_handler_with_prepend(self):
    method test_copytruncate_off_smaller (line 189) | def test_copytruncate_off_smaller(self):
    method test_copytruncate_on_smaller (line 204) | def test_copytruncate_on_smaller(self):
    method _test_copytruncate_larger (line 212) | def _test_copytruncate_larger(self, onoff):
    method test_copytruncate_larger_off (line 221) | def test_copytruncate_larger_off(self):
    method test_copytruncate_larger_on (line 224) | def test_copytruncate_larger_on(self):
    method test_offset_file (line 227) | def test_offset_file(self):
    method test_on_update_with_paranoid (line 250) | def test_on_update_with_paranoid(self):
    method test_on_update_without_paranoid (line 267) | def test_on_update_without_paranoid(self):
    method test_every_n (line 280) | def test_every_n(self):
    method test_renamecreate (line 297) | def test_renamecreate(self):
    method test_renamecreate_unknown_rotated_name (line 312) | def test_renamecreate_unknown_rotated_name(self):
    method test_full_lines (line 335) | def test_full_lines(self):
    method test_save_on_end (line 350) | def test_save_on_end(self):
    method test_iterator_with_offsets (line 369) | def test_iterator_with_offsets(self):
    method test_offset_comparisons (line 396) | def test_offset_comparisons(self):
  function main (line 408) | def main():

FILE: setup.py
  function main (line 7) | def main():
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (66K chars).
[
  {
    "path": ".gitignore",
    "chars": 166,
    "preview": "*.pyc\n*.egg-info\n.coverage\nbuild\ndist\n.tox\ndebian/.debhelper/**\ndebian/files\ndebian/pygtail/**\ndebian/tmp/**\ndebian/pyth"
  },
  {
    "path": ".travis.yml",
    "chars": 121,
    "preview": "language: python\npython:\n  - 2.7\n  - pypy\n  - 3.6\n  - 3.7\n  - 3.8\n  - 3.9\n  - 3.10\n  - 3.11\nscript: python setup.py test"
  },
  {
    "path": "LICENSE",
    "chars": 18091,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Fr"
  },
  {
    "path": "README.md",
    "chars": 2499,
    "preview": "pygtail\n=======\n\nA python \"port\" of [logcheck's logtail2](http://logcheck.org).\n\nPygtail reads log file lines that have "
  },
  {
    "path": "README.txt",
    "chars": 2582,
    "preview": "pygtail\n=======\n\nA python \"port\" of `logcheck's logtail2 <http://logcheck.org>`__.\n\nPygtail reads log file lines that ha"
  },
  {
    "path": "debian/changelog",
    "chars": 4455,
    "preview": "pygtail (0.14.0) lucid; urgency=low\n\n  * Added with_offsets iterator and write_offset_to_file method. (thanks @pajowu!)\n"
  },
  {
    "path": "debian/compat",
    "chars": 2,
    "preview": "7\n"
  },
  {
    "path": "debian/control",
    "chars": 934,
    "preview": "Source: pygtail\nSection: python\nPriority: optional\nMaintainer: Brad Greenlee <brad@footle.org>\nBuild-Depends: debhelper "
  },
  {
    "path": "debian/copyright",
    "chars": 309,
    "preview": "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\nUpstream-Name: pygtail\nUpstream-Contact: Brad"
  },
  {
    "path": "debian/rules",
    "chars": 310,
    "preview": "#!/usr/bin/make -f\n\n#export DH_VERBOSE = 1\nexport PYBUILD_NAME = pygtail\n\n%:\n\tdh $@ --with python2,python3 --buildsystem"
  },
  {
    "path": "debian/source/format",
    "chars": 4,
    "preview": "1.0\n"
  },
  {
    "path": "makedoc.sh",
    "chars": 63,
    "preview": "#!/bin/bash\n\npandoc -f markdown -t rst -o README.txt README.md\n"
  },
  {
    "path": "pygtail/__init__.py",
    "chars": 70,
    "preview": "from pygtail.core import __version__\nfrom pygtail.core import Pygtail\n"
  },
  {
    "path": "pygtail/core.py",
    "chars": 16248,
    "preview": "#!/usr/bin/python -tt\n# -*- coding: utf-8 -*-\n\n# pygtail - a python \"port\" of logtail2\n# Copyright (C) 2011 Brad Greenle"
  },
  {
    "path": "pygtail/test/__init__.py",
    "chars": 1,
    "preview": "#"
  },
  {
    "path": "pygtail/test/test_pygtail.py",
    "chars": 14591,
    "preview": "import os\nimport sys\n\ntry:\n    # python 2.6\n    import unittest2 as unittest\nexcept ImportError:\n    import unittest\nimp"
  },
  {
    "path": "pypi.txt",
    "chars": 281,
    "preview": "To update the version on PyPi:\n\n1. Update __version__ in pygtail/core.py\n2. Update debian/changelog\n3. rm dist/*\n4. pyth"
  },
  {
    "path": "setup.py",
    "chars": 1488,
    "preview": "import os\nfrom setuptools import setup\n\nfrom pygtail import __version__\n\n\ndef main():\n    cwd = os.path.dirname(os.path."
  },
  {
    "path": "tox.ini",
    "chars": 386,
    "preview": "# Tox (http://codespeak.net/~hpk/tox/) is a tool for running tests\n# in multiple virtualenvs. This configuration file wi"
  }
]

About this extraction

This page contains the full source code of the bgreenlee/pygtail GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (61.1 KB), approximately 15.5k tokens, and a symbol index with 67 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!