From 33f3c6fc4ca8810cee4c517594be725a26eec70c Mon Sep 17 00:00:00 2001 From: Dimitri Staessens Date: Sun, 26 Jul 2026 13:49:26 +0200 Subject: pyouroboros: Use consistent format docstrings This is basically a clean up pass to make sure all docstrings are aligned. The copyright banners are replaced by SPDX statements. Also did a general clean up on the comments. Passes pylint. No structural changes. Signed-off-by: Dimitri Staessens --- AUTHORS | 1 + LICENSE | 344 ----------------------------------------- LICENSE-BSD | 30 ++++ LICENSE-LGPL | 344 +++++++++++++++++++++++++++++++++++++++++ README.md | 8 +- examples/oecho.py | 61 +++----- examples/oecho_set.py | 63 +++----- ffi/fccntl_wrap.h | 4 +- ffi/pyouroboros_build_dev.py | 29 ++-- ffi/pyouroboros_build_irm.py | 29 ++-- ouroboros/__init__.py | 36 +---- ouroboros/_qosspec.py | 49 +++--- ouroboros/_timespec.py | 51 +++---- ouroboros/cli.py | 302 ++++++++++++++---------------------- ouroboros/dev.py | 356 +++++++++++++++++++++++++++++++++---------- ouroboros/errors.py | 222 ++++++++++++++++----------- ouroboros/event.py | 114 +++++++++----- ouroboros/irm.py | 292 +++++++++++++++++++++++------------ ouroboros/qos.py | 37 ++--- pyproject.toml | 15 +- setup.py | 24 ++- 21 files changed, 1323 insertions(+), 1088 deletions(-) create mode 100644 AUTHORS delete mode 100644 LICENSE create mode 100644 LICENSE-BSD create mode 100644 LICENSE-LGPL mode change 100755 => 100644 examples/oecho.py mode change 100755 => 100644 examples/oecho_set.py diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..4ad80cb --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Dimitri Staessens diff --git a/LICENSE b/LICENSE deleted file mode 100644 index c321e52..0000000 --- a/LICENSE +++ /dev/null @@ -1,344 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called "this License"). -Each licensee is addressed as "you". - - A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - - The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - - "Source code" for a work means the preferred form of the work for -making modifications to it. For a library, 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 library. - - Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - - 1. You may copy and distribute verbatim copies of the Library's -complete 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 distribute a copy of this License along with the -Library. - - 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 Library or any portion -of it, thus forming a work based on the Library, 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) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices - stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no - charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a - table of data to be supplied by an application program that uses - the facility, other than as an argument passed when the facility - is invoked, then you must make a good faith effort to ensure that, - in the event an application does not supply such function or - table, the facility still operates, and performs whatever part of - its purpose remains meaningful. - - (For example, a function in a library to compute square roots has - a purpose that is entirely well-defined independent of the - application. Therefore, Subsection 2d requires that any - application-supplied function or table used by this function must - be optional: if the application does not supply it, the square - root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -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 Library, 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 Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - - Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - - This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - - 4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you 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. - - If distribution of 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 satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - - 5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - - However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - - When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - - If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - - Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - - 6. As an exception to the Sections above, you may also combine or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - - You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - - a) Accompany the work with the complete corresponding - machine-readable source code for the Library including whatever - changes were used in the work (which must be distributed under - Sections 1 and 2 above); and, if the work is an executable linked - with the Library, with the complete machine-readable "work that - uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified - executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the - Library will not necessarily be able to recompile the application - to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (1) uses at run time a - copy of the library already present on the user's computer system, - rather than copying library functions into the executable, and (2) - will operate properly with a modified version of the library, if - the user installs one, as long as the modified version is - interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at - least three years, to give the same user the materials - specified in Subsection 6a, above, for a charge no more - than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy - from a designated place, offer equivalent access to copy the above - specified materials from the same place. - - e) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. - - For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be 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. - - It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - - 7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities. This must be distributed under the terms of the - Sections above. - - b) Give prominent notice with the combined library of the fact - that part of it is a work based on the Library, and explaining - where to find the accompanying uncombined form of the same work. - - 8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library 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. - - 9. 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 Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - - 10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -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 with -this License. - - 11. 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 Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library 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 Library. - -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. - - 12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library 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. - - 13. The Free Software Foundation may publish revised and/or new -versions of the Lesser 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 Library -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 Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - - 14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -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 - - 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "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 -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. 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 LIBRARY 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 -LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - - END OF TERMS AND CONDITIONS diff --git a/LICENSE-BSD b/LICENSE-BSD new file mode 100644 index 0000000..258ef3b --- /dev/null +++ b/LICENSE-BSD @@ -0,0 +1,30 @@ +Copyright (C) 2016 - 2026, Dimitri Staessens + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following +disclaimer in the documentation and/or other materials provided +with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived +from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSE-LGPL b/LICENSE-LGPL new file mode 100644 index 0000000..c321e52 --- /dev/null +++ b/LICENSE-LGPL @@ -0,0 +1,344 @@ + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, 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 library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete 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 distribute a copy of this License along with the +Library. + + 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 Library or any portion +of it, thus forming a work based on the Library, 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) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +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 Library, 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 Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you 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. + + If distribution of 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 satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be 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. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library 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. + + 9. 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 Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +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 with +this License. + + 11. 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 Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library 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 Library. + +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. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library 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. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +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 Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +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 + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. 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 LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md index 5871d8c..1a55fc3 100644 --- a/README.md +++ b/README.md @@ -314,7 +314,7 @@ from ouroboros.irm import ( For most use cases, the higher-level [`ouroboros.cli`](#cli-helpers) wrappers are easier to work with — they mirror the `irm` CLI tool and -take IPCP names rather than pids. +are keyed by IPCP name. ### IPCP Management @@ -534,7 +534,7 @@ See the module docstring for the full CLI-command ↔ Python mapping. ## Examples -See the [`examples/`](examples/) folder for runnable client/server +See the [`examples/`](examples/) directory for runnable client/server demos. ## Versioning @@ -545,8 +545,8 @@ pyOuroboros uses `setuptools_scm` to derive its version from git tags. | Scope | Rule | |---|---| -| ouroboros (C) ↔ pyouroboros / rumba | Shared `major.minor` — pyouroboros requires at least the same ouroboros `major.minor` | -| pyouroboros ↔ rumba ↔ ouroboros-integration | Strict lockstep `major.minor.patch` — always released together | +| ouroboros (C) ↔ pyouroboros / rumba | Shared `major.minor`, C side at least equal | +| pyouroboros ↔ rumba ↔ ouroboros-integration | Lockstep `major.minor.patch`, released together | ## License diff --git a/examples/oecho.py b/examples/oecho.py old mode 100755 new mode 100644 index 4bf3e05..ae94103 --- a/examples/oecho.py +++ b/examples/oecho.py @@ -1,43 +1,13 @@ -#!/bin/python - -# Ouroboros - Copyright (C) 2016 - 2026 -# -# A simple echo application -# -# Dimitri Staessens -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: # -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens +# SPDX-License-Identifier: BSD-3-Clause # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# 3. Neither the name of the copyright holder nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, -# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, -# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED -# OF THE POSSIBILITY OF SUCH DAMAGE. - -"""A simple echo client/server example using Ouroboros flows.""" - -# Standalone demo; intentionally duplicates oecho_set.py. + +""" +A simple echo client/server example using Ouroboros flows. +""" + +# Standalone demo; duplicates oecho_set.py. # pylint: disable=duplicate-code from __future__ import annotations @@ -48,27 +18,38 @@ from ouroboros.dev import flow_accept, flow_alloc def client() -> None: - """Send a message and print the echo reply.""" + """ + Send a message and print the echo reply. + """ + with flow_alloc("oecho") as f: f.writeline("Hello, PyOuroboros!") print(f.readline()) def server() -> None: - """Accept flows and echo back received messages.""" + """ + Accept flows and echo back received messages. + """ + print("Starting the server.") + while True: with flow_accept() as f: print("New flow.") + line = f.readline() + print(f"Message from client is {line}") f.writeline(line) if __name__ == "__main__": parser = argparse.ArgumentParser(description='A simple echo client/server') + parser.add_argument('-l', '--listen', help='run as a server', action='store_true') + args = parser.parse_args() if args.listen: server() diff --git a/examples/oecho_set.py b/examples/oecho_set.py old mode 100755 new mode 100644 index 72265df..469fdb0 --- a/examples/oecho_set.py +++ b/examples/oecho_set.py @@ -1,43 +1,13 @@ -#!/bin/python - -# Ouroboros - Copyright (C) 2016 - 2026 -# -# A simple echo application -# -# Dimitri Staessens -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. # -# 2. Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. +# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens +# SPDX-License-Identifier: BSD-3-Clause # -# 3. Neither the name of the copyright holder nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, -# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, -# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED -# OF THE POSSIBILITY OF SUCH DAMAGE. - -"""A simple echo client/server example using Ouroboros flow event sets.""" - -# Standalone demo; intentionally duplicates oecho.py. + +""" +A simple echo client/server example using Ouroboros flow event sets. +""" + +# Standalone demo; duplicates oecho.py. # pylint: disable=duplicate-code from __future__ import annotations @@ -49,32 +19,45 @@ from ouroboros.event import FEventQueue, FEventType, FlowSet def client() -> None: - """Send a message and print the echo reply.""" + """ + Send a message and print the echo reply. + """ + with flow_alloc("oecho") as f: f.writeline("Hello, PyOuroboros!") print(f.readline()) def server() -> None: - """Accept flows and echo back received messages using flow events.""" + """ + Accept flows and echo back received messages using flow events. + """ + print("Starting the server.") + while True: with flow_accept() as f: print("New flow.") with FEventQueue() as fq, FlowSet([f]) as fs: fs.wait(fq) + f2, t = fq.next() + if t != FEventType.FLOW_PKT: continue + line = f2.readline() + print(f"Message from client is {line}") f2.writeline(line) if __name__ == "__main__": parser = argparse.ArgumentParser(description='A simple echo client/server') + parser.add_argument('-l', '--listen', help='run as a server', action='store_true') + args = parser.parse_args() if args.listen: server() diff --git a/ffi/fccntl_wrap.h b/ffi/fccntl_wrap.h index 0555e24..7cfc902 100644 --- a/ffi/fccntl_wrap.h +++ b/ffi/fccntl_wrap.h @@ -71,7 +71,7 @@ int flow_get_flags(int fd) { uint32_t flags; - if (fccntl(fd, FLOWGFLAGS, &flags)) + if (fccntl(fd, FLOWGFLAGS, &flags) < 0) return -EPERM; return (int) flags; @@ -86,7 +86,7 @@ int flow_get_frct_flags(int fd) { uint16_t flags; - if (fccntl(fd, FRCTGFLAGS, &flags)) + if (fccntl(fd, FRCTGFLAGS, &flags) < 0) return -EPERM; return (int) flags; diff --git a/ffi/pyouroboros_build_dev.py b/ffi/pyouroboros_build_dev.py index d32d577..fc7a05a 100644 --- a/ffi/pyouroboros_build_dev.py +++ b/ffi/pyouroboros_build_dev.py @@ -1,29 +1,18 @@ # -# Ouroboros - Copyright (C) 2016 - 2026 -# -# Python API for Ouroboros -# -# Dimitri Staessens -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public License -# version 2.1 as published by the Free Software Foundation. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., http://www.fsf.org/about/contact/. +# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens +# SPDX-License-Identifier: LGPL-2.1-only # -"""CFFI build script for the ouroboros-dev library.""" +""" +CFFI build script for the ouroboros-dev library. +""" -# qosspec_t / timespec cdef is duplicated in the irm builder per CFFI module. +# qosspec_t / timespec cdef is duplicated in the irm builder, one copy +# per CFFI module. # pylint: disable=duplicate-code +from __future__ import annotations + from cffi import FFI ffibuilder: FFI = FFI() diff --git a/ffi/pyouroboros_build_irm.py b/ffi/pyouroboros_build_irm.py index 1773788..a9f4bc9 100644 --- a/ffi/pyouroboros_build_irm.py +++ b/ffi/pyouroboros_build_irm.py @@ -1,29 +1,18 @@ # -# Ouroboros - Copyright (C) 2016 - 2026 -# -# Python API for Ouroboros -# -# Dimitri Staessens -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public License -# version 2.1 as published by the Free Software Foundation. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., http://www.fsf.org/about/contact/. +# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens +# SPDX-License-Identifier: LGPL-2.1-only # -"""CFFI build script for the ouroboros-irm library.""" +""" +CFFI build script for the ouroboros-irm library. +""" -# qosspec_t / timespec cdef is duplicated in the dev builder per CFFI module. +# qosspec_t / timespec cdef is duplicated in the dev builder, one copy +# per CFFI module. # pylint: disable=duplicate-code +from __future__ import annotations + from cffi import FFI ffibuilder: FFI = FFI() diff --git a/ouroboros/__init__.py b/ouroboros/__init__.py index a4de9bf..cbca95d 100644 --- a/ouroboros/__init__.py +++ b/ouroboros/__init__.py @@ -1,35 +1,15 @@ # -# Ouroboros - Copyright (C) 2016 - 2026 -# -# Python API for Ouroboros -# -# Dimitri Staessens -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public License -# version 2.1 as published by the Free Software Foundation. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., http://www.fsf.org/about/contact/. +# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens +# SPDX-License-Identifier: LGPL-2.1-only # -"""Public Python API for Ouroboros. +""" +Public Python API for Ouroboros. -The package surface only re-exports pure-Python helpers (errors and -QoS). Applications import the load-bearing parts of the API from -their submodule explicitly: flows from :mod:`ouroboros.dev`, the -event loop from :mod:`ouroboros.event`, and the control plane from -:mod:`ouroboros.irm` (or :mod:`ouroboros.cli`). Submodule paths -make the caller's intent explicit and keep the package importable -in environments that have no business loading the libouroboros CFFI -extensions (see ``CLAUDE.md`` for the underlying init-on-load -behaviour that makes this contract load-bearing in practice). +The package surface re-exports the pure-Python helpers only: errors and +QoS. The rest of the API is imported from its submodule: flows from +:mod:`ouroboros.dev`, the event loop from :mod:`ouroboros.event`, the +control plane from :mod:`ouroboros.irm` (or :mod:`ouroboros.cli`). """ from __future__ import annotations diff --git a/ouroboros/_qosspec.py b/ouroboros/_qosspec.py index 6031c42..5c8b245 100644 --- a/ouroboros/_qosspec.py +++ b/ouroboros/_qosspec.py @@ -1,47 +1,35 @@ # -# Ouroboros - Copyright (C) 2016 - 2026 -# -# Internal helpers for translating QoSSpec <-> CFFI qosspec_t. -# -# Dimitri Staessens -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public License -# version 2.1 as published by the Free Software Foundation. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., http://www.fsf.org/about/contact/. +# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens +# SPDX-License-Identifier: LGPL-2.1-only # -"""Shared QoSSpec <-> qosspec_t conversion helpers. +""" +Shared QoSSpec <-> qosspec_t conversion helpers. -dev.py and irm.py each link against their own CFFI extension module -(``_ouroboros_dev_cffi`` and ``_ouroboros_irm_cffi``), so they each -have their own ``ffi`` instance and their own ``qosspec_t`` type. -CFFI types are not interchangeable across modules, so these helpers -take the caller's ``ffi`` instance as a parameter. +CFFI types are not interchangeable across extension modules, so the +caller passes its own ``ffi`` instance. """ from __future__ import annotations -from typing import Optional +from typing import TYPE_CHECKING, Any, Optional from ouroboros.qos import QoSSpec +if TYPE_CHECKING: + from cffi import FFI -def qos_to_qosspec(ffi, qos: Optional[QoSSpec]): - """Convert a :class:`QoSSpec` to a freshly-allocated ``qosspec_t *``. + +def qos_to_qosspec(ffi: FFI, qos: Optional[QoSSpec]) -> Any: + """ + Convert a :class:`QoSSpec` to a freshly-allocated ``qosspec_t *``. Returns ``ffi.NULL`` when *qos* is ``None``. """ + if qos is None: return ffi.NULL + return ffi.new("qosspec_t *", [qos.service, qos.delay, @@ -53,13 +41,16 @@ def qos_to_qosspec(ffi, qos: Optional[QoSSpec]): qos.timeout]) -def qosspec_to_qos(ffi, _qos) -> Optional[QoSSpec]: - """Convert a ``qosspec_t *`` back to a :class:`QoSSpec`. +def qosspec_to_qos(ffi: FFI, _qos: Any) -> Optional[QoSSpec]: + """ + Convert a ``qosspec_t *`` back to a :class:`QoSSpec`. Returns ``None`` when *_qos* is ``ffi.NULL``. """ + if _qos == ffi.NULL: return None + return QoSSpec(service=_qos.service, delay=_qos.delay, bandwidth=_qos.bandwidth, diff --git a/ouroboros/_timespec.py b/ouroboros/_timespec.py index 2fcf666..1656eb2 100644 --- a/ouroboros/_timespec.py +++ b/ouroboros/_timespec.py @@ -1,64 +1,59 @@ # -# Ouroboros - Copyright (C) 2016 - 2026 -# -# Internal helpers for translating float seconds <-> struct timespec. -# -# Dimitri Staessens -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public License -# version 2.1 as published by the Free Software Foundation. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., http://www.fsf.org/about/contact/. +# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens +# SPDX-License-Identifier: LGPL-2.1-only # -"""Shared float-seconds <-> ``struct timespec *`` conversions. +""" +Shared float-seconds <-> ``struct timespec *`` conversions. -The dev and event modules both deal in seconds-as-float timeouts on -the Python side and pass ``struct timespec *`` to the C library. -The helpers take the caller's ``ffi`` instance as a parameter so the -returned CData is bound to the right CFFI module. +The caller passes its own ``ffi`` instance so the returned CData is +bound to the right CFFI extension module. """ from __future__ import annotations from math import modf -from typing import Optional +from typing import TYPE_CHECKING, Any, Optional + +if TYPE_CHECKING: + from cffi import FFI BILLION = 1000 * 1000 * 1000 -def fl_to_timespec(ffi, timeo: Optional[float]): - """Convert *timeo* (seconds, or None) to a ``struct timespec *``. +def fl_to_timespec(ffi: FFI, timeo: Optional[float]) -> Any: + """ + Convert *timeo* (seconds, or None) to a ``struct timespec *``. *None* yields ``ffi.NULL`` (block forever). A non-positive *timeo* yields a zeroed timespec (async / poll). """ + if timeo is None: return ffi.NULL + if timeo <= 0: return ffi.new("struct timespec *", [0, 0]) + frac, whole = modf(timeo) _timeo = ffi.new("struct timespec *") _timeo.tv_sec = int(whole) _timeo.tv_nsec = int(frac * BILLION) + return _timeo -def timespec_to_fl(ffi, _timeo) -> Optional[float]: - """Convert a ``struct timespec *`` to seconds-as-float. +def timespec_to_fl(ffi: FFI, _timeo: Any) -> Optional[float]: + """ + Convert a ``struct timespec *`` to seconds-as-float. ``ffi.NULL`` yields *None* (block forever). """ + if _timeo == ffi.NULL: return None + if _timeo.tv_sec <= 0 and _timeo.tv_nsec == 0: return 0.0 + return _timeo.tv_sec + _timeo.tv_nsec / BILLION diff --git a/ouroboros/cli.py b/ouroboros/cli.py index 4b9ebb7..e887410 100644 --- a/ouroboros/cli.py +++ b/ouroboros/cli.py @@ -1,64 +1,13 @@ # -# Ouroboros - Copyright (C) 2016 - 2026 -# -# Python API for Ouroboros - CLI equivalents -# -# Dimitri Staessens -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public License -# version 2.1 as published by the Free Software Foundation. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., http://www.fsf.org/about/contact/. +# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens +# SPDX-License-Identifier: LGPL-2.1-only # -"""Higher-level wrappers that mirror the ``irm`` CLI tool behaviour. - -This is the Python counterpart of the C project's ``ouroboros/tools/irm`` -command-line tool. The ``irm`` CLI performs steps that the raw C -library API does not, such as resolving program names via ``realpath``, -looking up IPCP pids, and handling the ``autobind`` flag during -bootstrap/enroll. This module exposes those same patterns as a Python -API so callers do not need to re-implement them. - -Each wrapper corresponds to a specific ``irm`` sub-command: - -========================= ==================================== -Python CLI equivalent -========================= ==================================== -``create_ipcp`` ``irm ipcp create`` -``destroy_ipcp`` ``irm ipcp destroy`` -``bootstrap_ipcp`` ``irm ipcp bootstrap [autobind]`` -``enroll_ipcp`` ``irm ipcp enroll [autobind]`` -``connect_ipcp`` ``irm ipcp connect`` -``disconnect_ipcp`` ``irm ipcp disconnect`` -``list_ipcps`` ``irm ipcp list`` -``bind_program`` ``irm bind program`` -``bind_process`` ``irm bind process`` -``bind_ipcp`` ``irm bind ipcp`` -``unbind_program`` ``irm unbind program`` -``unbind_process`` ``irm unbind process`` -``unbind_ipcp`` ``irm unbind ipcp`` -``create_name`` ``irm name create`` -``destroy_name`` ``irm name destroy`` -``reg_name`` ``irm name register`` -``unreg_name`` ``irm name unregister`` -``list_names`` ``irm name list`` -``autoboot`` ``irm ipcp bootstrap autobind`` - (with implicit ``create``) -========================= ==================================== - -Usage:: - - from ouroboros.cli import create_ipcp, bootstrap_ipcp, enroll_ipcp - from ouroboros.cli import bind_program, autoboot +""" +Higher-level wrappers that mirror the ``irm`` CLI tool behaviour. + +Adds program path resolution, IPCP name-to-pid lookup and ``autobind`` +handling on top of the raw IRM API. """ from __future__ import annotations @@ -110,7 +59,7 @@ __all__ = [ "unbind_program", "unbind_process", "unbind_ipcp", "create_name", "destroy_name", "reg_name", "unreg_name", "list_names", "autoboot", - # Re-exported configuration types (convenience) + # Re-exported configuration types "DhtConfig", "DirConfig", "DtConfig", "EthConfig", "IpcpConfig", "IpcpInfo", "IpcpType", "LinkStateConfig", "NameInfo", "RoutingConfig", @@ -119,10 +68,14 @@ __all__ = [ def _pid_of(ipcp_name: str) -> int: - """Look up the pid of a running IPCP by its name.""" + """ + Look up the pid of a running IPCP by its name. + """ + for info in _irm_list_ipcps(): if info.name == ipcp_name: return info.pid + raise ValueError(f"No IPCP named {ipcp_name!r}") @@ -130,68 +83,83 @@ def _collect_pids(ipcp: Optional[str], ipcps: Optional[List[str]], layer: Optional[str], layers: Optional[List[str]]) -> Set[int]: - """Collect the set of pids matching the given name/layer filters.""" + """ + Collect the set of pids matching the given name/layer filters. + """ + ipcp_names: List[str] = [] + if ipcp is not None: ipcp_names.append(ipcp) + if ipcps is not None: ipcp_names.extend(ipcps) layer_names: List[str] = [] + if layer is not None: layer_names.append(layer) + if layers is not None: layer_names.extend(layers) pids: Set[int] = set() + if not ipcp_names and not layer_names: return pids all_ipcps = _irm_list_ipcps() + for ipcp_name in ipcp_names: for i in all_ipcps: if i.name == ipcp_name: pids.add(i.pid) break + for lyr in layer_names: for i in all_ipcps: if i.layer == lyr: pids.add(i.pid) + return pids def destroy_ipcp(name: str) -> None: - """Destroy an IPCP by name. - - Mirrors ``irm ipcp destroy name ``. - - Resolves the IPCP name to a pid, then destroys the IPCP. + """ + Destroy an IPCP by name. :param name: Name of the IPCP to destroy. :raises ValueError: If no IPCP with *name* exists. """ + _irm_destroy_ipcp(_pid_of(name)) def list_ipcps(name: Optional[str] = None, layer: Optional[str] = None, ipcp_type: Optional[IpcpType] = None) -> List[IpcpInfo]: - """List running IPCPs, optionally filtered. + """ + List running IPCPs, optionally filtered. - Mirrors ``irm ipcp list [name ] [type ] [layer ]``. + Filters are exact matches. :param name: Filter by IPCP name (exact match). :param layer: Filter by layer name (exact match). :param ipcp_type: Filter by IPCP type. :return: List of matching :class:`IpcpInfo` objects. """ + result = _irm_list_ipcps() + if name is not None: result = [i for i in result if i.name == name] + if layer is not None: result = [i for i in result if i.layer == layer] + if ipcp_type is not None: result = [i for i in result if i.type == ipcp_type] + return result @@ -200,21 +168,10 @@ def reg_name(name: str, ipcps: Optional[List[str]] = None, layer: Optional[str] = None, layers: Optional[List[str]] = None) -> None: - """Register a name with IPCP(s), creating it first if needed. - - Mirrors ``irm name register ipcp [ipcp ...] - layer [layer ...]``. - - The C CLI tool resolves IPCP names and layer names to pids, - checks whether the name already exists and calls - ``irm_create_name`` before ``irm_reg_name`` for each IPCP. - - The function accepts flexible input: + """ + Register a name with IPCP(s), creating it first if needed. - - A single *ipcp* name or list of *ipcps* names. - - A single *layer* name or list of *layers* names (registers - with every IPCP in each of those layers). - - Any combination of the above. + Registers with every IPCP in *layer* and *layers*. :param name: The name to register. :param ipcp: Single IPCP name to register with. @@ -222,6 +179,7 @@ def reg_name(name: str, :param layer: Single layer name to register with. :param layers: List of layer names to register with. """ + existing = {n.name for n in _irm_list_names()} if name not in existing: _irm_create_name(NameInfo(name=name)) @@ -234,129 +192,120 @@ def bind_program(prog: str, name: str, opts: int = 0, argv: Optional[List[str]] = None) -> None: - """Bind a program to a name, resolving bare names to full paths. - - Mirrors ``irm bind program name ``. - - The ``irm bind program`` CLI tool calls ``realpath()`` on *prog* - before passing it to the library. The raw C function - ``irm_bind_program`` contains a ``check_prog_path`` helper that - corrupts the ``PATH`` environment variable (writes NUL over ``:`` - separators) when given a bare program name. Only the first such - call would succeed in a long-running process. - - This wrapper resolves *prog* via ``shutil.which()`` before calling - the library, avoiding the bug entirely. + """ + Bind a program to a name. :param prog: Program name or path. Bare names (without ``/``) are - resolved on ``PATH`` via ``shutil.which()``. + resolved on ``PATH``. :param name: Name to bind to. :param opts: Bind options (e.g. ``BIND_AUTO``). - :param argv: Arguments to pass when the program is auto-started. - :raises BindError: If the program cannot be found or the bind - call fails. + :param argv: Arguments passed when the program is auto-started. + :raises BindError: If the program is not found on ``PATH``. """ + if '/' not in prog: resolved = shutil.which(prog) if resolved is None: raise BindError(f"Program {prog!r} not found on PATH") + prog = resolved + _irm_bind_program(prog, name, opts=opts, argv=argv) def unbind_program(prog: str, name: str) -> None: - """Unbind a program from a name. - - Mirrors ``irm unbind program name ``. + """ + Unbind a program from a name. :param prog: Path to the program. :param name: Name to unbind from. """ + _irm_unbind_program(prog, name) def bind_process(pid: int, name: str) -> None: - """Bind a running process to a name. - - Mirrors ``irm bind process name ``. + """ + Bind a running process to a name. :param pid: PID of the process. :param name: Name to bind to. """ + _irm_bind_process(pid, name) def unbind_process(pid: int, name: str) -> None: - """Unbind a process from a name. - - Mirrors ``irm unbind process name ``. + """ + Unbind a process from a name. :param pid: PID of the process. :param name: Name to unbind from. """ + _irm_unbind_process(pid, name) def bind_ipcp(ipcp: str, name: str) -> None: - """Bind an IPCP to a name. - - Mirrors ``irm bind ipcp name ``. - - Resolves the IPCP name to a pid, then calls :func:`bind_process`. + """ + Bind an IPCP to a name. :param ipcp: IPCP instance name. :param name: Name to bind to. :raises ValueError: If no IPCP with *ipcp* exists. """ + _irm_bind_process(_pid_of(ipcp), name) def unbind_ipcp(ipcp: str, name: str) -> None: - """Unbind an IPCP from a name. - - Mirrors ``irm unbind ipcp name ``. - - Resolves the IPCP name to a pid, then calls :func:`unbind_process`. + """ + Unbind an IPCP from a name. :param ipcp: IPCP instance name. :param name: Name to unbind from. :raises ValueError: If no IPCP with *ipcp* exists. """ + _irm_unbind_process(_pid_of(ipcp), name) def create_name(name: str, pol_lb: Optional[int] = None, info: Optional[NameInfo] = None) -> None: - """Create a registered name. - - Mirrors ``irm name create [lb ]``. + """ + Create a registered name. :param name: The name to create. - :param pol_lb: Load-balance policy (optional). - :param info: Full :class:`NameInfo` (overrides *name* / *pol_lb* - if given). + :param pol_lb: Load-balance policy. + :param info: Full :class:`NameInfo`; overrides *name* and + *pol_lb*. """ + if info is not None: _irm_create_name(info) else: ni = NameInfo(name=name) + if pol_lb is not None: ni.pol_lb = pol_lb + _irm_create_name(ni) def list_names(name: Optional[str] = None) -> List[NameInfo]: - """List all registered names, optionally filtered. - - Mirrors ``irm name list []``. + """ + List all registered names, optionally filtered on exact name. :param name: Filter by name (exact match). :return: List of :class:`NameInfo` objects. """ + result = _irm_list_names() + if name is not None: result = [n for n in result if n.name == name] + return result @@ -365,12 +314,10 @@ def unreg_name(name: str, ipcps: Optional[List[str]] = None, layer: Optional[str] = None, layers: Optional[List[str]] = None) -> None: - """Unregister a name from IPCP(s). - - Mirrors ``irm name unregister ipcp [ipcp ...] - layer [layer ...]``. + """ + Unregister a name from IPCP(s). - Accepts the same flexible input as :func:`reg_name`. + Unregisters from every IPCP in *layer* and *layers*. :param name: The name to unregister. :param ipcp: Single IPCP name to unregister from. @@ -378,6 +325,7 @@ def unreg_name(name: str, :param layer: Single layer name to unregister from. :param layers: List of layer names to unregister from. """ + for pid in _collect_pids(ipcp, ipcps, layer, layers): _irm_unreg_name(name, pid) @@ -385,28 +333,21 @@ def unreg_name(name: str, def bootstrap_ipcp(name: str, conf: IpcpConfig, autobind: bool = False) -> None: - """Bootstrap an IPCP, optionally binding it to its name and layer. - - Mirrors ``irm ipcp bootstrap name layer [autobind]``. - - When *autobind* is ``True`` and the IPCP type is ``UNICAST`` or - ``BROADCAST``, the sequence is:: - - bind_process(pid, ipcp_name) # accept flows for ipcp name - bind_process(pid, layer_name) # accept flows for layer name - bootstrap_ipcp(pid, conf) # bootstrap into the layer + """ + Bootstrap an IPCP, optionally binding it to its name and layer. - This matches the C ``irm ipcp bootstrap`` tool exactly. If - bootstrap fails after autobind, the bindings are rolled back. + Autobind applies to UNICAST and BROADCAST IPCPs only, and is rolled + back if the bootstrap fails. :param name: Name of the IPCP. - :param conf: IPCP configuration (includes layer name & type). + :param conf: IPCP configuration, including layer name and type. :param autobind: Bind the IPCP process to its name and layer. """ + pid = _pid_of(name) layer_name = conf.layer_name - autobind_types = (IpcpType.UNICAST, IpcpType.BROADCAST) + autobind_types = (IpcpType.UNICAST, IpcpType.BROADCAST) if autobind and conf.ipcp_type in autobind_types: _irm_bind_process(pid, name) _irm_bind_process(pid, layer_name) @@ -417,33 +358,26 @@ def bootstrap_ipcp(name: str, if autobind and conf.ipcp_type in autobind_types: _irm_unbind_process(pid, name) _irm_unbind_process(pid, layer_name) + raise def enroll_ipcp(name: str, dst: str, autobind: bool = False) -> None: - """Enroll an IPCP, optionally binding it to its name and layer. - - Mirrors ``irm ipcp enroll name layer [autobind]``. - - When *autobind* is ``True``, the sequence is:: - - enroll_ipcp(pid, dst) - bind_process(pid, ipcp_name) - bind_process(pid, layer_name) # layer learned from enrollment - - This matches the C ``irm ipcp enroll`` tool exactly. + """ + Enroll an IPCP, optionally binding it to its name and layer. :param name: Name of the IPCP. :param dst: Destination name or layer to enroll with. - :param autobind: Bind the IPCP process to its name and layer - after successful enrollment. + :param autobind: Bind the IPCP process to its name and to the layer + learned during enrollment. """ + pid = _pid_of(name) + _irm_enroll_ipcp(pid, dst) if autobind: - # Look up enrolled layer from the IPCP list. for info in _irm_list_ipcps(): if info.pid == pid: _irm_bind_process(pid, info.name) @@ -453,44 +387,38 @@ def enroll_ipcp(name: str, dst: str, def connect_ipcp(name: str, dst: str, comp: str = "*", qos: Optional[QoSSpec] = None) -> None: - """Connect IPCP components to a destination. - - Mirrors ``irm ipcp connect name dst [component ] - [qos ]``. - - When *comp* is ``"*"`` (default), both ``dt`` and ``mgmt`` - components are connected, matching the CLI default. + """ + Connect IPCP components to a destination. :param name: Name of the IPCP. :param dst: Destination IPCP name. - :param comp: Component to connect: ``"dt"``, ``"mgmt"``, or - ``"*"`` for both (default). + :param comp: ``"dt"``, ``"mgmt"``, or ``"*"`` for both. :param qos: QoS specification for the dt component. """ + pid = _pid_of(name) + if comp in ("*", "mgmt"): _irm_connect_ipcp(pid, MGMT_COMP, dst) + if comp in ("*", "dt"): _irm_connect_ipcp(pid, DT_COMP, dst, qos=qos) def disconnect_ipcp(name: str, dst: str, comp: str = "*") -> None: - """Disconnect IPCP components from a destination. - - Mirrors ``irm ipcp disconnect name dst - [component ]``. - - When *comp* is ``"*"`` (default), both ``dt`` and ``mgmt`` - components are disconnected, matching the CLI default. + """ + Disconnect IPCP components from a destination. :param name: Name of the IPCP. :param dst: Destination IPCP name. - :param comp: Component to disconnect: ``"dt"``, ``"mgmt"``, or - ``"*"`` for both (default). + :param comp: ``"dt"``, ``"mgmt"``, or ``"*"`` for both. """ + pid = _pid_of(name) + if comp in ("*", "mgmt"): _irm_disconnect_ipcp(pid, MGMT_COMP, dst) + if comp in ("*", "dt"): _irm_disconnect_ipcp(pid, DT_COMP, dst) @@ -499,24 +427,22 @@ def autoboot(name: str, ipcp_type: IpcpType, layer: str, conf: Optional[IpcpConfig] = None) -> None: - """Create, autobind and bootstrap an IPCP in one step. - - Convenience wrapper equivalent to:: - - irm ipcp bootstrap name type layer autobind - - (with an implicit ``create`` if the IPCP does not yet exist). + """ + Create, autobind and bootstrap an IPCP in one step. :param name: Name for the IPCP. :param ipcp_type: Type of IPCP to create. :param layer: Layer name to bootstrap into. - :param conf: Optional IPCP configuration. If *None*, a default - :class:`IpcpConfig` is created for the given - *ipcp_type* and *layer*. + :param conf: IPCP configuration. If *None*, a default + :class:`IpcpConfig` is built from *ipcp_type* and + *layer*. """ + create_ipcp(name, ipcp_type) + if conf is None: conf = IpcpConfig(ipcp_type=ipcp_type, layer_name=layer) else: conf.layer_name = layer + bootstrap_ipcp(name, conf, autobind=True) diff --git a/ouroboros/dev.py b/ouroboros/dev.py index ffa59b6..1397bca 100644 --- a/ouroboros/dev.py +++ b/ouroboros/dev.py @@ -1,31 +1,17 @@ # -# Ouroboros - Copyright (C) 2016 - 2026 -# -# Python API for Ouroboros -# -# Dimitri Staessens -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public License -# version 2.1 as published by the Free Software Foundation. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., http://www.fsf.org/about/contact/. +# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens +# SPDX-License-Identifier: LGPL-2.1-only # -"""Flow allocation, acceptance and I/O for the Ouroboros dev API.""" +""" +Flow allocation, acceptance and I/O for the Ouroboros dev API. +""" from __future__ import annotations import warnings from enum import IntFlag -from typing import Optional +from typing import TYPE_CHECKING, Optional, Type from _ouroboros_dev_cffi import ffi, lib @@ -40,19 +26,28 @@ from ouroboros.errors import ( ) from ouroboros.qos import QoSSpec +if TYPE_CHECKING: + from types import TracebackType + check_ouroboros_version(lib.OUROBOROS_VERSION_MAJOR, lib.OUROBOROS_VERSION_MINOR) class FrctFlags(IntFlag): - """FRCT-level feature flags.""" + """ + FRCT-level feature flags. + """ + RETRANSMIT = 0o1 RESCNTL = 0o2 LINGER = 0o4 class FlowProperties(IntFlag): - """Flags describing flow properties and blocking behaviour.""" + """ + Flags describing flow properties and blocking behaviour. + """ + READ_ONLY = 0o0 WRITE_ONLY = 0o1 READ_WRITE = 0o2 @@ -65,47 +60,68 @@ class FlowProperties(IntFlag): class Flow: - """Represents an allocated Ouroboros flow.""" + """ + Represents an allocated Ouroboros flow. + """ def __init__(self, fd: int = -1) -> None: - """Construct a Flow wrapper. + """ + Construct a Flow wrapper. - :param fd: Existing flow descriptor to wrap, or -1 for an - unallocated Flow that will be populated by - :meth:`alloc`, :meth:`accept` or :meth:`join`. + :param fd: Existing flow descriptor, or -1 for an unallocated + flow. """ + self._fd: int = fd def __enter__(self) -> Flow: return self - def __exit__(self, exc_type, exc_value, tb) -> None: + def __exit__(self, exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + tb: Optional[TracebackType]) -> None: self.dealloc() def __del__(self) -> None: + """ + Deallocate on collection; errors are swallowed as the library + may already be torn down at interpreter shutdown. + """ + try: self.dealloc() except Exception: # pylint: disable=broad-exception-caught - pass # interpreter shutdown may have torn down lib already + pass def __repr__(self) -> str: return f"Flow(fd={self._fd})" def fileno(self) -> int: - """Return the underlying ouroboros flow descriptor.""" + """ + Return the underlying ouroboros flow descriptor. + + :return: The ouroboros flow descriptor, or -1 if unallocated. + """ + return self._fd def alloc(self, dst: str, qos: Optional[QoSSpec] = None, timeo: Optional[float] = None) -> Optional[QoSSpec]: - """Allocate a flow with a certain QoS to a destination. + """ + Allocate a flow with a certain QoS to a destination. :param dst: Destination name. :param qos: Requested QoS. - :param timeo: Allocation timeout (None blocks forever, 0 is async). + :param timeo: Allocation timeout (None blocks forever, 0 is + async). :return: The QoS the IRM granted for the new flow. + :raises FlowAlreadyAllocatedError: If this Flow is already + allocated. + :raises FlowError: On a negative ouroboros return code. """ + if self._fd >= 0: raise FlowAlreadyAllocatedError() @@ -113,18 +129,25 @@ class Flow: _timeo = fl_to_timespec(ffi, timeo) rc = lib.flow_alloc(dst.encode(), _qos, _timeo) + raise_errno(rc) + self._fd = rc return qosspec_to_qos(ffi, _qos) def accept(self, timeo: Optional[float] = None) -> Optional[QoSSpec]: - """Accept an incoming flow and return its QoS. + """ + Accept an incoming flow and return its QoS. :param timeo: Accept timeout (None blocks forever, 0 is async). :return: The QoS of the accepted flow. + :raises FlowAlreadyAllocatedError: If this Flow is already + allocated. + :raises FlowError: On a negative ouroboros return code. """ + if self._fd >= 0: raise FlowAlreadyAllocatedError() @@ -132,7 +155,9 @@ class Flow: _timeo = fl_to_timespec(ffi, timeo) rc = lib.flow_accept(_qos, _timeo) + raise_errno(rc) + self._fd = rc return qosspec_to_qos(ffi, _qos) @@ -140,26 +165,38 @@ class Flow: def join(self, dst: str, timeo: Optional[float] = None) -> None: - """Join a broadcast layer. + """ + Join a broadcast layer. - :param dst: Destination broadcast layer name. + :param dst: Broadcast layer name. :param timeo: Join timeout (None blocks forever, 0 is async). + :raises FlowAlreadyAllocatedError: If this Flow is already + allocated. + :raises FlowError: On a negative ouroboros return code. """ + if self._fd >= 0: raise FlowAlreadyAllocatedError() _timeo = fl_to_timespec(ffi, timeo) rc = lib.flow_join(dst.encode(), _timeo) + raise_errno(rc) + self._fd = rc def dealloc(self) -> None: - """Deallocate this flow. Idempotent: a no-op on an unallocated flow.""" + """ + Deallocate this flow; a no-op on an unallocated flow. + """ + if self._fd < 0: return + rc = lib.flow_dealloc(self._fd) self._fd = -1 + if rc < 0: warnings.warn(f"flow_dealloc returned {rc}", FlowDeallocWarning, stacklevel=2) @@ -167,13 +204,17 @@ class Flow: def write(self, buf: bytes, count: Optional[int] = None) -> int: - """Write up to *count* bytes to the flow. + """ + Write up to *count* bytes to the flow. :param buf: Buffer to write from. - :param count: Number of bytes to write (defaults to ``len(buf)``). + :param count: Number of bytes to write (defaults to + ``len(buf)``). :return: Number of bytes written. - :raises FlowError: (or subclass) on negative ouroboros return codes. + :raises FlowError: On a negative ouroboros return code. + :raises FlowNotAllocatedError: If the flow is not allocated. """ + if self._fd < 0: raise FlowNotAllocatedError() @@ -181,30 +222,38 @@ class Flow: count = len(buf) rc = lib.flow_write(self._fd, ffi.from_buffer(buf), count) + return raise_errno(rc) def writeline(self, ln: str) -> int: - """Encode *ln* as UTF-8 and write it to the flow. + """ + Encode *ln* as UTF-8 and write it to the flow. :param ln: String to write. :return: Number of bytes written. + :raises FlowNotAllocatedError: If the flow is not allocated. """ + if self._fd < 0: raise FlowNotAllocatedError() data = ln.encode() + return self.write(data, len(data)) def read(self, count: Optional[int] = None) -> bytes: - """Read up to *count* bytes from the flow. + """ + Read up to *count* bytes from the flow. :param count: Maximum number of bytes to read (default 2048). - :return: Bytes read (may be empty when an SDU was just - fully consumed by a previous matching read). - :raises FlowError: (or subclass) on negative ouroboros return codes. + :return: Bytes read; empty when a previous read fully + consumed the SDU. + :raises FlowError: On a negative ouroboros return code. + :raises FlowNotAllocatedError: If the flow is not allocated. """ + if self._fd < 0: raise FlowNotAllocatedError() @@ -214,181 +263,320 @@ class Flow: _buf = ffi.new("char []", count) rc = lib.flow_read(self._fd, _buf, count) + raise_errno(rc) return ffi.unpack(_buf, rc) def readline(self) -> str: - """Read from the flow and decode the result as UTF-8.""" + """ + Read from the flow and decode the result as UTF-8. + + :return: Decoded UTF-8 string read from the flow. + :raises FlowNotAllocatedError: If the flow is not allocated. + """ + if self._fd < 0: raise FlowNotAllocatedError() return self.read().decode() def set_snd_timeout(self, timeo: float) -> None: - """Set the timeout for blocking writes (seconds).""" + """ + Set the timeout for blocking writes (seconds). + + :param timeo: Write timeout in seconds. + :raises FlowError: On a negative ouroboros return code. + """ + _timeo = fl_to_timespec(ffi, timeo) + raise_errno(lib.flow_set_snd_timeout(self._fd, _timeo)) def get_snd_timeout(self) -> Optional[float]: - """Return the timeout for blocking writes (seconds).""" + """ + Return the timeout for blocking writes (seconds). + + :return: Timeout for blocking writes, in seconds. + :raises FlowError: On a negative ouroboros return code. + """ + _timeo = ffi.new("struct timespec *") + raise_errno(lib.flow_get_snd_timeout(self._fd, _timeo)) + return timespec_to_fl(ffi, _timeo) def set_rcv_timeout(self, timeo: float) -> None: - """Set the timeout for blocking reads (seconds).""" + """ + Set the timeout for blocking reads (seconds). + + :param timeo: Read timeout in seconds. + :raises FlowError: On a negative ouroboros return code. + """ + _timeo = fl_to_timespec(ffi, timeo) + raise_errno(lib.flow_set_rcv_timeout(self._fd, _timeo)) def get_rcv_timeout(self) -> Optional[float]: - """Return the timeout for blocking reads (seconds).""" + """ + Return the timeout for blocking reads (seconds). + + :return: Timeout for blocking reads, in seconds. + :raises FlowError: On a negative ouroboros return code. + """ + _timeo = ffi.new("struct timespec *") + raise_errno(lib.flow_get_rcv_timeout(self._fd, _timeo)) + return timespec_to_fl(ffi, _timeo) def get_qos(self) -> Optional[QoSSpec]: - """Return the current QoS in effect on the flow.""" + """ + Return the current QoS in effect on the flow. + + :return: The QoS currently in effect on the flow. + :raises FlowError: On a negative ouroboros return code. + """ + _qos = ffi.new("qosspec_t *") + raise_errno(lib.flow_get_qos(self._fd, _qos)) + return qosspec_to_qos(ffi, _qos) def get_rx_queue_len(self) -> int: - """Return the receive queue length (bytes).""" + """ + Return the receive queue length (bytes). + + :return: Receive queue length in bytes. + :raises FlowError: On a negative ouroboros return code. + """ + size = ffi.new("size_t *") + raise_errno(lib.flow_get_rx_qlen(self._fd, size)) + return int(size[0]) def get_tx_queue_len(self) -> int: - """Return the transmit queue length (bytes).""" + """ + Return the transmit queue length (bytes). + + :return: Transmit queue length in bytes. + :raises FlowError: On a negative ouroboros return code. + """ + size = ffi.new("size_t *") + raise_errno(lib.flow_get_tx_qlen(self._fd, size)) + return int(size[0]) def get_mtu(self) -> int: - """Return the per-packet MTU. + """ + Return the maximum user payload that fits in one n-1 PDU after + crypto headers (bytes), or 0 if unknown. - This is the maximum user payload that fits in one n-1 PDU, - after crypto headers. Returns 0 if unknown. + :return: Maximum user payload in bytes, or 0 if unknown. + :raises FlowError: On a negative ouroboros return code. """ + mtu = ffi.new("size_t *") + raise_errno(lib.flow_get_mtu(self._fd, mtu)) + return int(mtu[0]) def set_flags(self, flags: FlowProperties) -> None: - """Replace the full set of flags for this flow. - - .. warning:: - This OVERWRITES the entire ``oflags`` value, including - the access-mode bits (``READ_ONLY`` / ``WRITE_ONLY`` / - ``READ_WRITE``). Passing only ``NO_PARTIAL_READ`` (for - example) silently downgrades the flow to read-only and - subsequent writes will fail with :class:`FlowPermissionError`. - - To preserve the existing access mode use - :meth:`add_flags` / :meth:`remove_flags`, or read the - current flags with :meth:`get_flags` and OR in the new - bits explicitly. + """ + Replace the full set of flags for this flow. :param flags: Bitmask of :class:`FlowProperties` values. + :raises FlowError: On a negative ouroboros return code. """ + raise_errno(lib.flow_set_flags(self._fd, int(flags))) def add_flags(self, flags: FlowProperties) -> None: - """OR *flags* into the current ``oflags`` value. + """ + OR *flags* into the current flags, leaving other bits set. - Preserves the access mode and any other already-set bits. + :param flags: :class:`FlowProperties` bits to set. + :raises FlowError: On a negative ouroboros return code. """ + current = self.get_flags() + self.set_flags(current | FlowProperties(int(flags))) def remove_flags(self, flags: FlowProperties) -> None: - """Clear *flags* from the current ``oflags`` value. + """ + Clear *flags* from the current flags, leaving other bits set. - Preserves every other bit (including the access mode). + :param flags: :class:`FlowProperties` bits to clear. + :raises FlowError: On a negative ouroboros return code. """ + current = self.get_flags() + self.set_flags(current & ~FlowProperties(int(flags))) def get_flags(self) -> FlowProperties: - """Return the current flags for this flow.""" + """ + Return the current flags for this flow. + + :return: The current :class:`FlowProperties` bitmask. + :raises FlowError: On a negative ouroboros return code. + """ + flags = raise_errno(lib.flow_get_flags(self._fd)) + return FlowProperties(int(flags)) def set_frct_flags(self, flags: FrctFlags) -> None: - """Set FRCT flags for this flow. + """ + Set FRCT flags for this flow. :param flags: Bitmask of :class:`FrctFlags`. + :raises FlowError: On a negative ouroboros return code. """ + raise_errno(lib.flow_set_frct_flags(self._fd, int(flags))) def get_frct_flags(self) -> FrctFlags: - """Return the FRCT flags for this flow.""" + """ + Return the FRCT flags for this flow. + + :return: The current :class:`FrctFlags` bitmask. + :raises FlowError: On a negative ouroboros return code. + """ + flags = raise_errno(lib.flow_get_frct_flags(self._fd)) + return FrctFlags(int(flags)) def set_frct_max_sdu(self, size: int) -> None: - """Set the maximum reassembly SDU size for FRCT (bytes).""" + """ + Set the maximum reassembly SDU size for FRCT (bytes). + + :param size: Maximum reassembly SDU size in bytes. + :raises FlowError: On a negative ouroboros return code. + """ + raise_errno(lib.flow_set_frct_max_sdu(self._fd, size)) def get_frct_max_sdu(self) -> int: - """Return the maximum reassembly SDU size for FRCT (bytes).""" + """ + Return the maximum reassembly SDU size for FRCT (bytes). + + :return: Maximum reassembly SDU size in bytes. + :raises FlowError: On a negative ouroboros return code. + """ + size = ffi.new("size_t *") + raise_errno(lib.flow_get_frct_max_sdu(self._fd, size)) + return int(size[0]) def set_frct_rcv_ring_size(self, size: int) -> None: - """Set the stream receive ring size (bytes, power of two).""" + """ + Set the stream receive ring size (bytes, power of two). + + :param size: Stream receive ring size in bytes (power of two). + :raises FlowError: On a negative ouroboros return code. + """ + raise_errno(lib.flow_set_frct_rcv_ring_sz(self._fd, size)) def get_frct_rcv_ring_size(self) -> int: - """Return the stream receive ring size (bytes).""" + """ + Return the stream receive ring size (bytes). + + :return: Stream receive ring size in bytes. + :raises FlowError: On a negative ouroboros return code. + """ + size = ffi.new("size_t *") + raise_errno(lib.flow_get_frct_rcv_ring_sz(self._fd, size)) + return int(size[0]) def set_frct_rto_min(self, rto_ns: int) -> None: - """Set the FRCT RTO floor (nanoseconds).""" + """ + Set the FRCT RTO floor (nanoseconds). + + :param rto_ns: FRCT RTO floor in nanoseconds. + :raises FlowError: On a negative ouroboros return code. + """ + raise_errno(lib.flow_set_frct_rto_min(self._fd, rto_ns)) def get_frct_rto_min(self) -> int: - """Return the FRCT RTO floor (nanoseconds).""" + """ + Return the FRCT RTO floor (nanoseconds). + + :return: FRCT RTO floor in nanoseconds. + :raises FlowError: On a negative ouroboros return code. + """ + rto = ffi.new("time_t *") + raise_errno(lib.flow_get_frct_rto_min(self._fd, rto)) + return int(rto[0]) def flow_alloc(dst: str, qos: Optional[QoSSpec] = None, timeo: Optional[float] = None) -> Flow: - """Allocate a new flow and return the :class:`Flow` wrapper. + """ + Allocate a new flow and return the :class:`Flow` wrapper. :param dst: Destination name. :param qos: Requested QoS. :param timeo: Allocation timeout (None blocks forever, 0 is async). + :return: The allocated flow. """ + f = Flow() + f.alloc(dst, qos, timeo) + return f def flow_accept(timeo: Optional[float] = None) -> Flow: - """Accept an incoming flow and return the :class:`Flow` wrapper. + """ + Accept an incoming flow and return the :class:`Flow` wrapper. :param timeo: Accept timeout (None blocks forever, 0 is async). + :return: The accepted flow. """ + f = Flow() + f.accept(timeo) + return f def flow_join(dst: str, timeo: Optional[float] = None) -> Flow: - """Join a broadcast layer and return the :class:`Flow` wrapper. + """ + Join a broadcast layer and return the :class:`Flow` wrapper. :param dst: Broadcast layer name. :param timeo: Join timeout (None blocks forever, 0 is async). + :return: The joined broadcast flow. """ + f = Flow() + f.join(dst, timeo) + return f diff --git a/ouroboros/errors.py b/ouroboros/errors.py index aa1e174..6ab6053 100644 --- a/ouroboros/errors.py +++ b/ouroboros/errors.py @@ -1,30 +1,13 @@ # -# Ouroboros - Copyright (C) 2016 - 2026 -# -# Python API for Ouroboros - Exceptions and errno translation -# -# Dimitri Staessens -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public License -# version 2.1 as published by the Free Software Foundation. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., http://www.fsf.org/about/contact/. +# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens +# SPDX-License-Identifier: LGPL-2.1-only # -"""Exception hierarchy and errno translation for pyouroboros. +""" +Exception hierarchy and errno translation for pyouroboros. -The hierarchy follows stdlib precedent (``socket``, ``ssl``, ``json``): -a single root :class:`OuroborosError`, semantically-named subclasses, -and multiple inheritance from Python builtins where the semantics -match (e.g. :class:`FlowTimeout` is a :class:`TimeoutError`). +The ``E*`` constants mirror the Ouroboros-specific errno values in +``include/ouroboros/errno.h``. """ from __future__ import annotations @@ -33,8 +16,6 @@ import errno import os from importlib.metadata import PackageNotFoundError, version -# Ouroboros-specific errno values, mirroring include/ouroboros/errno.h. -# Imported here (rather than via CFFI) so this module stays FFI-free ENOTALLOC = 1000 # Flow is not allocated EIPCPTYPE = 1001 # Unknown IPCP type EIRMD = 1002 # Failed to communicate with IRMD @@ -47,128 +28,177 @@ ECRYPT = 1008 # Encryption error EAUTH = 1009 # Authentication error EREPLAY = 1010 # OAP replay detected +_O7S_ERRNO_STR: dict[int, str] = { + ENOTALLOC: "flow is not allocated", + EIPCPTYPE: "unknown IPCP type", + EIRMD: "failed to communicate with IRMd", + EIPCP: "failed to communicate with IPCP", + EIPCPSTATE: "target in wrong state", + EFLOWDOWN: "flow is down", + EFLOWPEER: "flow peer timed out", + ENAME: "naming error", + ECRYPT: "encryption error", + EAUTH: "authentication error", + EREPLAY: "OAP replay detected", +} -# --- Root --- class OuroborosError(Exception): - """Base class for all pyouroboros exceptions.""" - + """ + Base class for all pyouroboros exceptions. + """ -# --- IRM-side --- class IrmError(OuroborosError): - """Base class for IRM (control plane) errors.""" + """ + Base class for IRM (control plane) errors. + """ class IpcpCreateError(IrmError): - """Raised when IPCP creation fails.""" + """ + Raised when IPCP creation fails. + """ class IpcpBootstrapError(IrmError): - """Raised when IPCP bootstrapping fails.""" + """ + Raised when IPCP bootstrapping fails. + """ class IpcpEnrollError(IrmError): - """Raised when IPCP enrollment fails.""" + """ + Raised when IPCP enrollment fails. + """ class IpcpConnectError(IrmError): - """Raised when IPCP connection or disconnection fails.""" + """ + Raised when IPCP connection or disconnection fails. + """ class IpcpTypeError(IrmError, ValueError): - """Raised when an unknown IPCP type is encountered.""" + """ + Raised when an unknown IPCP type is encountered. + """ class IpcpStateError(IrmError): - """Raised when an IPCP/IRMd is in the wrong state for the request.""" + """ + Raised when an IPCP/IRMd is in the wrong state for the request. + """ class IrmdError(IrmError): - """Raised when the IRM daemon is unreachable.""" + """ + Raised when the IRM daemon is unreachable. + """ class IpcpdError(IrmError): - """Raised when the IPCP daemon is unreachable.""" + """ + Raised when the IPCP daemon is unreachable. + """ class BindError(IrmError): - """Raised when binding a program/process/IPCP to a name fails.""" + """ + Raised when binding a program/process/IPCP to a name fails. + """ class NameNotFoundError(IrmError): - """Raised when a name lookup or unregister target does not exist.""" + """ + Raised when a name lookup or unregister target is missing. + """ class NameExistsError(IrmError): - """Raised when a name already exists.""" + """ + Raised when a name already exists. + """ class InvalidNameError(IrmError, ValueError): - """Raised when a name is malformed or otherwise invalid.""" - + """ + Raised when a name is malformed or otherwise invalid. + """ -# --- Flow / dev-side --- class FlowError(OuroborosError): - """Base class for flow-plane errors.""" + """ + Base class for flow-plane errors. + """ class FlowAlreadyAllocatedError(FlowError): - """Raised when allocating on a Flow that already holds an fd.""" + """ + Raised when allocating on a Flow that already holds an fd. + """ class FlowNotAllocatedError(FlowError): - """Raised when operating on a Flow that has no fd.""" + """ + Raised when operating on a Flow that has no fd. + """ class FlowDownError(FlowError, ConnectionError): - """Raised when the flow has gone down.""" + """ + Raised when the flow has gone down. + """ class FlowPeerError(FlowDownError): - """Raised when the flow's peer has timed out.""" + """ + Raised when the flow's peer has timed out. + """ class FlowPermissionError(FlowError, PermissionError): - """Raised when an operation is not permitted on the flow.""" + """ + Raised when an operation is not permitted on the flow. + """ class FlowTimeout(FlowError, TimeoutError): - """Raised when a flow operation times out.""" + """ + Raised when a flow operation times out. + """ class FlowCryptError(FlowError): - """Raised on flow encryption errors.""" + """ + Raised on flow encryption errors. + """ class FlowAuthError(FlowError): - """Raised on flow authentication errors.""" + """ + Raised on flow authentication errors. + """ class FlowReplayError(FlowError): - """Raised when OAP replay is detected.""" + """ + Raised when OAP replay is detected. + """ class FlowEventError(FlowError): - """Raised on errors from the flow event subsystem.""" + """ + Raised on errors from the flow event subsystem. + """ class FlowDeallocWarning(Warning): - """Warning issued when a deallocation reports a non-fatal problem.""" - + """ + Warning issued when a deallocation reports a non-fatal problem. + """ -# --- errno translation --- -# Errno -> Python exception class. Errnos are in their canonical -# positive form here; raise_errno() flips the sign. -# -# Mapped errnos override the caller's `default=` argument, so we only -# map errnos whose semantics are unambiguous across both flow and IRM -# contexts. EPERM and EINVAL are intentionally NOT mapped: they can -# mean very different things on different operations, and a global -# mapping would silently override the call-site's chosen default (e.g. -# masking IpcpBootstrapError as a bare ValueError). _ERRNO_MAP: dict[int, type[BaseException]] = { errno.ETIMEDOUT: TimeoutError, errno.EAGAIN: BlockingIOError, @@ -194,49 +224,57 @@ _ERRNO_MAP: dict[int, type[BaseException]] = { def _strerror(err: int) -> str: if err >= 1000: return _O7S_ERRNO_STR.get(err, f"ouroboros errno {err}") - return os.strerror(err) - -_O7S_ERRNO_STR: dict[int, str] = { - ENOTALLOC: "flow is not allocated", - EIPCPTYPE: "unknown IPCP type", - EIRMD: "failed to communicate with IRMd", - EIPCP: "failed to communicate with IPCP", - EIPCPSTATE: "target in wrong state", - EFLOWDOWN: "flow is down", - EFLOWPEER: "flow peer timed out", - ENAME: "naming error", - ECRYPT: "encryption error", - EAUTH: "authentication error", - EREPLAY: "OAP replay detected", -} + return os.strerror(err) def raise_errno(rc: int, default: type[OuroborosError] = FlowError) -> int: - """Translate a non-negative-or-negative-errno return code. - - Returns *rc* unchanged when it is non-negative. Otherwise raises - the mapped exception (or *default* if no mapping exists). """ + Translate a non-negative-or-negative-errno return code. + + ``_ERRNO_MAP`` holds errnos in their canonical positive form. + + :param rc: The return code to translate; a negative + value is a negated errno. + :param default: Exception class to raise when the errno + has no mapping; a mapped errno overrides + it. + :return: *rc* unchanged when it is non-negative. + :raises OuroborosError: (or the builtin mapped for ``-rc``) when + *rc* is negative. + """ + if rc >= 0: return rc + err = -rc exc_cls = _ERRNO_MAP.get(err, default) raise exc_cls(_strerror(err)) def check_ouroboros_version(o7s_major: int, o7s_minor: int) -> None: - """Verify that the installed pyouroboros matches the linked library. - - Compares *o7s_major*/*o7s_minor* (from - ``OUROBOROS_VERSION_{MAJOR,MINOR}`` in the CFFI module) against - the installed pyouroboros distribution metadata. Silently returns - when running from a source tree without distribution metadata. """ + Verify that the installed pyouroboros matches the linked library. + + Compares the linked library version against the installed + pyouroboros distribution metadata. Silently returns when running + from a source tree without distribution metadata. + + :param o7s_major: Major version of the linked library, from + ``OUROBOROS_VERSION_MAJOR`` in the CFFI + module. + :param o7s_minor: Minor version of the linked library, from + ``OUROBOROS_VERSION_MINOR`` in the CFFI + module. + :raises RuntimeError: If the library and pyouroboros ``major.minor`` + differ. + """ + try: pyo7s_parts = version("PyOuroboros").split(".") except PackageNotFoundError: return + if o7s_major != int(pyo7s_parts[0]) or \ o7s_minor != int(pyo7s_parts[1]): raise RuntimeError( diff --git a/ouroboros/event.py b/ouroboros/event.py index aa03ad4..24b58b8 100644 --- a/ouroboros/event.py +++ b/ouroboros/event.py @@ -1,30 +1,16 @@ # -# Ouroboros - Copyright (C) 2016 - 2026 -# -# Python API for Ouroboros -# -# Dimitri Staessens -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public License -# version 2.1 as published by the Free Software Foundation. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., http://www.fsf.org/about/contact/. +# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens +# SPDX-License-Identifier: LGPL-2.1-only # -"""Asynchronous flow event monitoring for Ouroboros.""" +""" +Asynchronous flow event monitoring for Ouroboros. +""" from __future__ import annotations from enum import IntFlag -from typing import List, Optional, Tuple +from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Type from _ouroboros_dev_cffi import ffi, lib @@ -32,9 +18,15 @@ from ouroboros._timespec import fl_to_timespec from ouroboros.dev import Flow from ouroboros.errors import FlowEventError, raise_errno +if TYPE_CHECKING: + from types import TracebackType + class FEventType(IntFlag): - """Types of flow events.""" + """ + Types of flow events. + """ + FLOW_PKT = lib.FLOW_PKT FLOW_DOWN = lib.FLOW_DOWN FLOW_UP = lib.FLOW_UP @@ -44,7 +36,9 @@ class FEventType(IntFlag): class FEventQueue: - """A queue of flow events waiting to be drained.""" + """ + A queue of flow events waiting to be drained. + """ def __init__(self) -> None: self._fq = lib.fqueue_create() @@ -54,40 +48,58 @@ class FEventQueue: def __enter__(self) -> FEventQueue: return self - def __exit__(self, exc_type, exc_value, tb) -> None: + def __exit__(self, exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + tb: Optional[TracebackType]) -> None: self.destroy() def __del__(self) -> None: self.destroy() def destroy(self) -> None: - """Destroy the underlying queue. Idempotent.""" + """ + Destroy the underlying queue. Idempotent. + """ + if self._fq != ffi.NULL: lib.fqueue_destroy(self._fq) + self._fq = ffi.NULL def next(self) -> Tuple[Flow, FEventType]: - """Return the next ``(flow, event_type)`` pair from the queue.""" + """ + Return the next ``(flow, event_type)`` pair from the queue. + + :return: The :class:`~ouroboros.dev.Flow` the event occurred + on, and its :class:`FEventType`. + :raises FlowEventError: On a negative ouroboros return code. + """ + fd = lib.fqueue_next(self._fq) + raise_errno(fd, default=FlowEventError) _type = lib.fqueue_type(self._fq) + raise_errno(_type, default=FlowEventError) return Flow(fd), FEventType(_type) @property - def handle(self): - """Return the underlying ``fqueue_t *``. + def handle(self) -> Any: + """ + Return the underlying ``fqueue_t *``. - Intended for use by :meth:`FlowSet.wait` within the same - package; not part of the user-facing API. + :return: The underlying ``fqueue_t *`` CFFI pointer. """ + return self._fq class FlowSet: - """A set of flows that can be monitored for events.""" + """ + A set of flows that can be monitored for events. + """ def __init__(self, flows: Optional[List[Flow]] = None) -> None: self._set = lib.fset_create() @@ -98,26 +110,40 @@ class FlowSet: for flow in flows: if lib.fset_add(self._set, flow.fileno()) != 0: lib.fset_destroy(self._set) + self._set = ffi.NULL raise MemoryError(f"Failed to add flow {flow.fileno()}.") def __enter__(self) -> FlowSet: return self - def __exit__(self, exc_type, exc_value, tb) -> None: + def __exit__(self, exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + tb: Optional[TracebackType]) -> None: self.destroy() def __del__(self) -> None: self.destroy() def destroy(self) -> None: - """Destroy the underlying set. Idempotent.""" + """ + Destroy the underlying set. Idempotent. + """ + if self._set != ffi.NULL: lib.fset_destroy(self._set) + self._set = ffi.NULL def add(self, flow: Flow) -> None: - """Add *flow* to this set.""" + """ + Add *flow* to this set. + + :param flow: The flow to add to this set. + :raises ValueError: If the FlowSet has been destroyed. + :raises MemoryError: If the flow could not be added. + """ + if self._set == ffi.NULL: raise ValueError("FlowSet has been destroyed") @@ -125,14 +151,25 @@ class FlowSet: raise MemoryError(f"Failed to add flow {flow.fileno()}") def zero(self) -> None: - """Remove all flows from this set.""" + """ + Remove all flows from this set. + + :raises ValueError: If the FlowSet has been destroyed. + """ + if self._set == ffi.NULL: raise ValueError("FlowSet has been destroyed") lib.fset_zero(self._set) def remove(self, flow: Flow) -> None: - """Remove *flow* from this set.""" + """ + Remove *flow* from this set. + + :param flow: The flow to remove from this set. + :raises ValueError: If the FlowSet has been destroyed. + """ + if self._set == ffi.NULL: raise ValueError("FlowSet has been destroyed") @@ -141,15 +178,20 @@ class FlowSet: def wait(self, fq: FEventQueue, timeo: Optional[float] = None) -> None: - """Wait for at least one event on one of the monitored flows. + """ + Wait for at least one event on one of the monitored flows. :param fq: The :class:`FEventQueue` to drain events into. :param timeo: Wait timeout in seconds (None blocks forever). + :raises ValueError: If the FlowSet has been destroyed. + :raises FlowEventError: On a negative ouroboros return code. """ + if self._set == ffi.NULL: raise ValueError("FlowSet has been destroyed") _timeo = fl_to_timespec(ffi, timeo) rc = lib.fevent(self._set, fq.handle, _timeo) + raise_errno(rc, default=FlowEventError) diff --git a/ouroboros/irm.py b/ouroboros/irm.py index 34a65fb..ea7d665 100644 --- a/ouroboros/irm.py +++ b/ouroboros/irm.py @@ -1,31 +1,17 @@ # -# Ouroboros - Copyright (C) 2016 - 2026 -# -# Python API for Ouroboros - IRM -# -# Dimitri Staessens -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public License -# version 2.1 as published by the Free Software Foundation. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., http://www.fsf.org/about/contact/. +# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens +# SPDX-License-Identifier: LGPL-2.1-only # -"""IRM (IPC Resource Manager) bindings for Ouroboros.""" +""" +IRM (IPC Resource Manager) bindings for Ouroboros. +""" from __future__ import annotations from dataclasses import dataclass, field from enum import IntEnum -from typing import List, Optional +from typing import Any, List, Optional from _ouroboros_irm_cffi import ffi, lib @@ -47,11 +33,17 @@ from ouroboros.qos import QoSSpec check_ouroboros_version(lib.OUROBOROS_VERSION_MAJOR, lib.OUROBOROS_VERSION_MINOR) +BIND_AUTO = lib.BIND_AUTO + +DT_COMP = "Data Transfer" +MGMT_COMP = "Management" -# --- Enumerations --- class IpcpType(IntEnum): - """IPCP types available in Ouroboros.""" + """ + IPCP types available in Ouroboros. + """ + LOCAL = lib.IPCP_LOCAL UNICAST = lib.IPCP_UNICAST BROADCAST = lib.IPCP_BROADCAST @@ -62,35 +54,53 @@ class IpcpType(IntEnum): class AddressAuthPolicy(IntEnum): - """Address authority policies for unicast IPCPs.""" + """ + Address authority policies for unicast IPCPs. + """ + FLAT_RANDOM = lib.ADDR_AUTH_FLAT_RANDOM class LinkStatePolicy(IntEnum): - """Link state routing policies.""" + """ + Link state routing policies. + """ + SIMPLE = lib.LS_SIMPLE LFA = lib.LS_LFA ECMP = lib.LS_ECMP class RoutingPolicy(IntEnum): - """Routing policies.""" + """ + Routing policies. + """ + LINK_STATE = lib.ROUTING_LINK_STATE class CongestionAvoidPolicy(IntEnum): - """Congestion avoidance policies.""" + """ + Congestion avoidance policies. + """ + NONE = lib.CA_NONE MB_ECN = lib.CA_MB_ECN class DirectoryPolicy(IntEnum): - """Directory policies.""" + """ + Directory policies. + """ + DHT = lib.DIR_DHT class DirectoryHashAlgo(IntEnum): - """Directory hash algorithms.""" + """ + Directory hash algorithms. + """ + SHA3_224 = lib.DIR_HASH_SHA3_224 SHA3_256 = lib.DIR_HASH_SHA3_256 SHA3_384 = lib.DIR_HASH_SHA3_384 @@ -98,23 +108,20 @@ class DirectoryHashAlgo(IntEnum): class LoadBalancePolicy(IntEnum): - """Load balancing policies for names.""" + """ + Load balancing policies for names. + """ + ROUND_ROBIN = lib.LB_RR SPILL = lib.LB_SPILL -BIND_AUTO = lib.BIND_AUTO - -# Unicast IPCP component names -DT_COMP = "Data Transfer" -MGMT_COMP = "Management" - - -# --- Configuration classes --- - @dataclass class LinkStateConfig: - """Configuration for link state routing.""" + """ + Configuration for link state routing. + """ + pol: LinkStatePolicy = LinkStatePolicy.SIMPLE t_recalc: int = 4 t_update: int = 15 @@ -123,14 +130,20 @@ class LinkStateConfig: @dataclass class RoutingConfig: - """Routing configuration.""" + """ + Routing configuration. + """ + pol: RoutingPolicy = RoutingPolicy.LINK_STATE ls: LinkStateConfig = field(default_factory=LinkStateConfig) @dataclass class DtConfig: - """Data transfer configuration for unicast IPCPs.""" + """ + Data transfer configuration for unicast IPCPs. + """ + addr_size: int = 4 eid_size: int = 8 max_ttl: int = 60 @@ -139,7 +152,10 @@ class DtConfig: @dataclass class DhtConfig: - """DHT directory configuration.""" + """ + DHT directory configuration. + """ + alpha: int = 3 k: int = 8 t_expire: int = 86400 @@ -149,14 +165,20 @@ class DhtConfig: @dataclass class DirConfig: - """Directory configuration.""" + """ + Directory configuration. + """ + pol: DirectoryPolicy = DirectoryPolicy.DHT dht: DhtConfig = field(default_factory=DhtConfig) @dataclass class UnicastConfig: - """Configuration for unicast IPCPs.""" + """ + Configuration for unicast IPCPs. + """ + dt: DtConfig = field(default_factory=DtConfig) dir: DirConfig = field(default_factory=DirConfig) addr_auth: AddressAuthPolicy = AddressAuthPolicy.FLAT_RANDOM @@ -165,14 +187,20 @@ class UnicastConfig: @dataclass class EthConfig: - """Configuration for Ethernet IPCPs (LLC or DIX).""" + """ + Configuration for Ethernet IPCPs (LLC or DIX). + """ + dev: str = "" ethertype: int = 0xA000 @dataclass class Udp4Config: - """Configuration for UDP over IPv4 IPCPs.""" + """ + Configuration for UDP over IPv4 IPCPs. + """ + ip_addr: str = "0.0.0.0" dns_addr: str = "0.0.0.0" port: int = 3435 @@ -180,7 +208,10 @@ class Udp4Config: @dataclass class Udp6Config: - """Configuration for UDP over IPv6 IPCPs.""" + """ + Configuration for UDP over IPv6 IPCPs. + """ + ip_addr: str = "::" dns_addr: str = "::" port: int = 3435 @@ -188,18 +219,10 @@ class Udp6Config: @dataclass class IpcpConfig: - """Configuration for bootstrapping an IPCP. - - Depending on the IPCP type, set the appropriate sub-configuration: - - - UNICAST: ``unicast`` (:class:`UnicastConfig`) - - ETH_LLC: ``eth`` (:class:`EthConfig`) - - ETH_DIX: ``eth`` (:class:`EthConfig`) - - UDP4: ``udp4`` (:class:`Udp4Config`) - - UDP6: ``udp6`` (:class:`Udp6Config`) - - LOCAL: no extra config needed - - BROADCAST: no extra config needed """ + Configuration for bootstrapping an IPCP. + """ + ipcp_type: IpcpType layer_name: str = "" dir_hash_algo: DirectoryHashAlgo = DirectoryHashAlgo.SHA3_256 @@ -211,7 +234,10 @@ class IpcpConfig: @dataclass class NameSecPaths: - """Security paths for a name (security config, key, certificate).""" + """ + Security paths for a name. + """ + sec: str = "" key: str = "" crt: str = "" @@ -219,7 +245,10 @@ class NameSecPaths: @dataclass class NameInfo: - """Information about a registered name.""" + """ + Information about a registered name. + """ + name: str pol_lb: LoadBalancePolicy = LoadBalancePolicy.ROUND_ROBIN server_sec: NameSecPaths = field(default_factory=NameSecPaths) @@ -228,23 +257,30 @@ class NameInfo: @dataclass class IpcpInfo: - """Information about a running IPCP (from list_ipcps).""" + """ + Information about a running IPCP. + """ + pid: int type: IpcpType name: str layer: str -# --- Internal conversion functions --- +def _ipcp_config_to_c(conf: IpcpConfig) -> Any: + """ + Convert an :class:`IpcpConfig` to a C ``struct ipcp_config *``. + + The layer name is truncated to the 255 bytes the C struct holds. + """ -def _ipcp_config_to_c(conf: IpcpConfig): - """Convert an :class:`IpcpConfig` to a C ``struct ipcp_config *``.""" _conf = ffi.new("struct ipcp_config *") - # Layer info layer_name = conf.layer_name.encode() + ffi.memmove(_conf.layer_info.name, layer_name, min(len(layer_name), 255)) + _conf.layer_info.dir_hash_algo = conf.dir_hash_algo _conf.type = conf.ipcp_type @@ -271,7 +307,9 @@ def _ipcp_config_to_c(conf: IpcpConfig): elif conf.ipcp_type in (IpcpType.ETH_LLC, IpcpType.ETH_DIX): ec = conf.eth or EthConfig() dev = ec.dev.encode() + ffi.memmove(_conf.eth.dev, dev, min(len(dev), 255)) + _conf.eth.ethertype = ec.ethertype elif conf.ipcp_type == IpcpType.UDP4: @@ -279,6 +317,7 @@ def _ipcp_config_to_c(conf: IpcpConfig): _conf.udp4.port = uc4.port if lib.ipcp_config_udp4_set_ip(_conf, uc4.ip_addr.encode()) != 0: raise ValueError(f"Invalid IPv4 address: {uc4.ip_addr}") + if lib.ipcp_config_udp4_set_dns(_conf, uc4.dns_addr.encode()) != 0: raise ValueError(f"Invalid IPv4 DNS address: {uc4.dns_addr}") @@ -287,45 +326,52 @@ def _ipcp_config_to_c(conf: IpcpConfig): _conf.udp6.port = uc6.port if lib.ipcp_config_udp6_set_ip(_conf, uc6.ip_addr.encode()) != 0: raise ValueError(f"Invalid IPv6 address: {uc6.ip_addr}") + if lib.ipcp_config_udp6_set_dns(_conf, uc6.dns_addr.encode()) != 0: raise ValueError(f"Invalid IPv6 DNS address: {uc6.dns_addr}") return _conf -def _name_info_to_c(info: NameInfo): - """Convert a :class:`NameInfo` to a C ``struct name_info *``.""" +def _name_info_to_c(info: NameInfo) -> Any: + """ + Convert a :class:`NameInfo` to a C ``struct name_info *``. + """ + _info = ffi.new("struct name_info *") name = info.name.encode() + ffi.memmove(_info.name, name, min(len(name), 255)) + _info.pol_lb = info.pol_lb for attr, sec in (('s', info.server_sec), ('c', info.client_sec)): sec_paths = getattr(_info, attr) + for fld in ('sec', 'key', 'crt'): val = getattr(sec, fld).encode() + ffi.memmove(getattr(sec_paths, fld), val, min(len(val), 511)) return _info -# --- IRM API functions --- - def create_ipcp(name: str, ipcp_type: IpcpType) -> int: - """Create a new IPCP. + """ + Create a new IPCP. :param name: Name for the IPCP. :param ipcp_type: Type of IPCP to create. :return: PID of the created IPCP. + :raises IpcpCreateError: If creation fails or the IPCP is not found. """ + raise_errno(lib.irm_create_ipcp(name.encode(), ipcp_type), default=IpcpCreateError) - # The C function returns 0 on success, not the pid. - # Look up the actual pid by name. for info in list_ipcps(): if info.name == name: return info.pid @@ -334,24 +380,32 @@ def create_ipcp(name: str, def destroy_ipcp(pid: int) -> None: - """Destroy an IPCP. + """ + Destroy an IPCP. :param pid: PID of the IPCP to destroy. + :raises IrmError: If the IPCP could not be destroyed. """ + raise_errno(lib.irm_destroy_ipcp(pid), default=IrmError) def list_ipcps() -> List[IpcpInfo]: - """List all running IPCPs. + """ + List all running IPCPs. :return: List of :class:`IpcpInfo` objects. + :raises IrmError: If the IPCPs could not be listed. """ + _ipcps = ffi.new("struct ipcp_list_info **") n = raise_errno(lib.irm_list_ipcps(_ipcps), default=IrmError) result = [] + for i in range(n): info = _ipcps[0][i] + result.append(IpcpInfo( pid=info.pid, type=IpcpType(info.type), @@ -366,22 +420,29 @@ def list_ipcps() -> List[IpcpInfo]: def enroll_ipcp(pid: int, dst: str) -> None: - """Enroll an IPCP in a layer. + """ + Enroll an IPCP in a layer. :param pid: PID of the IPCP to enroll. - :param dst: Name to use for enrollment. + :param dst: Name of a member of the layer to enroll with. + :raises IpcpEnrollError: If the IPCP could not enroll with *dst*. """ + raise_errno(lib.irm_enroll_ipcp(pid, dst.encode()), default=IpcpEnrollError) def bootstrap_ipcp(pid: int, conf: IpcpConfig) -> None: - """Bootstrap an IPCP. + """ + Bootstrap an IPCP. :param pid: PID of the IPCP to bootstrap. :param conf: Configuration for the IPCP. + :raises IpcpBootstrapError: If the IPCP could not be bootstrapped. """ + _conf = _ipcp_config_to_c(conf) + raise_errno(lib.irm_bootstrap_ipcp(pid, _conf), default=IpcpBootstrapError) @@ -390,17 +451,21 @@ def connect_ipcp(pid: int, component: str, dst: str, qos: Optional[QoSSpec] = None) -> None: - """Connect an IPCP component to a destination. + """ + Connect an IPCP component to a destination. :param pid: PID of the IPCP. - :param component: Component to connect (:data:`DT_COMP` or - :data:`MGMT_COMP`). + :param component: :data:`DT_COMP` or :data:`MGMT_COMP`. :param dst: Destination name. :param qos: QoS specification for the connection. + :raises IpcpConnectError: If *component* could not be + connected to *dst*. """ + _qos = qos_to_qosspec(ffi, qos) if _qos == ffi.NULL: _qos = ffi.new("qosspec_t *") + raise_errno( lib.irm_connect_ipcp(pid, dst.encode(), component.encode(), _qos[0]), @@ -411,12 +476,16 @@ def connect_ipcp(pid: int, def disconnect_ipcp(pid: int, component: str, dst: str) -> None: - """Disconnect an IPCP component from a destination. + """ + Disconnect an IPCP component from a destination. :param pid: PID of the IPCP. - :param component: Component to disconnect. + :param component: :data:`DT_COMP` or :data:`MGMT_COMP`. :param dst: Destination name. + :raises IpcpConnectError: If *component* could not be + disconnected from *dst*. """ + raise_errno( lib.irm_disconnect_ipcp(pid, dst.encode(), component.encode()), default=IpcpConnectError, @@ -427,17 +496,24 @@ def bind_program(prog: str, name: str, opts: int = 0, argv: Optional[List[str]] = None) -> None: - """Bind a program to a name. + """ + Bind a program to a name. :param prog: Path to the program. :param name: Name to bind to. :param opts: Bind options (e.g. :data:`BIND_AUTO`). - :param argv: Arguments to pass when the program is started. + :param argv: Arguments passed when the IRM starts the program. + :raises BindError: If *prog* could not be bound to *name*. """ + if argv: argc = len(argv) - _argv = ffi.new("char *[]", [ffi.new("char[]", a.encode()) - for a in argv]) + _args = [] + + for a in argv: + _args.append(ffi.new("char[]", a.encode())) + + _argv = ffi.new("char *[]", _args) else: argc = 0 _argv = ffi.NULL @@ -450,64 +526,85 @@ def bind_program(prog: str, def unbind_program(prog: str, name: str) -> None: - """Unbind a program from a name. + """ + Unbind a program from a name. :param prog: Path to the program. :param name: Name to unbind from. + :raises BindError: If *prog* could not be unbound from *name*. """ + raise_errno(lib.irm_unbind_program(prog.encode(), name.encode()), default=BindError) def bind_process(pid: int, name: str) -> None: - """Bind a running process to a name. + """ + Bind a running process to a name. :param pid: PID of the process. :param name: Name to bind to. + :raises BindError: If the process could not be bound to *name*. """ + raise_errno(lib.irm_bind_process(pid, name.encode()), default=BindError) def unbind_process(pid: int, name: str) -> None: - """Unbind a process from a name. + """ + Unbind a process from a name. :param pid: PID of the process. :param name: Name to unbind from. + :raises BindError: If the process could not be unbound from *name*. """ + raise_errno(lib.irm_unbind_process(pid, name.encode()), default=BindError) def create_name(info: NameInfo) -> None: - """Create a name in the IRM. + """ + Create a name in the IRM. :param info: :class:`NameInfo` describing the name to create. + :raises NameExistsError: If the name already exists. """ + _info = _name_info_to_c(info) + raise_errno(lib.irm_create_name(_info), default=NameExistsError) def destroy_name(name: str) -> None: - """Destroy a name in the IRM. + """ + Destroy a name in the IRM. :param name: The name to destroy. + :raises NameNotFoundError: If the name is unknown. """ + raise_errno(lib.irm_destroy_name(name.encode()), default=NameNotFoundError) def list_names() -> List[NameInfo]: - """List all registered names. + """ + List all registered names. :return: List of :class:`NameInfo` objects. + :raises IrmError: If the names could not be listed. """ + _names = ffi.new("struct name_info **") n = raise_errno(lib.irm_list_names(_names), default=IrmError) result = [] + for i in range(n): info = _names[0][i] + result.append(NameInfo( name=ffi.string(info.name).decode(), pol_lb=LoadBalancePolicy(info.pol_lb), @@ -530,20 +627,27 @@ def list_names() -> List[NameInfo]: def reg_name(name: str, pid: int) -> None: - """Register an IPCP to a name. + """ + Register an IPCP to a name. :param name: The name to register. :param pid: PID of the IPCP to register. + :raises NameNotFoundError: If the name is unknown. """ + raise_errno(lib.irm_reg_name(name.encode(), pid), default=NameNotFoundError) def unreg_name(name: str, pid: int) -> None: - """Unregister an IPCP from a name. + """ + Unregister an IPCP from a name. :param name: The name to unregister. :param pid: PID of the IPCP to unregister. + :raises NameNotFoundError: If the name or the registration + is unknown. """ + raise_errno(lib.irm_unreg_name(name.encode(), pid), default=NameNotFoundError) diff --git a/ouroboros/qos.py b/ouroboros/qos.py index f1b6d11..c42a62e 100644 --- a/ouroboros/qos.py +++ b/ouroboros/qos.py @@ -1,25 +1,11 @@ # -# Ouroboros - Copyright (C) 2016 - 2026 -# -# Python API for Ouroboros - QoS -# -# Dimitri Staessens -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public License -# version 2.1 as published by the Free Software Foundation. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., http://www.fsf.org/about/contact/. +# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens +# SPDX-License-Identifier: LGPL-2.1-only # -"""QoS specification for Ouroboros flows.""" +""" +QoS specification for Ouroboros flows. +""" from __future__ import annotations @@ -32,7 +18,10 @@ UINT64_MAX = 0xFFFFFFFFFFFFFFFF class QoSService(IntEnum): - """Mirrors ``enum qos_service`` in ``ouroboros/qos.h``.""" + """ + Mirrors ``enum qos_service`` in ``ouroboros/qos.h``. + """ + RAW = 0 # No FRCT; best-effort raw messages MESSAGE = 1 # FRCT, reliable ordered messages STREAM = 2 # FRCT, reliable ordered byte stream @@ -40,11 +29,8 @@ class QoSService(IntEnum): @dataclass(frozen=True) class QoSSpec: - """QoS specification for a flow. - - Frozen so the module-level ``QOS_*`` predefined cubes are safe to - share. Construct a new spec with ``dataclasses.replace(qos, ...)`` - or ``QoSSpec(...)`` to override fields. + """ + QoS specification for a flow. :ivar service: :class:`QoSService` (gates FRCT when > 0). :ivar delay: Maximum one-way delay in ms. @@ -55,6 +41,7 @@ class QoSSpec: :ivar max_gap: Maximum interruption in ms. :ivar timeout: Peer timeout in ms (default 120000 ms). """ + service: QoSService = QoSService.RAW delay: int = UINT32_MAX bandwidth: int = 0 diff --git a/pyproject.toml b/pyproject.toml index 9a9f8b6..d6f94c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens +# SPDX-License-Identifier: LGPL-2.1-only + [build-system] requires = ["setuptools>=64", "setuptools-scm>=8", "cffi>=1.0.0"] build-backend = "setuptools.build_meta" @@ -8,9 +11,7 @@ dynamic = ["version"] description = "Python API for Ouroboros" license = "LGPL-2.1-only" requires-python = ">=3.8" -authors = [ - { name = "Dimitri Staessens", email = "dimitri@ouroboros.rocks" } -] +authors = [{ name = "Dimitri Staessens", email = "dimitri@ouroboros.rocks" }] keywords = ["ouroboros", "IPC"] dependencies = ["cffi>=1.0.0"] @@ -23,8 +24,11 @@ packages = ["ouroboros"] [tool.setuptools_scm] [tool.pylint.main] -extension-pkg-allow-list = ["_ouroboros_dev_cffi", "_ouroboros_irm_cffi"] -ignored-modules = ["setuptools_scm", "_ouroboros_dev_cffi", "_ouroboros_irm_cffi"] +ignored-modules = [ + "setuptools_scm", + "_ouroboros_dev_cffi", + "_ouroboros_irm_cffi", +] [tool.pylint.format] max-line-length = 100 @@ -34,4 +38,3 @@ max-args = 10 max-positional-arguments = 10 max-attributes = 10 max-public-methods = 30 - diff --git a/setup.py b/setup.py index 6d8d160..a7b06e9 100755 --- a/setup.py +++ b/setup.py @@ -1,9 +1,15 @@ -"""Build script for the PyOuroboros CFFI extensions. +# +# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens +# SPDX-License-Identifier: LGPL-2.1-only +# + +""" +Build script for the PyOuroboros CFFI extensions. Resolves the ouroboros library version via ``pkg-config`` and the pyouroboros source version via ``setuptools_scm``, verifies that the -``major.minor`` halves match, and delegates the actual build to the -CFFI builders under ``ffi/``. +``major.minor`` halves match, and delegates the actual build to the CFFI +builders under ``ffi/``. """ from __future__ import annotations @@ -28,19 +34,21 @@ def _get_ouroboros_version() -> str: def _check_build_version_compat() -> None: + """ + Exit when the ouroboros and pyouroboros ``major.minor`` disagree. + + Returns without checking when the tree carries no SCM information. + """ + try: pyo7s_ver = get_version(root='.', relative_to=__file__) except (LookupError, OSError): - return # no SCM info, skip check + return o7s_ver = _get_ouroboros_version() - # setuptools_scm: '0.23.1.dev3+g' or '0.23.0' - # pkg-config: '0.23.0' - # Compare major.minor only. o7s_parts = o7s_ver.split('.') pyo7s_parts = pyo7s_ver.split('.') - if o7s_parts[0] != pyo7s_parts[0] or o7s_parts[1] != pyo7s_parts[1]: sys.exit( f"ERROR: Version mismatch: ouroboros {o7s_ver} " -- cgit v1.2.3