Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-organizing key dataclasses into a single model.py and then exposing those for import from prymer. #109

Merged
merged 3 commits into from
Jan 15, 2025

Conversation

tfenne
Copy link
Member

@tfenne tfenne commented Jan 14, 2025

Instead of having a module per class, this consolidates several classes including Strand, Span, Oligo, PrimerPair etc. into a single module. All the symbols are then re-exported so that they can be accessed via:

from prymer import Span
from prymer import Oligo
...

@tfenne tfenne requested a review from nh13 as a code owner January 14, 2025 23:04
Copy link

codecov bot commented Jan 14, 2025

Codecov Report

Attention: Patch coverage is 97.26562% with 7 lines in your changes missing coverage. Please review.

Project coverage is 97.32%. Comparing base (11390a5) to head (d5df89f).
Report is 1 commits behind head on dev.

Files with missing lines Patch % Lines
prymer/model.py 96.99% 5 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #109      +/-   ##
==========================================
- Coverage   97.37%   97.32%   -0.06%     
==========================================
  Files          23       21       -2     
  Lines        1677     1642      -35     
  Branches      198      195       -3     
==========================================
- Hits         1633     1598      -35     
  Misses         26       26              
  Partials       18       18              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link

coderabbitai bot commented Jan 14, 2025

Walkthrough

The pull request introduces a significant architectural refactoring of the prymer library, primarily focusing on consolidating model-related classes into a single prymer/model.py module. Multiple classes such as MinOptMax, Span, Oligo, PrimerPair, and Strand have been moved from various api submodules to this centralized location.

The changes involve removing several API modules like clustering.py, coordmath.py, minoptmax.py, oligo.py, oligo_like.py, primer.py, primer_pair.py, and span.py. Import statements across the project have been systematically updated to reflect these structural changes.

Additionally, the project's Python version requirement has been upgraded from 3.11 to 3.12, and the version number has been incremented to 3.0.0, signaling a major architectural revision. A new dependency on primer3-py has also been added to the project configuration.

Finishing Touches

  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (7)
tests/conftest.py (1)

31-38: Add docstring and consider session scope.

Fixture needs documentation. Consider session scope if dictionary is immutable.

 @pytest.fixture
+@pytest.fixture(scope="session")
 def seq_dict() -> SequenceDictionary:
+    """Returns a SequenceDictionary with three chromosomes for testing."""
     metadatas: list[SequenceMetadata] = [
prymer/model.py (2)

27-30: Clarify inequality conditions in docstring

In the MinOptMax class docstring, the constraints can be more precisely stated. Consider rephrasing as:

"min must be less than or equal to opt, and opt must be less than or equal to max."

This clarifies the allowable equality in the conditions.


707-707: Specify strand when creating amplicon Span

In calculate_amplicon_span, the strand defaults to Strand.POSITIVE. If strand orientation is significant for the amplicon, explicitly set the strand based on primer strands.

prymer/api/coordmath.py (1)

1-2: Update module docstring.

Current docstring is too vague after removal of methods.

-# Methods for coordinate-based math.
+"""Methods for calculating coordinate-based intervals and positions."""
tests/primer3/test_primer3_input_tag.py (1)

10-11: Document Python version requirement.

Convert comment to docstring to better document the version constraint.

-        # This is supported in python 3.12+, but not in 3.11
-        # assert f"{tag}" in Primer3InputTag
+        """
+        Note: The following assertion is only supported in Python 3.12+:
+        assert f"{tag}" in Primer3InputTag
+        """
prymer/primer3/primer3_input_tag.py (1)

32-33: Move developer note to class docstring.

Important implementation detail should be in class docstring for better visibility.

 class Primer3InputTag(UppercaseStrEnum):
     """
     Enumeration of Primer3 input tags.
 
     Please see the Primer3 manual for additional details:
      https://primer3.org/manual.html#commandLineTags
 
     This class represents two categories of Primer3 input tags:
      * `SEQUENCE_` tags are those that control sequence-specific attributes of Primer3 jobs.
        These can be modified after each query submitted to Primer3.
      * `PRIMER_` tags are those that describe more general parameters of Primer3 jobs.
        These attributes persist across queries to Primer3 unless they are explicitly reset.
        Errors in these "global" input tags are fatal.
+
+    Important: sequence-specific tags must be specified prior to global tags
     """
-    # Developer note: sequence-specific tags must be specified prior to global tags
prymer/primer3/primer3.py (1)

247-248: Remove unnecessary super().__exit__ call

AbstractContextManager.__exit__ does nothing. Removing this call simplifies the code.

Apply this diff:

-            super().__exit__(exc_type, exc_value, traceback)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa21269 and 69f0a5e.

⛔ Files ignored due to path filters (1)
  • poetry.lock is excluded by !**/*.lock
📒 Files selected for processing (39)
  • .github/workflows/tests.yml (2 hunks)
  • .github/workflows/wheels.yml (1 hunks)
  • prymer/__init__.py (1 hunks)
  • prymer/api/__init__.py (0 hunks)
  • prymer/api/clustering.py (0 hunks)
  • prymer/api/coordmath.py (1 hunks)
  • prymer/api/minoptmax.py (0 hunks)
  • prymer/api/oligo.py (0 hunks)
  • prymer/api/oligo_like.py (0 hunks)
  • prymer/api/picking.py (2 hunks)
  • prymer/api/primer.py (0 hunks)
  • prymer/api/primer_pair.py (0 hunks)
  • prymer/api/span.py (0 hunks)
  • prymer/api/variant_lookup.py (1 hunks)
  • prymer/model.py (1 hunks)
  • prymer/offtarget/offtarget_detector.py (2 hunks)
  • prymer/primer3/primer3.py (14 hunks)
  • prymer/primer3/primer3_input.py (2 hunks)
  • prymer/primer3/primer3_input_tag.py (1 hunks)
  • prymer/primer3/primer3_parameters.py (1 hunks)
  • prymer/primer3/primer3_task.py (2 hunks)
  • pyproject.toml (4 hunks)
  • tests/api/conftest.py (0 hunks)
  • tests/api/test_clustering.py (0 hunks)
  • tests/api/test_coordmath.py (1 hunks)
  • tests/api/test_oligo_like.py (0 hunks)
  • tests/api/test_picking.py (1 hunks)
  • tests/api/test_variant_lookup.py (1 hunks)
  • tests/conftest.py (2 hunks)
  • tests/model/test_minoptmax.py (1 hunks)
  • tests/model/test_oligo.py (9 hunks)
  • tests/model/test_primer_pair.py (2 hunks)
  • tests/model/test_span.py (1 hunks)
  • tests/offtarget/test_offtarget.py (1 hunks)
  • tests/primer3/test_primer3.py (7 hunks)
  • tests/primer3/test_primer3_input.py (1 hunks)
  • tests/primer3/test_primer3_input_tag.py (1 hunks)
  • tests/primer3/test_primer3_parameters.py (1 hunks)
  • tests/primer3/test_primer3_task.py (1 hunks)
💤 Files with no reviewable changes (11)
  • tests/api/conftest.py
  • prymer/api/primer.py
  • prymer/api/primer_pair.py
  • prymer/api/init.py
  • prymer/api/oligo.py
  • tests/api/test_oligo_like.py
  • prymer/api/clustering.py
  • prymer/api/minoptmax.py
  • prymer/api/oligo_like.py
  • tests/api/test_clustering.py
  • prymer/api/span.py
✅ Files skipped from review due to trivial changes (12)
  • tests/model/test_minoptmax.py
  • prymer/primer3/primer3_input.py
  • tests/primer3/test_primer3_task.py
  • prymer/api/variant_lookup.py
  • tests/api/test_variant_lookup.py
  • tests/primer3/test_primer3_parameters.py
  • prymer/primer3/primer3_task.py
  • tests/offtarget/test_offtarget.py
  • tests/api/test_picking.py
  • prymer/primer3/primer3_parameters.py
  • prymer/api/picking.py
  • prymer/offtarget/offtarget_detector.py
🔇 Additional comments (24)
tests/conftest.py (1)

8-12: Import changes look good.

Imports align with the PR objective of consolidating dataclasses into model.py.

prymer/model.py (2)

298-304: Handle missing reference names in _to_tuple

In the Span method _to_tuple, accessing seq_dict.by_name(self.refname).index may raise a KeyError if self.refname is not in seq_dict. Validate that self.refname exists before accessing to prevent unexpected errors.


569-573: Catch exceptions during amplicon computation

In PrimerPair.__post_init__, calling self.amplicon may raise a ValueError if primers are misconfigured. Consider handling exceptions to provide clearer messages or validate inputs before computing the amplicon.

prymer/__init__.py (1)

1-7: Exports enhanced for user convenience

Re-exporting MinOptMax, Oligo, PrimerPair, Span, and Strand directly from prymer simplifies imports for users, aligning with the PR objectives.

tests/api/test_coordmath.py (1)

1-7: Ensure complete test coverage after removals

With the removal of tests for get_locus_string and require_same_refname, verify that all remaining functions in coordmath are adequately tested. Consider adding tests for any untested functions.

prymer/api/coordmath.py (1)

Line range hint 1-24: Verify removed methods.

Confirm that require_same_refname() and get_locus_string() were moved to model.py.

tests/primer3/test_primer3_input.py (1)

3-5: LGTM!

Import paths correctly updated to use the new model module.

tests/model/test_span.py (2)

Line range hint 1-300: LGTM!

Test coverage remains comprehensive after restructuring. All key Span functionality is well-tested.


4-5: Verify BedLikeCoords removal impact.

Confirm that BedLikeCoords functionality is either deprecated or moved to model.py.

✅ Verification successful

BedLikeCoords functionality consolidated into Span class.

Span class now handles all coordinate operations with improved functionality. Removal is safe.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining BedLikeCoords references
rg "BedLikeCoords" 

Length of output: 18


Script:

#!/bin/bash
# Check git history for BedLikeCoords
git log -S "BedLikeCoords" --oneline

# Examine current model.py content
cat prymer/model.py

Length of output: 28053

prymer/primer3/primer3_input_tag.py (2)

34-37: LGTM!

Clean implementation with clear docstring.


39-42: LGTM!

Logical implementation leveraging is_sequence_arg.

tests/model/test_oligo.py (2)

9-11: LGTM!

Import changes align with module reorganization.


196-196: LGTM!

Property access is more Pythonic than method calls.

Also applies to: 248-248

tests/model/test_primer_pair.py (1)

9-12: LGTM!

Import changes align with module reorganization.

tests/primer3/test_primer3.py (4)

10-14: LGTM!

Import changes align with module reorganization.


162-162: LGTM!

More specific error message improves debugging.


190-190: LGTM!

Property access is more Pythonic than method calls.

Also applies to: 237-237, 448-448, 452-452, 469-469


364-364: LGTM!

Error message better reflects the I/O nature of the failure.

.github/workflows/wheels.yml (1)

14-15: Clarify Python version support strategy.

Dropping Python 3.11 support needs explanation. If temporary, add timeline for re-enabling.

.github/workflows/tests.yml (2)

19-20: Match Python version strategy with wheels.yml.

Version matrix changes should be synchronized with wheels.yml.


60-63: Add timeline for re-enabling documentation build.

Disabled documentation builds risk documentation drift. Set deadline for re-enabling.

pyproject.toml (2)

121-122: Confirm primer3 type hints.

Added primer3 to mypy ignores. Check if type hints are available.

✅ Verification successful

External primer3-py package type hints can be safely ignored.

Local primer3 implementation has complete type hints. Mypy ignore is for external dependency.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for primer3 type hints
pip show primer3-py | grep -i "typing"

Length of output: 132


Script:

#!/bin/bash
# Search for primer3 type hint files and usage
echo "=== Searching for .pyi files ==="
fd -e pyi primer3

echo "=== Checking for type hints usage ==="
rg "from typing import|import typing" -A 2 | grep -i primer3

echo "=== Looking for primer3 imports with type annotations ==="
rg "primer3.*:.*" -A 2

Length of output: 8714


35-35: Verify dependency compatibility.

New Python 3.12 requirement and primer3-py addition need compatibility verification.

Also applies to: 42-42

✅ Verification successful

Python 3.12 and primer3-py are compatible.

primer3-py 2.0.3 has official Python 3.12 wheels.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check dependency compatibility
poetry check
poetry lock --check

Length of output: 230


Script:

#!/bin/bash
# Find requirements files and check their content
fd requirements.txt --exec cat {}

# Find primer3-py imports and usage
rg "primer3" -l

# Check setup.py if exists
fd setup.py --exec cat {}

# Look for primer3 in install_requires
rg "install_requires" -A 5

Length of output: 900


Script:

#!/bin/bash
# Check poetry.lock for primer3-py version
cat poetry.lock | grep -A 10 "primer3-py"

# Check installation docs
cat docs/installation-and-developers-documentation.md

# Check pyproject.toml dependencies section
cat pyproject.toml | grep -A 10 "\[tool.poetry.dependencies\]"

Length of output: 5466

prymer/primer3/primer3.py (1)

123-123: Ensure 'Self' type is compatible

The Self type hint is available in Python 3.11 and above. If earlier versions need support, replace Self with the class name in quotes.

pyproject.toml Outdated
@@ -1,6 +1,6 @@
[tool.poetry]
name = "prymer"
version = "2.2.0"
version = "3.0.0"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Add CHANGELOG.md to document breaking changes

Version 3.0.0 indicates breaking changes. Create CHANGELOG.md in repository root to document them.

🔗 Analysis chain

Document breaking changes for version 3.0.0.

Major version bump requires changelog entry detailing breaking changes.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for CHANGELOG updates
rg -i "version.*3\.0\.0|breaking.*changes" CHANGELOG.md || echo "No changelog entry found for 3.0.0"

Length of output: 189


Script:

#!/bin/bash
# Search for changelog files in various formats
fd -i 'changelog|changes|history|news' --type f

Length of output: 49

Comment on lines 238 to 239
def __enter__(self) -> Self:
return self
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Adjust return type in __enter__ method

Replace -> Self with -> 'Primer3' for compatibility with Python versions before 3.11.

Apply this diff:

-    def __enter__(self) -> Self:
+    def __enter__(self) -> 'Primer3':
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def __enter__(self) -> Self:
return self
def __enter__(self) -> 'Primer3':
return self

Comment on lines 753 to 759
max_dinuc_bases: int = -1
if design_input.task.requires_primer_amplicon_params:
max_dinuc_bases = design_input.primer_and_amplicon_params.primer_max_dinuc_bases
elif design_input.task.requires_probe_params:
max_dinuc_bases = design_input.probe_params.probe_max_dinuc_bases

return oligo_design.longest_dinucleotide_run_length() <= max_dinuc_bases
return oligo_design.longest_dinucleotide_run_length <= max_dinuc_bases
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle uninitialized max_dinuc_bases

If neither condition is met, max_dinuc_bases remains -1, leading to incorrect comparisons. Add an else clause to raise an error.

Apply this diff:

     elif design_input.task.requires_probe_params:
         max_dinuc_bases = design_input.probe_params.probe_max_dinuc_bases
+    else:
+        raise ValueError("Design task must require primer or probe parameters.")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
max_dinuc_bases: int = -1
if design_input.task.requires_primer_amplicon_params:
max_dinuc_bases = design_input.primer_and_amplicon_params.primer_max_dinuc_bases
elif design_input.task.requires_probe_params:
max_dinuc_bases = design_input.probe_params.probe_max_dinuc_bases
return oligo_design.longest_dinucleotide_run_length() <= max_dinuc_bases
return oligo_design.longest_dinucleotide_run_length <= max_dinuc_bases
max_dinuc_bases: int = -1
if design_input.task.requires_primer_amplicon_params:
max_dinuc_bases = design_input.primer_and_amplicon_params.primer_max_dinuc_bases
elif design_input.task.requires_probe_params:
max_dinuc_bases = design_input.probe_params.probe_max_dinuc_bases
else:
raise ValueError("Design task must require primer or probe parameters.")
return oligo_design.longest_dinucleotide_run_length <= max_dinuc_bases

@dataclass(init=True, slots=True, frozen=True)
class Primer3Result(Generic[OligoLikeType]):
class Primer3Result[ResultType: (Oligo, PrimerPair)]:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix invalid generic class syntax

The declaration class Primer3Result[ResultType: (Oligo, PrimerPair)]: is invalid. Use TypeVar and Generic from the typing module to define a generic class correctly.

Apply this diff:

+ from typing import TypeVar, Generic
+ ResultType = TypeVar('ResultType', Oligo, PrimerPair)
- class Primer3Result[ResultType: (Oligo, PrimerPair)]:
+ class Primer3Result(Generic[ResultType]):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class Primer3Result[ResultType: (Oligo, PrimerPair)]:
from typing import TypeVar, Generic
ResultType = TypeVar('ResultType', Oligo, PrimerPair)
class Primer3Result(Generic[ResultType]):

@nh13 nh13 changed the base branch from main to dev January 15, 2025 04:03
Copy link
Member

@nh13 nh13 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving assuming that the imports from prymer.model will get changed to from prymer.

Comment on lines -10 to -11
- [`get_closed_end()`][prymer.api.coordmath.get_closed_end] -- gets the closed end of an
interval given its start and length.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we keep this doc?

Comment on lines +29 to +32
from prymer.model import MinOptMax
from prymer.model import Oligo
from prymer.model import PrimerPair
from prymer.model import Span
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we prefer importing from the top-level, here and elsewhere? Or is there a reason for the preference to import it from model?

Suggested change
from prymer.model import MinOptMax
from prymer.model import Oligo
from prymer.model import PrimerPair
from prymer.model import Span
from prymer import MinOptMax
from prymer import Oligo
from prymer import PrimerPair
from prymer import Span

@@ -11,7 +11,7 @@

```python
>>> from pathlib import Path
>>> from prymer.api.span import Strand
>>> from prymer.model import Strand
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in the docs we should import these as we would recommend users to import them, namely from the top-level prymer namespace. If you agree, can you fix in the rest of the modules as well?

Suggested change
>>> from prymer.model import Strand
>>> from prymer import Strand

@tfenne
Copy link
Member Author

tfenne commented Jan 15, 2025

Answering here @nh13 rather than to individual comments. On the imports - it is unfortunately necessary that within the prymer codebase itself that we use imports from the module where the code is rather than where it's re-exported to. If we were to e.g. use from prymer import Span within prymer you end up very quickly with circular imports where e.g. prymer.primer3 wants to use prymer.Span which requires initializing prymer ... which also e.g. re-exports symbols from prymer.primer3, etc. etc.

That said, all examples and doc-tests can be updated to use the re-exported location(s) and I've done that. Additionally tests can import from the top level too, and I have also updated those.

@tfenne tfenne merged commit a40056b into dev Jan 15, 2025
3 of 5 checks passed
@tfenne tfenne deleted the tf_reorg_phase1 branch January 15, 2025 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants